跳转至

CPCTF - omikuji Writeup

题目信息

  • 比赛: CPCTF
  • 题目: omikuji
  • 类别: Reverse
  • 难度: 待补
  • 附件/URL: omikuji
  • 附件链接: 下载附件 · 仓库位置
  • Flag格式: CPCTF{...}
  • 状态: 已解

解题过程

先查看文件基础信息:

file CTF_Writeups/files/omikuji/omikuji
strings -a CTF_Writeups/files/omikuji/omikuji | grep -E "Flag|omikuji"
readelf -sW CTF_Writeups/files/omikuji/omikuji | grep -E "main|omikuji"

可以看到二进制中存在 Flag: %s,同时符号表没有被完全 strip,保留了 mainomikuji 这两个关键函数:

main
_Z7omikujiNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

继续反汇编 main,可以发现程序没有从外部读取输入,而是在 main 中构造了一个 std::string,随后将该字符串传给 omikuji

lea     rax, [rbp - 0x60]
mov     esi, 0x43
mov     rdi, rax
call    std::string::operator+=(char)

lea     rax, [rbp - 0x60]
mov     esi, 0x50
mov     rdi, rax
call    std::string::operator+=(char)

...

lea     rdx, [rbp - 0x60]
lea     rax, [rbp - 0x40]
mov     rsi, rdx
mov     rdi, rax
call    std::string::string(std::string const&)

lea     rax, [rbp - 0x40]
mov     rdi, rax
call    omikuji(std::string)

其中每次 mov esi, imm 都是在向字符串追加一个字符。把这些立即数按 ASCII 转换即可得到完整 flag。

提取到的字符序列如下:

0x43 0x50 0x43 0x54 0x46 0x7b 0x44 0x33
0x72 0x5f 0x34 0x31 0x37 0x33 0x5f 0x77
0x75 0x72 0x66 0x33 0x31 0x37 0x5f 0x6e
0x31 0x63 0x68 0x37 0x7d

转换后为:

CPCTF{D3r_4173_wurf317_n1ch7}

omikuji 函数逻辑

omikuji 会使用 std::random_device 初始化 std::mt19937,然后生成一个 [0, 0x7fffffff] 范围内的随机数。

关键逻辑如下:

r = random();

if (r == 0x7ea) {
    printf("Flag: %s", flag);
} else {
    v = r % 100;

    if (v <= 9) {
        printf("...: %s", flag.substr(0, 10).c_str());
    } else if (v <= 24) {
        printf("...\nFlag: %s", flag.substr(0, 8).c_str());
    } else if (v <= 49) {
        printf("...\nFlag: %s", flag.substr(0, 6).c_str());
    } else if (v <= 84) {
        printf("...\nFlag: %s", flag.substr(0, 4).c_str());
    } else if (v <= 94) {
        printf("...\nFlag: %s", flag.substr(0, 2).c_str());
    } else {
        puts("...\nFlag:");
    }
}

也就是说,运行程序时大多数情况下只能看到 flag 的前缀;只有随机数正好等于 0x7ea,也就是十进制 2026 时,才会直接输出完整 flag。这个概率很低,因此不需要依赖爆破运行,直接静态分析 main 中构造的字符串即可。

自动提取脚本

也可以用脚本从 main 的反汇编中自动提取传给 std::string::operator+=(char) 的立即数:

from elftools.elf.elffile import ELFFile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from capstone.x86 import X86_REG_ESI

with open("CTF_Writeups/files/omikuji/omikuji", "rb") as f:
    elf = ELFFile(f)
    text = elf.get_section_by_name(".text")
    symtab = elf.get_section_by_name(".symtab")
    main = next(s for s in symtab.iter_symbols() if s.name == "main")

    start = main["st_value"]
    size = main["st_size"]
    off = start - text["sh_addr"]
    code = text.data()[off:off + size]

md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True

chars = []
last = None

for ins in md.disasm(code, start):
    if (
        ins.mnemonic == "mov"
        and len(ins.operands) == 2
        and ins.operands[0].reg == X86_REG_ESI
        and ins.operands[1].type == 2
    ):
        last = ins.operands[1].imm
    elif ins.mnemonic == "call" and ins.op_str == "0x2210" and last is not None:
        chars.append(last)
        last = None

print("".join(chr(c) for c in chars))

输出:

CPCTF{D3r_4173_wurf317_n1ch7}

Flag

CPCTF{D3r_4173_wurf317_n1ch7}

评论