Pwnable.tw WP

Pwnable.tw start

Pwnable.tw WP_第1张图片

我们分析上图程序的汇编代码:

上图中的第一个方框,其实是将 : Let's start the CTF:  这句字符串压进栈;

第二个方框:其实是 system_write()函数, 参数中 fd =1, 说明是标准输出,也就是将 字符串打印输出在屏幕上;

第三个方框: 其实 标准输入函数,允许输入的长度是 3ch 也就是 60个字节; 但是buffer的长度却只有 14h 也就是20, 所以这里存在明显的 栈溢出漏洞,我们可以通过覆盖返回地址劫持程序流程。

接下来我们的思路,其实就是将我们的shellcode输入栈中,更改返回地址为我们的shellcode的首地址,执行我们的shellcode。

我们首先执行程序,输入20个字节,查看栈的变化:

Pwnable.tw WP_第2张图片

从上图中,我们可以看到我们输入 12345678901234567890   20个字节后,此20个字节的后面的位置就是返回地址的位置,而返回地址下面一个地址是什么呢?我们查看程序 ret 后的指令是 pop esp, 也就是 esp 的地址。

那么我们输入的格式如下: 20*a + shellcode首地址 + 4*a+shellcode

那么接下来,我们的目的是获得shellcode的首地址,我们再看上图中的程序:

Pwnable.tw WP_第3张图片

此处是,标准输出函数,我们发现输出buffer的首地址是 ecx = esp, 也就是 输出首地址是esp, 如果我们将返回地址更改为此处地址,再次执行标准输出函数,那么我们就能将esp的地址打印出来,然后我们就可以通过计算偏移,获得shellcode的首地址。

exp如下:

Pwnable.tw WP_第4张图片


calc

此题算是目前为止,我做的非常有难度的题了,谁让我菜呢?看了别人的wp,来加强一下自己的理解。

Pwnable.tw WP_第5张图片

程序的主函数,十分好理解,我们来看一下这个calc() 函数。

Pwnable.tw WP_第6张图片

calc函数中主要的四个函数的功能分别如下:

bzero()函数对 s 初始化为0, s 的大小是 0x400h 即1024字节。

get_expr()函数对输入的 表达式进行读取,其中将非法字符进行了过滤,只保留了数字和运算符,内部如何实现我们不关心。

init_pool()函数,也是一个初始化函数,对v1进行初始化,v1的作用我们接下来会讲解。

parse_expr()函数,是本程序的关键,主要是对读入的算式进行计算,我们跟进查看一下。

parse_expr()函数首先是一个大循环对读入的数据以此进行判断处理。

Pwnable.tw WP_第7张图片

先对输入的每个字符 - 48与9比较,主要是为了判断是否是 运算符,若不是进入if。 然后判断输入的是否为0,若是,程序退出。

若不是,

就将当前输入存储在另一个buffer中,此处我将名字更改为num。记住这个num 将会存储每次输入的数字,并且由count计存储了多少个。存储的规则如下:  num[0] = count,  每多读入一个数值,那么 就根据 count ++ , num[count] = 数值。     如我们输入  3+2, 则num 中 num[0]就会存储count的值为2, num[1] 存储3, num[2]存储2。  若我们输入  1+2+3, 则 num[0] 存储count的值为3,num[1] 为1, num[2] 为2,num[3] 为3。

Pwnable.tw WP_第8张图片

当我们读入当前输入符为运算符时,判断他的下一个是否是运算符,若是则报错。

Pwnable.tw WP_第9张图片

此处当我们读入了两个运算符就要先将前面的计算一次了。其中 num为存储的 数字,  s中存储的是 运算符。  如我们 输入 1+2+3, 读完1+2, 在读到+时, 程序就会先计算1+2, 之后再继续读取后面的符号。

我们跟进eval()中查看一下。

Pwnable.tw WP_第10张图片

从上图中,我们可以很简单的知道他的运算逻辑,我们唯一需要注意的是,在这里,他将每一次运算的结果都存储在了 num[1]的位置。 我们知道这几个运算符都是二目运算符,也就是 num[0] 中count的值将会永远为2, 那么 num[num[0] - 1] = num [2 - 1 ]= num[num[0] - 1] + num[ num[0] ] = num[2-1] + num[2]

这里就存在一个程序漏洞。 

试想,如果我们可以将num[0] 的值更改, 那么 我们是不是就可以 将 运算得到的值 存储在 栈中 任意的位置上。

那么我们接下来,来看一下 num[0] 的值,是怎么来的。

Pwnable.tw WP_第11张图片

我们看到这里,当我们输入的第一个操作数是数值时,那么 count 也就是 num[0] 的值就会开始 +1 。 那么如果我们从刚开始输入的第一个操作数不是数值,那么  count的 值就仍然为0。 

如果我们输入+350,那么程序读入之后 num中存储的值分别是:  num[0] = 1,  num[1] =350

那么: num[ num[0] - 1] = num[0] = num[ num[0] -1] + num[ num[0]] = num[0]+ num[1] = 1 + 350 = 351

由于程序最终输出 是 根据:  num[ num[0] - 1]来输出的, 也就是程序最终会输出 num[ 351 -1] = num[350]的值。

这样我们是不是 就实现了,查看栈 的内容。 那么接下来是进行写:

如果我们输入:  + 350 +20 那么进行第二次运算 +20  时:

num[ num[0] - 1] = num [ 350] =   num[ num[0] -1] + num[ num[0] ] = num[ 350] + num [ 351] = num[350] +20 

那么我们就成功对任意地址处进行了写入。但是我们这里需要注意,由于我们栈中存储数值的缓冲区和 存储 运算符的缓冲区, 程序没进行一次运算之后,都会进行一次初始化清零, 所以我们如果在这两个缓冲区内写入的化,会被清零,所以我们要寻找各位的缓冲区。

Pwnable.tw WP_第12张图片

此处从 5A0h 开始为 num的缓冲区 1024大小 到  0ch 处为 运算符的缓冲区大小,同时因为程序开启了 canary,所以我们得绕过canary保护,直接写入返回地址。

(5A0h) / 4 =  360, 所以返回地址在 num[361]处。

Pwnable.tw WP_第13张图片

然后接下来,由于我们只能写入一些数字,所以我们的方法只有ROP了。

寻找 godget的方法类似如下:

Pwnable.tw WP_第14张图片

我们通过ROP,来执行execve()函数。需要写入的寄存器如下:

Pwnable.tw WP_第15张图片

 

 

 

 

然后,有关于如何计算得到 /bin/sh 在栈上地址的方法,我目前还有一些疑惑,所以就不班门弄斧了。

然后poc,就简单了,不写了。

你可能感兴趣的:(CTF)