pwn32

FORTIFY_SOURCE 是 GCC 和 Clang 等编译器提供的一项重要安全特性,用于增强程序对内存操作漏洞的防护

等级 说明 增强的函数列表
0 完全禁用保护 (不进行任何检查)
1 基础保护 (对高危函数插入长度检查) memcpy, memmove, memset,strcpy,
strncpy, strcat, strncat,sprintf,
vsprintf, snprintf, vsnprintf,gets
2 严格保护 (增加格式化字符串和文件操作检查) Level 1 的所有函数 +fdopen, fread,
fwrite, read,recv, send, printf,
fprintf, vprintf(限制 %n等危险格式化字符)

checkse

64 位小端序,这里没有显示 FORTIFY,说明没有打开这项保护,题目上也说防御等级是 0

用 ida 打开,反编译

发现这里都是各种没有被增强的漏洞函数,我们先来找 flag。shift+F12打开 string 表

点击跳转,Ctrl+X,点 ok,跳转到 flag 在代码的位置

再按Tab切换到伪 C 代码识图

在这个Undefined()函数里,直接读取打印了 flag,也就是说,只要调用这个函数即可

回到 main 函数

这里最后调用Undefined()函数的要求就是argc > 4

argc在 pwn23 中介绍过,其实就是 main 函数的参数个数,而这里 argc 自己本身就是一个参数

所以只需要再附上 4 个参数,加起来一共 5 个参数,argc = 5 > 4,满足条件

开启远程环境,ssh 连接

给了我们 shell,但是没有权限直接读取 flag 文件,需要用刚刚我们分析的程序来获取,这里的 pwnme 就是

运行 pwnme 并跟上四个参数./pwnme 1 1 1 1,回车运行没有反应

这是因为程序里有一步输入,我们随便输入再回车

拿到了 flag

同时程序也提醒我们不要只是得到 flag,要理解程序原理

这里我们来逐行分以下 main 函数,也方便后两道题讲解

1. 权限设置

v3 = getegid();             // 获取当前进程的有效组ID
setresgid(v3, v3, v3);      // 设置真实、有效、保存的组ID为 v3

用户的权限和程序的权限是不一样的,我们虽然获得了 shell,但是却不能访问 flag 文件

ls -l ./pwnme查看程序的权限,发现程序是 root

- r w s r - s r - x
│ │ │ │ │   │ │   └─ 其他用户:执行权限(x)
│ │ │ │ │   │ └───── 其他用户:读权限(r)
│ │ │ │ │   └─────── 组用户:setgid 权限(s)
│ │ │ │ └─────────── 组用户:读权限(r)
│ │ │ └───────────── 所有者:setuid 权限(s)
│ │ └─────────────── 所有者:写权限(w)
│ └───────────────── 所有者:读权限(r)
└─────────────────── 文件类型(`-` 表示普通文件)

第一个 s(所有者权限位)​​:setuid 程序运行时,进程的有效用户 ID 变为文件所有者(对应第一个 root)
​​第二个 s(组权限位)​​:setgid 程序运行时,进程的有效组 ID 变为文件所属组(对应第二个 root)

这两步指令就是把程序在给自己提权,我们就能通过程序访问到 flag

2. 复制 argv[1]到 buf1(11字节缓冲区)

v4 = argv[1];  // 获取第一个命令行参数
*(_QWORD *)buf1 = *(_QWORD *)v4;         // 复制前8字节(64位)
*(_WORD *)&buf1[8] = *((_WORD *)v4 + 4); // 复制接下来的2字节(16位)
buf1[10] = v4[10];                       // 复制第11字节

这里就有一个风险,buf1 只有 11 字节,若 argv[1]长度 <11,会读取越界(未定义行为)

fortify 保护也并不会保护此类操作

3. 硬编码字符串拷贝到 buf2

strcpy(buf2, "CTFshowPWN");  // 将固定字符串复制到 buf2

“CTFshowPWN”(10字节) + 终止符 \0= 11字节,刚好填满 buf2

strcpy 是危险函数但是这里没什么风险

4.打印 buf1和 buf2

printf("%s %s\n", buf1, buf2); 	 // 输出两个缓冲区内容

若 buf1未正确终止(如无 \0),可能泄露相邻内存

**5. 解析 ****argv[3]****为整数 **v5**,从 ****argv[2]****拷贝 ****v5****字节到 ****buf1**

v5 = strtol(argv[3], 0LL, 10);  	// 将第三个参数转为十进制整数
memcpy(buf1, argv[2], v5);  			// 拷贝数据,拷贝长度是 v5

v5完全可控,未检查 v5是否为负数或超过 buf1大小,可指定任意长度,导致 buf1溢出

6.再次拷贝 argv[1]到 buf2,打印 buf1和 buf2

strcpy(buf2, argv[1]);  					// 直接拷贝,无长度限制:
printf("%s %s\n", buf1, buf2);  	// 可能泄露内存或触发崩溃

若 argv[1]长度 >11,覆盖栈数据(如返回地址)

若 buf1、buf2 未正确终止(如无 \0),可能泄露相邻内存

7. 从标准输入读取到 buf1 再输出

fgets(buf1, 11, _bss_start);  // 从_bss_start(应为stdin)读取最多10字节+NULL
printf(buf1, &num);  // 直接使用用户输入作为格式化字符串

_bss_start可能是反编译符号错误,实际应为 stdin

buf1 可控,存折格式化字符串漏洞,可输入 %p泄露内存,或 %n修改 num的值(任意地址写入)

总而言之

没有开启 Fortify 保护,这些危险函数都有可利用的空间

pwn33

checkse

发现了 fortify 保护,这里是看不出来保护等级的,题目上说明包护等级为 1,也可以观察函数变化

ida 反编译

可以发现已经有危险函数被替换

__memcpy_chk(buf1, argv[2], v5, 11LL);

这里限制了 v5 的大小,如果 v5 大于 11 就会引发报错

__strcpy_chk(buf2, argv[1], 11LL);

这里限制了传入的第一个参数,参数长于 10 位就会引发报错,因为需要额外 1 字节存储\0

除这两处之外,其他漏洞仍然存在,这里我们试一下格式化字符串

%n%N$ 都能被利用

获取 flag 的方式和上一题一样,加上了刚刚提到的:

  • 参数个数大于五个
  • 第一个参数不能长于 10 位
  • 第三个参数不能大于 11

拿到 flag

pwn34

checkse

发现了 fortify 保护,题目上说明包护等级为 2

ida 反编译

很明显,这次保护又提升了,加强了 printf 函数

连远程试一下

%n任意地址写入不能再被利用了

%N$格式化字符串也必须从%1$开始连续才有效

但是 %n 被禁用了,就很难再利用了

这题拿 flag 的条件还是和上题一样,这里官方的附件好像是给错了,附件里没有参数大于 4 这个条件

但是连上远程还是需要又四个参数

拿到 flag

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐