Pwnable tw 3x17
需要掌握的知识点
- 静态编译和动态编译,以及Stripped
- Linux 程序执行流程(main 函数是如何执行起来的)
- 64 Bit SystemCall in Assembly
漏洞分析
Stage 1
首先很不幸,IDA Pro分析程序,发现全是SUB_XXX的样式。所以应该先恢复符号表,这个请看另外一篇文章。
Stage 2
恢复完成,代码如下。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
int v4; // eax
char *v5; // ST08_8
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 cookie; // [rsp+28h] [rbp-8h]
cookie = __readfsqword(0x28u);
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
write(1u, "addr:", 5uLL);
read(0, &buf, 0x18uLL);
sub_40EE70((__int64)&buf);
v5 = (char *)v4;
write(1u, "data:", 5uLL);
read(0, v5, 0x18uLL);
result = 0;
}
return result;
}
分析代码,现在唯一的问题就是这个
SUB_40EE70,可以看到在他函数之后,他将 eax 赋给了另外一个变量。而我们知道在64Bit中,eax 存放的是返回值,所以进入 GDB 调试。
查看的情况就不贴图了,输入 100,发现经过这个函数之后 eax 为0x64,所以说这个函数功能就是把字符串参数转换为数值。这个函数我原来以为是自创的。后来查阅得知这个函数就是 strtol (想一下atoi,是不是很像,就是属于XtoY的一类)。
Stage 3
那么程序的逻辑清楚了: 输入一个字符串, 程序将其转为数值, 之后可以对这个数值指向的内存进行写入。但是关键在于只能执行一次任意写,只有一次修改这种威力完全不够。所以怎么可以多次进行任意写呢?
答案: fin_array, 查阅代码,观察_libc_csu_fini。
__int64 _libc_csu_fini()
{
signed __int64 index; // rbx
// 右移3位,即除以8,因为前面的那个是1-Byte排序,而index要的是4-Byte排序
// (4B4100 - 4B40F0) / 8 = 2,所以该函数一共执行2次子函数,即fin_arr[1], fin_arr[0]
if ( (&unk_4B4100 - (_UNKNOWN *)off_4B40F0) >> 3 )
{
index = ((&unk_4B4100 - (_UNKNOWN *)off_4B40F0) >> 3) - 1;
do
off_4B40F0[index--]();
while ( index != -1 );
}
return sub_48E32C();
}
注意fin_array是逆序执行,fin_array[1] 更改为main函数,然后就可以继续执行main函数,然后进行多次任意写了。
你要问我为什么不把fin_array[0]改为main函数,事实上它要改成_libc_csu_fini(),即利用它重新执行该fin_arr[1]。
如果把fin_arr[0]改成main函数,那么执行完两次main函数,_libc_csu_fini()就结束了,可以看到 main 方法中只有flag == 1 才会执行,所以执行三次 main 函数丝毫不会有影响。只有不断执行 main 函数,让flag 溢出回到1,这样才OK。
Stage 4
所以就先把 fin_array 改掉,然后可以任意写之后,构造 ret2syscall 咯。现在问题是如何确保最后执行了 ret2syscall,最后一次返回地址改掉就好了哇。
坑点
第一次应该怎么写,可以确保有第二次写的出现?
我本来是打算,写成fin_array[1] = main,然后第二次再进行fin_array[0] = libc_csu_fin,但是结果失败。
原因在于,虽然确实执行了main方法,但是里面(flag == 2),因此不会执行写操作。
正确做法是,一口气把fin_array全部写完,这样才能保证会不断循环,然后有了第二次写的出现
总结反思
- 经验
- 只有一次写的机会,可以尝试修改fin_array