ᕦʕ •ᴥ•ʔᕤ

58anY 解析

本题是 egelloc.nwp 的 reverse 的最后一题,为了避免泄露答案,所有的单词均以倒序拼写。根据前面几关的练习,已知这道题自定义了一个模拟器,拥有自己的指令,寄存器和内存,需要你输入模拟器指令来拿到 flag。但是与前面的题目不同的是,指令和寄存器的标志都是随机的,不是固定的,无法通过静态分析获得,因此需要根据现有已知的条件动态推断出所有的指令和寄存器等。

58anY 的体系结构

内存

内存由一个 byte[1024] 的数组 a 构成。内存的前 768 个 byte 为代码段,保存用户输入的指令。768 后的内存为数据段,数据存储的地址从 0 开始。因为栈寄存器起始为 0,a[768] 往后的一段空间应仅供栈使用,避免出现冲突。

寄存器

寄存器包括4个通用寄存器 a b c d, 栈寄存器 s,指令寄存器i以及标志寄存器 f。寄存器地址紧跟在内存后面,因此在代码中也用 a[1024]\ ~ a[1030] 来访问。在指令中由不同的操作数代表不同寄存器,每个操作数的二进制只有一位为 1,即用 1,2,4,8,16,32,64,128 中的 7 个来代表寄存器。由于模拟器运行时会随机打乱操作数的顺序,无法通过静态分析知道操作数与寄存器的对应关系

指令

每个指令长度为 3 bytes,指令与两个操作数各用一个 byte,但是指令与操作数的位置每道题不一样,仅在最后构造读取flag指令的时候需要注意。每个指令也由 $2^0$ ~ $2^7$ 这 8 个数来表示,对应关系未知。

指令 指令描述
imm op1, op2 将 op2 表示的数存入 op1 表示的寄存器
add op1, op2 将 op2 表示的寄存器的值加到 op1 表示的寄存器
stk op1, op2 栈操作,op2 不为 0 时,将 op2 表示的寄存器中的值入栈,op1 不为 0 时,再将栈顶数出栈于 op1 表示的寄存器
stm op1, op2 以 op1 表示的寄存器中的值作为地址,写入 op2 表示的寄存器中的值
ldm op1, op2 以 op2 表示的寄存器中的值作为地址,读取数据并存入 op1 表示的寄存器
cmp op1, op2 比较 op1 和 op2 表示的寄存器中的值,并根据大小,是否相等设置相关标志位
jmp op1, op2 op1 为 0 时,无条件跳转到 op2 表示的寄存器中的值,op1 不为 0 时,判断 op1 和标志寄存器与的结果,不为 0 则跳转到 op2 表示的寄存器中的值,通过修改指令寄存器的值实现跳转
sys op1, op2 根据 op1 的值进行不同的系统调用,并将结果存入 op2 表示的寄存器(exit 不需要将结果存入寄存器)。系统调用需要的参数通过寄存器 a, b, c 传入

系统调用

该模拟器实现的系统调用有 open, read, write, sleep, exit 等,使用 open, read, write 即可读取 flag 并打印。

已知条件

虽然随机打乱了寄存器、指令、系统调用和标志位的顺序,我们仍可以观察程序运行的状态来推测出一些指令和寄存器的标志。以下是代码流程中一些需要注意的地方:

推断过程

通过构建不同的指令,根据程序执行的状态是退出或者死循环来逐步推测出指令,寄存器,和系统调用的对应关系。

寻找无效寄存器

将指令码设置为 0xff,那么一定会执行 imm,再遍历所有寄存器标志,如果程序退出并输出 "unknown register",那么该标志为 register_invalid

for register in set([0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]):
    0xff register, register

寻找 sys 指令和 exit 系统调用

遍历指令码和系统调用,并将 op2 设置为 register_invalid,那么当程序退出且没有输出 "unknown register" 的时候,即为通过 exit 退出。

# try to construct:
# [sys] [exit], register_invalid
possible_values = set([0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80])
for instruction in possible_values:
    for syscall in possible_values:
        instruction syscall, register_invalid

寻找 imm 指令和指令寄存器 i

现在已知 sys 指令,从剩余指令和所有寄存器中遍历,并将 op2 设置为 register_invalid,然后添加一条无效寄存器的指令来终止程序。在剩下的指令中 add,stk,stm,ldm,cmp,jmp 都会因为 op2 为无效寄存器而退出,只有 imm 改变指令寄存器i设置的情况会使程序进入死循环。另外要注意特殊情况,当 register_invalid == 1 的时候,指令寄存器 i 被置为 1,执行第二条无效语句也会退出,因此多加一条 imm 语句使终止程序的语句后移。

# try to construct:
# [imm] [i], register_invalid
# 0xff register_invalid, register_invalid
for inst in get_possible_instructions():
    for op1 in get_possible_registers():
        inst op1, register_invalid
        if register_invalid == 1:
            inst op1, register_invalid
        0xff register_invalid, register_invalid # 用来终止程序

寻找 stk 指令

使用 stk 指令将寄存器 i 置为 0 以达到死循环的目的,若指令不为 stk,则会导致程序退出。

# try to construct:
# [stk] i, 0
# imm register_invalid, 0
for inst in get_possible_instructions():
    inst i, 0
    imm register_invalid, 0

寻找栈寄存器 s

使用 stk 指令操作栈寄存器 s,注意 stk 指令在两个操作数均不为 0 时会依次进行入栈和出栈操作,入栈时会先将栈寄存器加一,再存入数据。因此使用指令 stk i, s 会使指令寄存器变为 1 进而执行无效寄存器指令退出。其它寄存器会使指令寄存器始终为 0 进入死循环。

# try to construct:
# stk i, [s]
# imm register_invalid, 0
for register_s in get_possible_registers():
    stk i, register_s
    imm register_invalid, 0

寻找 ldm 指令

目前未知的指令还有 add,stm,ldm,cmp,jmp,使用指令 ldm i, s 会使指令寄存器始终为 0 进入死循环,而其它指令会继续执行遇到无效寄存器指令退出。

# try to construct:
# [ldm] i, s
# imm register_invalid, 0
for inst in get_possible_instructions():
    inst i, s
    imm register_invalid, 0

寻找 stm 指令

目前未知的指令还有 add,stm,cmp,jmp,已知寄存器只有 s 和 i。寻找 stm 的思路就是随机选择另一个寄存器 reg (通用寄存器或标志寄存器),配合 stm 与 ldm 来对指令寄存器赋值,跳转到第 4 条无效寄存器指令退出。如果是 add,cmp 指令,内存中不会写入 4,赋给指令寄存器的值为 0,进入死循环。如果是 jmp 指令 jmp reg, s ,需要判断 reg 是否为标志寄存器,若不为标志寄存器,则不会发生跳转,若刚好随机选择了标志寄存器,为了防止跳转到第 4 条指令,需要使跳转条件不成立,因此将 reg 的值设为 0xff-reg ,两个数的与为 0,不会跳转。

# try to construct:
# imm reg, 0xff-reg
# imm s, 4
# [stm] reg, s
# ldm i, reg
# imm register_invalid, 0
reg = get_possible_registers().pop()
for inst in get_possible_instructions():
    imm reg, 0xff-reg
    imm s, 4
    inst reg, s
    ldm i, reg
    imm register_invalid, 0

寻找 write 系统调用

虽然仍然不能区分剩下的通用寄存器和标志寄存器,但是我们知道标准输出的描述符是 1,因此可以将这五个寄存器的的值都设为 1,在内存地址1的地方写入一个特殊字符,遍历系统调用检查是否打印了特殊字符。

# try to construct:
# imm abcdf, 1
# imm s, 0x99
# stm a, s
# sys [write], register_invalid
for syscall in get_possible_syscalls():
    for r in get_possible_registers():
        imm r, 1
    imm s, 0x99
    stm a, s
    sys syscall, register_invalid

寻找通用寄存器 abc

知道 write 系统调用后,可以尝试构造一个特殊字符串,遍历寄存器 abc 并调用 write 检查输出来找到这三个寄存器。

# try to construct:
# imm [a], 1
# imm [b], 0x92
# stm [b], [b]
# imm [b], 0x91
# stm [b], [b]
# imm b, 0x90
# stm [b], [b]
# imm [c], 3
# sys write, register_invalid

寻找 sleep 系统调用

将寄存器 a 设为一个较大的值,遍历剩余系统调用,过了很久还没退出的则是 sleep。

# try to construct:
# imm a, 80
# imm b, 0
# imm c, 0
# sys [sleep], register_invalid

寻找 open read 系统调用

在本机新建一个临时文本文件,遍历剩余系统调用尝试打开文件并读取文件内容,然后使用 write 输出,若能成功输出内容则找到了 open 和 read。代码略。

总结

在推测出 open,read 后,就可以读取 flag 并用 write 输出的控制台了。

参考

#Reverse