我们分析上图程序的汇编代码:
上图中的第一个方框,其实是将 : Let's start the CTF: 这句字符串压进栈;
第二个方框:其实是 system_write()函数, 参数中 fd =1, 说明是标准输出,也就是将 字符串打印输出在屏幕上;
第三个方框: 其实 标准输入函数,允许输入的长度是 3ch 也就是 60个字节; 但是buffer的长度却只有 14h 也就是20, 所以这里存在明显的 栈溢出漏洞,我们可以通过覆盖返回地址劫持程序流程。
接下来我们的思路,其实就是将我们的shellcode输入栈中,更改返回地址为我们的shellcode的首地址,执行我们的shellcode。
我们首先执行程序,输入20个字节,查看栈的变化:
从上图中,我们可以看到我们输入 12345678901234567890 20个字节后,此20个字节的后面的位置就是返回地址的位置,而返回地址下面一个地址是什么呢?我们查看程序 ret 后的指令是 pop esp, 也就是 esp 的地址。
那么我们输入的格式如下: 20*a + shellcode首地址 + 4*a+shellcode
那么接下来,我们的目的是获得shellcode的首地址,我们再看上图中的程序:
此处是,标准输出函数,我们发现输出buffer的首地址是 ecx = esp, 也就是 输出首地址是esp, 如果我们将返回地址更改为此处地址,再次执行标准输出函数,那么我们就能将esp的地址打印出来,然后我们就可以通过计算偏移,获得shellcode的首地址。
exp如下:
此题算是目前为止,我做的非常有难度的题了,谁让我菜呢?看了别人的wp,来加强一下自己的理解。
程序的主函数,十分好理解,我们来看一下这个calc() 函数。
calc函数中主要的四个函数的功能分别如下:
bzero()函数对 s 初始化为0, s 的大小是 0x400h 即1024字节。
get_expr()函数对输入的 表达式进行读取,其中将非法字符进行了过滤,只保留了数字和运算符,内部如何实现我们不关心。
init_pool()函数,也是一个初始化函数,对v1进行初始化,v1的作用我们接下来会讲解。
parse_expr()函数,是本程序的关键,主要是对读入的算式进行计算,我们跟进查看一下。
parse_expr()函数首先是一个大循环对读入的数据以此进行判断处理。
先对输入的每个字符 - 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。
当我们读入当前输入符为运算符时,判断他的下一个是否是运算符,若是则报错。
此处当我们读入了两个运算符就要先将前面的计算一次了。其中 num为存储的 数字, s中存储的是 运算符。 如我们 输入 1+2+3, 读完1+2, 在读到+时, 程序就会先计算1+2, 之后再继续读取后面的符号。
我们跟进eval()中查看一下。
从上图中,我们可以很简单的知道他的运算逻辑,我们唯一需要注意的是,在这里,他将每一次运算的结果都存储在了 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] 的值,是怎么来的。
我们看到这里,当我们输入的第一个操作数是数值时,那么 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
那么我们就成功对任意地址处进行了写入。但是我们这里需要注意,由于我们栈中存储数值的缓冲区和 存储 运算符的缓冲区, 程序没进行一次运算之后,都会进行一次初始化清零, 所以我们如果在这两个缓冲区内写入的化,会被清零,所以我们要寻找各位的缓冲区。
此处从 5A0h 开始为 num的缓冲区 1024大小 到 0ch 处为 运算符的缓冲区大小,同时因为程序开启了 canary,所以我们得绕过canary保护,直接写入返回地址。
(5A0h) / 4 = 360, 所以返回地址在 num[361]处。
然后接下来,由于我们只能写入一些数字,所以我们的方法只有ROP了。
寻找 godget的方法类似如下:
我们通过ROP,来执行execve()函数。需要写入的寄存器如下:
然后,有关于如何计算得到 /bin/sh 在栈上地址的方法,我目前还有一些疑惑,所以就不班门弄斧了。
然后poc,就简单了,不写了。