[pwnable.kr] echo2 (Rewritten with .md)

[pwnable.kr] echo2 (Rewritten with .md)

本文是用markdown加上wordpress的插件WP Editor.md重写而成。
wordpress的显示与Editor.md预览的完全不一样,非常坑爹。
代码部分沿用了Crayon插件,style.css的正文部分(.entry-content)经过大量的修改。
终于改到差不多满意。
2018.11.27, 凌晨1:35


pwnable.kr: echo2 地址

Pwn this echo service.
download : http://pwnable.kr/bin/echo2
Running at : nc pwnable.kr 9011

这道题虽然不是很难,但是因为有一些细节(甚至与pwn这道题没什么太大关系)我觉得比较重要。要是不弄清楚就会觉得很费解。另外,网上的其他writeup似乎都没有解释某些问题。于是我这次就写详细一点(不容易啊!)。

首先看看IDA F5的结果。

checksec看一下,这个程序什么保护都没有开。于是难题就剩下了ASLR。

配合着反编译的结果运行一下程序,可以有这么几个印象:

  • 选项1. BOF是没有用的。
  • 需要利用2. FSB3. UAF
  • FSB要泄露或者修改哪一部分内存呢?
  • UAF利用的是哪个之前free的内存呢?
  • Shellcode放在哪里呢?

基于第四个问题,我们可以去找整个程序中malloc的地方。除了echo3里面之外,应该就只有

那这个o什么时候被free了呢?看下面

这个程序的逻辑是,你选了4. exit之后,不管你后来是选y还是n,它反正先free掉了o了。因此,你会看到这样的现象:选了4. exit,然后选n回来主程序,然后再选一次4. exit,马上就会出现double free的错误。所以UAF就是要利用这个被free掉,程序却还可以不退出的漏洞。

接下来就想,我通过UAF获得了与o同样的一块内存之后,我能做什么。

重点应该是在于o的那块内存里存着两个函数的指针。于是利用UAF干什么就很清楚了:得到那块内存的控制权之后,修改这里面函数的地址,那么等下一次对应的函数要调用的时候,就可以去执行shellcode了。

然后就到了第五个问题,shellcode放在哪里呢?这个问题就看哪些地方可以输入。name可以输入24B,选项可以输入1B,echo2可以输入32B(为什么说“约”,等下再说),echo3可以输入32B。

然后再分析一下获取他们地址的难度,因为有ASLR。name就放在main函数的栈帧里面,有FSB的话还是比较好获得的。echo2的输入是在echo2的栈帧里面,有FSB也是可以获得的,但是由于echo2的栈帧底下存的是main函数的ebp,因此FSB泄露main函数栈帧的地址比较方便一点点。而echo3的地址是存在堆里面的,好像难以获得其地址。

在这样的分析之下,可以考虑将shellcode存在name里面。因为name只能输入24B,因此要找一个比较短的shellcode。https://www.exploit-db.com/exploits/36858/ 这是一个很多人采用的只有23B的shellcode,又短又好用。

这样的话FSB的目标就很明显了:泄露main函数的ebp,然后计算name的地址[rbp-20h]

然后UAF的目标也很明明显,把某个函数的地址用name的地址覆盖掉。等下次调用对应函数的时候,存在那个内存的地址就已经是我们shellcode的地址,因此我们就能成功getshell。

现在还有个问题就是覆盖哪个函数的问题。可以看到echo3malloc申请的内存大小为32B,原本o的大小为40B。greetings函数存在24B~32B之间,byebye函数存在32B~40B之间。通过GDB调试可以知道在UAF的过程中,前后两次申请内存的指针是一致,这说明我们只能控制原来内存的前32B,我们根本无法覆盖byebye函数的地址,我们只能覆盖greetings这一点是很多writeup都没有指出来的。

下面讲讲两个技术要点:

  1. FSB泄露main_ebp
  2. UAF修改greetings的地址

FSB泄露main_ebp

Step 1. name输入AAAABBBB,然后观察main_ebpname的关系。

echo_2_1

其实从IDA里面也能看出来。

不过我总是觉得GDB比较直观和可靠。总之我们能得到,name_Addr=main_ebp-0x20

Step 2. 确定FSB的偏移,在FSB输入的时候输入:AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p …

echo_2_2

数一下,AAAAAAAA4141414141414141的距离。可见到printf函数栈中的format字符串有6*8个字节的偏移。至于是从哪里到format字符串的偏移呢?

这一点与x64的函数调用约定有关。x86的参数全部采用栈进行传递,但是x64前几个参数是采用寄存器进行传递的。这就是这几个偏移的来源。

再观察一下栈,format字符串距离printf栈帧存着的ebp值 (实际上是main函数的ebp) 有4*8个字节的偏移。因此用%10$p就可以泄露main_ebp

echo_2_3

但是实际上%9$p也是可以的,为什么呢?其实它存了两份main_ebp,只不过上面那份被我们很长的%p给覆盖了而已,如下图。

echo_2_3

于是根据我们泄露出来的main_ebp就可以计算出name的地址name_Addr了。


UAF修改greetings的地址

Step 1. 执行4. exit,然后选择n返回。这样程序没有退出,但是却free了原来的o

Step 2. 执行3. UAF。用paddings覆盖前24B,然后剩下8B用name_Addr覆盖。

似乎没什么难度。


exp如下:

echo_2_4


通常呢,writeup随着cat flag的出现也就结束了。但是这里面会有一些让人百思不得其解的地方。如果真的深入思考,肯定会发现的。但是居然网上的writeup都没有提到过这些问题。所以我还是要理清一下这些问题。

A. 通过echo3我们真的能控制malloc的前32B吗?

echo3程序里面用了get_input()进行输入,而get_input()的实现是通过fgets()实现的。

fgets: 从文件结构体指针stream中读取数据,每次读取一行。读取的数据保存在buf指向的字符数组中,每次最多读取bufsize-1个字符(第bufsize个字符赋'\0'),如果文件中的该行,不足bufsize-1个字符,则读完该行就结束。如若该行(包括最后一个换行符)的字符数超过bufsize-1,则fgets只返回一个不完整的行,但是,缓冲区总是以NULL字符结尾,对fgets的下一次调用会继续读该行。

因此实际上echo3只能覆盖malloc的前31B。这表现在如果你输进去32个字母,它只会打印出31个。

echo_2_5

幸好幸好,p64(name_Addr)的最后一个字符以及greetings原来的地址的第一个字节(如下图)原本就都是\00,因此没有什么影响。(注意这是小端模式)

echo_2_6

B. 我们真的理应能getshell吗?

这个问题好像有点奇怪,为什么说我们原本不应该这样就能getshell呢?回想一下我们pwn的流程:泄露main_ebp,计算name (shellcode)的地址,free(o),UAF覆盖greetings的地址,结束

这样怎么就能getshell了呢?!覆盖了greetings的地址,你又没有再一次去执行greetings

虽然事实证明这样是可行的,但从来没有人提出过这个疑问。问题出在哪里呢?

因为最终确实能getshell,说明程序一定再次运行了greetings。为什么在我们没有输入选项的时候,程序就自动进入某一个选项运行呢?

答案就在问题A里面。

我们sendline("A"\*24+p64(name_addr)),这是一个32B+'\n'的字符串,但get_input()只能接收31B,那最后的'\00\n'去哪里了呢?

当然是留在了stdin里。答案于是就很明显了。stdin里面的'\00\n'作为scanf()的输入成为了选项。

那结果会如何呢?我们可以做一个实验,写一个测试的C程序。

看看输入数字2和输入\00 (NULL)时,c的变化以及scanf()返回值的变化。

echo_2_7

可见,scanf("%d", &c)觉得\00不符合%d的要求,因此不能完成匹配,c中的值没有更改,并且函数返回0,表示匹配个数为0

所以整个过程是:在我们pwn的最后,由于UAF过程多输入的'\00\n'作为了scanf()的输入,并且scanf()不匹配'\00',因此v6没有被改变,还是上次我们选择的3。于是程序再一次进入3. UAF echo执行了greetings,这样我们才执行了shellcode。

所以我们上面的exp在情理上不应该getshell。更符合程序流程的exp如下:

你可以通过注释掉最后的p.sendline("3"),看是不是还能getshell。

这样可以说明,最后触发shellcode一定是因为再次选择3之后调用了greetings

写得好累…Bye…

Leave a Comment