从CTFshow-PWN入门-pwn32~34理解FORTIFY保护
FORTIFY_SOURCE 是 GCC 和 Clang 等编译器提供的一项重要安全特性,用于增强程序对内存操作漏洞的防护memcpymemmovememsetstrcpystrncpystrcatstrncatsprintfvsprintfsnprintfvsnprintfgetsfdopenfreadfwritereadrecvsendprintffprintfvprintf%ncheckse
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
更多推荐
所有评论(0)