Pwnable.kr brain_fuck
程序分析
就是一个 Brainfuck语言 的模拟器(这名字有点搞的...)
首先是 main 方法,就是读取用户输入,然后根据输入一个字符一个字符进行处理。然后处理方法就是 do_brainfuck 函数。
程序一个全局指针 p,即它不是栈上的变量,每次执行时都是固定的。漏洞点很简单,就是可以移动指针往负方向移动,并且可以写入内容。也就是我们可以控制全局指针前面的一段内存。
int main() {
...
p = (int)&tape;
puts("welcome to brainfuck testing system!!");
puts("type some brainfuck instructions except [ ]");
memset(s, 0, 1024u);
fgets(s, 1024, stdin);
for ( i = 0; i < strlen(s); ++i )
do_brainfuck(s[i]);
return 0;
}
int do_brainfuck(char a1)
{
...
switch ( a1 ) {
case 43:
result = p;
++*(_BYTE *)p;
break;
case 44:
v2 = (_BYTE *)p;
result = getchar();
*v2 = result;
break;
case 45:
result = p;
--*(_BYTE *)p;
break;
case 46:
// 向指针指向的地址写入内容
result = putchar(*(char *)p);
break;
case 60:
// 指针可以反方向移动, 没有检查, 所以有漏洞
result = p-- - 1;
break;
case 62:
result = p++ + 1;
break;
case 91:
result = puts("[ and ] not supported.");
break;
default:
return result;
}
return result;
}
具体做法
现在我们可以写入一部分内存内容,观察发现竟然可以写入 GOT 表。首先要泄露 libc 地址,这个比较简单,因为我们现在相当于对于指针指向的内容可读可写,有两种方法。通过泄露 STDIN@GLIBC2.0 或者通过泄露 GOT 表的某个函数。
现在常规做法是我们把 GOT 表的某个函数改成 system,但是有个严重问题:我们要传入参数( /bin/sh ),但没啥好方法能在执行篡改的 system 时也能控制参数为 /bin/sh
所以这就是这道题值得写的地方:利用 main 函数。比如把 GOT 表的某个函数改成 main 方法,看看是否可以通过修改 main 方法来控制参数。
可以发现 main 最开始有 memset(s, 0, 1024u) 和 fgets(s, 1024, stdin),这两个似乎可以使用,但是又似乎不能使用,因为我们还是控制不了 s 这个变量,s 是栈上变量,memset 无法控制它。
而 fgets 只有一个,要是 fgets 后面再来一个被修改成 system 的函数多好。该死,要是先 fgets 再 memset 多好!!这样就可以先通过 fgets 把 s 变为 /bin/sh, 然后由于 memset 的 GOT 事先已经改为了 system, 所以执行 memset(s) 就相当于 system('/bin/sh') 了.
为什么不能这样呢?对啊,为什么不能这样呢!!既然不给我们这个,那我们就去创造这个!!
我们反正可以修改 GOT 表内容,那么这就简单了,把 memset 改成 gets,把 fgets 改成 system !注意要改的是 gets 而不是 fgets,如果改成 fgets,那么就是 fgets(s, 0, 1024u),显然这不符合 fgets 的用法,无法读取输入(正确的是 fgets(s, 1024, stdin))。
最后因为我们要重新执行一次 main 方法,所以随便改个 GOT 表里的函数,我选的是把 putchar 改成 main_addr。
总结知识
-
STDIN@GLIBC2.0 是什么
这个可以理解成 plt 那一套,这里面存的是 IO_2_1_stdin 那个结构体的起始位置。所以比如fgets(ptr, 1024, stdin),最后一个参数就是使用STDIN@@GLIBC2.0等价了 stdin 。 -
破解的关键?
第一,把putchar改成main,让main执行两次,从而利用main方法开头的地方。
第二,程序执行逻辑memset -> fgets,强行修改成gets -> memset。