Skip to content

Pwnable.kr tiny_easy 记录

复制本地路径 | 在线编辑

这道题现在回头来看不难,但是做的过程一步一步学习到的知识很多,也很有收获,因此题目写了记录二次,这个不能算是题解,算是还原了我当时是怎么做的过程。

给我自己的话:直接去看总结反思。

程序分析

在 gdb 中查看执行流,可以看到最后会执行程序名。比如程序名为 /home/allen/AAA ,那么最后就会 call 0x6d6f682f("/hom")

漏洞点

argv[0] 与 execve

很明显,我们需要改变程序名。然而程序名这怎么可能改变,陷入死路。所以这就是学习到的知识点了。

实际上程序名是 argv[0] ,我们要改变的是 argv[0] 。
argv[0] 是程序名只是约定俗称的事情,我们可以让它变成不一样的东西!
怎么变?使用 execve()

// 只要传入argv,那么argv[0]就是我们传入的参数,而不是程序名称
int execve(const char * filename, char * const argv[ ], char * const envp[ ]);

Heap Spray

那么现在的思路是什么?这个又卡住了,参考了网上的题解:将 shellcode 传入 argv 列表中,这样栈上就有 shellcode ,然后把 argv[0] 改成该 shellcode 地址即可。

关键...栈的位置随时变,我们不能准确知道 shellcode 的确定位置。
所以:Heap Spray,该你上场了!

方法就是 shellcode 不仅仅就传入 argv 的某一项,而是传入很多项。比如传入 50 个,这样 argv[1-50] 就全是 shellcode,栈里面就有许多许多 shellcode ,这样我们打入地址的成功概率将大大增加。

调试过程

argv[0] 与 pwntools.gdb

现在的想法就很自然了:能否使用 pwntoolsgdb 模块,这样可以便于调试。然而事实是不行的。

首先是 GDB DEBUG 使用方式

# 最普通的方式
p = gdb.debug('./my_program')

# 加入参数
p = gdb.debug(['./my_program', 'AAAA', 'BBBB'])

可以看到 argv[0] 必须是程序名,如果我把 argv[0] 强制改成别的会如何?

p = gdb.debug("./tiny_easy")
['/bin/gdbserver', '--multi', '--no-disable-randomization', 'localhost:0', './tiny_easy']

p = gdb.debug(['AAAA', 'BBBB', 'CCCC'])
['/bin/gdbserver', '--multi', '--no-disable-randomization', 'localhost:0', 'AAAA', 'BBBB', 'CCCC']

可以看到 GDB DEBUG 是利用 gdbserver 来启动的,反正最后 argv[0] 必须要是程序名,否则就不能启动了

当然如果不涉及 gdb,那么 python 还是和 c 语言一样,可以控制 argv,使用 process(argv, './my_program') 即可。

gdb 如何跟踪 execve 的子程序

所以老老实实编写 C 程序,在该程序调用我们的 tiny_easy。

这里面就要跟踪 C 程序执行的 execve 语句,使用 catchpoint

利用 python 加快 gdb 交互

假设我们的程序为 debug,那么我们使用 gdb debug 来看调用过程。

如果我们定向的栈地址没有成功,那只能结束掉本次的 gdb,然后重新再来一次。可怕的是,这个成功率其实特别低,而且就算成功了,我们也很可能没有完完全全保证说通过这一次我就完全了解了过程,很有可能需要好几次才真正理解。

所以,要使用脚本,在循环里面判断,那么问题来了,python 怎么和 gdb 打交道?

实际上和执行普通的程序是一样的呀。代码胜千言,看代码!代码进行了简化,反正大致意思就是和执行普通程序进行交互就可以。

for i in range(0x1000):
    p = process(['gdb', 'debug'])

    p.recvuntil('...')
    p.sendline('r')
    p.recvuntil('...')
    stack_lo = int( p.recvuntil('...', drop=True), 16 )

    p.sendline('vmmap')
    p.recvuntil('...')
    stack_hi = int( p.recvuntil('...', drop=True), 16 )

    # 如果成功就会收到相关信息,如果失败就会有 EOF_ERROR
    # 成功,进入交互,一定要来个 interactive()
    if (0xffbce630 < stack_hi and 0xffbce630 > stack_lo):
        p.interactive()
    else:
        p.close()

C 语言的注意点

C 语言处理 16 进制字符串

warning: hex escape sequence out of range

对于这样的语句 char *test = "\x90\x901"; ,这样会编译出错。因为 C 语言并没有规定 \x 后面只进行两位数转义,所以他相当于转义了901,而这会超出范围,关键是他还不报错只报警告,后续会导致一些问题!

所以最好的方式是 char *test = "\x90\x90""1";,每个特殊转义末都加个引号隔开。

execve 需要最后的 args 为 NULL

否则将会执行失败。

总结反思

  1. 学到的 pwn 知识

  2. 看到程序名就要想到 argv[0] ,对于 argv[0] ,就要想到可以使用 execve 来改变

  3. 使用 argv 列表将 shellcode 传入栈中,并且使用 Heap Spray 增加成功率

  4. 学到的 gdb 知识

  5. 如何跟踪 execve 的子程序:使用 catchpoint

  6. 学到的 python 知识

  7. 对于 pwntools.debug ,它的原理是通过 gdbserver 实现,并且要求 argv[0] 必须是程序名

  8. 对于某个程序,想用 gdb 执行它,怎么利用 python 进行交互?

  9. 学到的 c 知识

  10. C 程序处理 16 进制字符串要小心

  11. execve 需要最后的 args 为 NULL