吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1245|回复: 8
收起左侧

[CTF] 一个CTF题的write up

[复制链接]
FZZZP 发表于 2026-7-31 15:25
本帖最后由 FZZZP 于 2026-7-31 15:28 编辑

下载下来之后他是没有后缀的,用win hex查看,文件前面的标识是elf文件,我在Windows平台没法运行,只能静态分析

elf标识

elf标识



用ida-pro打开,搜索字符串,字符串也不多,可以看到有flag的格式,以及错误和正确的字符串,双击Right!然后交叉引用看一下

字符串

字符串



可以看到Right上面直接跟了一个_memcmp函数,两个值分别是s2跟s,其中一个应该就是跟flag相关的值,再往上看s2的值是两个8位的16进制串,实际上这里是把这两个串在栈里拼起来了
因为在
[Asm] 纯文本查看 复制代码
mov [rbp+s2],rax
mov [rbp+var_98],rdx

这里,这两个在栈上是相连的,而且下面的call指令edx的寄存器压入的值是10h,也就是十进制的16,刚好就是这两个串加起来的大小。
这两个值上面的函数是md5_final,推测应该是真正flag的md5值,跟我们输入的flag的md5值进行对比。接着往上找

交叉引用

交叉引用



还是md5相关的函数,继续往上找

md5

md5



这里有flag的格式了,就是将4个值拼成这个格式,var_70是拼接之后的缓冲区地址,上面的四个地址应该就是我们的输入的地址了

flag格式

flag格式



这块可以看到,我们的输入在var_104这个地方,然后把输入放进checker4这个函数,通过之后就到下面的拼接flag的函数了,所以类似的这个checker函数应该还有1、2、3,继续往上找

checker4

checker4



checker1

checker1

checker1



checker2跟3

checker2、3

checker2、3



现在主函数的脉络就清晰了,分别获取四个输入,然后进入对应的checker函数,如果符合输入就下一步,不符合就直接退出了,接下来就是检查这四个checker,看一下里面是怎么验证的


点进checker1,可以看到逻辑还是比较简单的,输入减去0x5294b771等于0x956438b9,这个逆着计算还是很好计算的

checker1内部

checker1内部



到checker2就稍微复杂一点了,但是分析还是挺好分析,这里就不细写了,感兴趣的可以看图里面的注释

checker2内部

checker2内部



然后就到checker3了,这个逻辑就复杂的多了,我只分析了一部分,后面就没往下看了,checker4更复杂,这样分析的话效率就太慢了

checker3内部-1

checker3内部-1

checker3内部-2

checker3内部-2

checker3内部-3

checker3内部-3



这里是反编译的结果,逻辑就是前面分析的那样,四个输入,检查完之后拼接起来计算MD5,然后用MD5的值进行比较输出,重点是四个checker函数的反编译,checker1跟checker2刚才通过汇编代码已经分析完了,但是checker3跟checker4还是太复杂了,所以看一下反编译之后的,能节省我们分析的时间

反编译1

反编译1

反编译2

反编译2



checker1的反编译:

checker1反编译

checker1反编译



checker2的反编译:

checker2反编译

checker2反编译



checker3的反编译:

checker3反编译

checker3反编译



checker4的反编译:

checker4反编译

checker4反编译



ida-pro已经帮我们把运算的逻辑理清了,我们的目标就是根据这些运算来求解出我们输入的未知数
下面是python代码:
[Python] 纯文本查看 复制代码
from z3 import Solver, BitVec, ULE, sat
def solve_checker1():
    """
    ┌──────────────────────────────────────────────────────────────┐
    │ IDA 反编译:                                                  │
    │   if ( a1 - 1385478001 == -1788593991 ) return 0;            │
    │                                                              │
    │ 纯算术,一步解出。这里用 Z3 仅作演示,实际手动算也行:       │
    │   a1 = 1385478001 + (-1788593991)                            │
    │      = -403115990  →  unsigned: 0xE7F8F02A                   │
    └──────────────────────────────────────────────────────────────┘
    """
    a1 = BitVec('a1', 32)                              # 32 位符号变量

    # C 语言中 int 常量 -1788593991 在与 unsigned 比较时,
    # 会隐式转为无符号:取低 32 位 → & 0xFFFFFFFF
    target = (-1788593991) & 0xFFFFFFFF

    s = Solver()                                       # 新建求解器
    s.add(a1 - 1385478001 == target)                   # 照抄反编译的等式

    if s.check() == sat:                               # 有解
        val = s.model()[a1].as_long()                  # 取出 a1 的值
        print(f"[Checker1] a1 = {val} (0x{val:08x})")
        return val
    else:
        print("[Checker1] UNSAT!")                     # 无解(不应出现)
        return None


def solve_checker2():
    """
    ┌──────────────────────────────────────────────────────────────┐
    │ IDA 反编译:                                                  │
    │   if ( 3 * (a1 & 0x52E3F96B)                                │
    │        - (a1 | 0xAD1C0694)                                   │
    │        - a1                                                  │
    │        == -477662227 ) return 0;                             │
    │                                                              │
    │ 注意: 0xAD1C0694 == ~0x52E3F96B                             │
    │ 编译器把 ~M 计算成了编译时常量,直接写死在指令里。           │
    │                                                              │
    │ 这个 checker 其实也可以逐位分析手动解(见本文档末尾注释),  │
    │ 但 Z3 直接求解更省事。                                       │
    └──────────────────────────────────────────────────────────────┘
    """
    a1    = BitVec('a1', 32)
    M     = 0x52E3F96B                                 # 掩码
    notM  = 0xAD1C0694                                 # = ~M,编译时常量
    target = (-477662227) & 0xFFFFFFFF                  # 有符号 → 无符号

    s = Solver()
    # 直接翻译反编译的表达式,Z3 的位运算符和 C 一模一样
    s.add(3 * (a1 & M) - (a1 | notM) - a1 == target)

    if s.check() == sat:
        val = s.model()[a1].as_long()
        print(f"[Checker2] a1 = {val} (0x{val:08x})")

        # --- 验证:用 Python 原生运算重新计算一遍,确保无误 ---
        # Z3 内部是位精确的,这里用 Python 的 & | 和 32 位掩码验证
        actual = (3 * (val & M) - (val | notM) - val) & 0xFFFFFFFF
        assert actual == target, f"验证失败: 0x{actual:08x} != 0x{target:08x}"
        print(f"          验证通过 ✅")
        return val
    else:
        print("[Checker2] UNSAT!")
        return None


def solve_checker3():
    """
    ┌──────────────────────────────────────────────────────────────┐
    │ IDA 反编译(Hex-Rays 把多条指令合并成了下面这个复杂表达式): │
    │                                                              │
    │   if ( a1 > 0x10000000 ) return -1;                          │
    │   if ( -3 * (~a1 | 0xD68E3FE9)                               │
    │        + 3 * ~(a1 | 0xD68E3FE9)                              │
    │        + 4 * ((~a1 & 0xD68E3FE9) + 2 * ~(~a1 | 0xD68E3FE9)) │
    │        + 10 * (a1 & 0xD68E3FE9)                              │
    │        - (a1 ^ 0xD68E3FE9)                                   │
    │        == 622576829 ) return 0;                              │
    │                                                              │
    │ 反汇编中大约是 50 条指令,编译器把 -3*(...) 展开成           │
    │ shl + sub 的组合。这里直接用 Hex-Rays 合并后的式子即可。    │
    └──────────────────────────────────────────────────────────────┘
    """
    a1 = BitVec('a1', 32)
    M = 0xD68E3FE9                                     # 唯一的掩码
    target = 622576829                                  # 正数,无需转换

    #  表达式 ─── 照抄反编译,括号对齐避免优先级错误 ───
    expr = (-3 * (~a1 | M)
            + 3 * ~(a1 | M)                            # ~(a | M): 对或的结果取反
            + 4 * ((~a1 & M) + 2 * ~(~a1 | M))         # (~a&M): 取反后与
            + 10 * (a1 & M)
            - (a1 ^ M))                                 # ^ 是 XOR

    s = Solver()
    s.add(ULE(a1, 0x10000000))                          # 反汇编 0x12b4: jbe → a1 ≤ 0x10000000
    s.add(expr == target)                               # 反汇编 0x1337: cmp + jnz → 表达式必须等于 target

    if s.check() == sat:
        val = s.model()[a1].as_long()
        print(f"[Checker3] a1 = {val} (0x{val:08x})")
        return val
    else:
        print("[Checker3] UNSAT!")
        return None


def solve_checker4():
    """
    ┌──────────────────────────────────────────────────────────────┐
    │ IDA 反编译 — 最复杂的一个,4 个不同的掩码参与运算:           │
    │                                                              │
    │   if ( a1 > 0x10000000 ) return -1;                          │
    │   if (  11 * (a1 ^ 0xAE7C284F)                               │
    │        + 4 * (a1 & 0xAE7C284F)                               │
    │        - (6 * (a1 & 0x5183D7B0) + 12 * ~(a1 | 0x5183D7B0))  │
    │        - 5 * a1                                              │
    │        - 2 * ~(a1 | 0x4EDA6B7C)                              │
    │        - (a1 | 0xB1259483)                                   │
    │        + 3 * (a1 & 0x4EDA6B7C)                               │
    │        + 4 * (a1 & 0xB1259483)                               │
    │        - (-2 * (a1 | 0xB1259483))                            │
    │        == -639151222 ) return 0;                             │
    │                                                              │
    │ ⚠️ 关键:这个约束在 a1≤0x10000000 范围内有 2 个解!         │
    │ 需要结合 MD5 才能确定哪一个是真正的 flag。                   │
    │                                                              │
    │ 为什么会有多个解?类比哈希碰撞——不同的输入经过复杂运算      │
    │ 后碰巧得到相同的 32 位结果。MD5 在这里充当了"二次验证"。    │
    └──────────────────────────────────────────────────────────────┘
    """
    a1 = BitVec('a1', 32)

    M1 = 0xAE7C284F
    M2 = 0x5183D7B0
    M3 = 0x4EDA6B7C
    M4 = 0xB1259483

    target = (-639151222) & 0xFFFFFFFF                  # 负数 → 无符号 32-bit

    #  表达式 ─── 照着反编译逐项翻译 ───
    expr = (11 * (a1 ^ M1)
            + 4 * (a1 & M1)
            - (6 * (a1 & M2) + 12 * ~(a1 | M2))        # 嵌套括号,注意外层是减法
            + (-5 * a1)                                  # Python 里写 -5 * a1 也行,这里加括号防歧义
            - 2 * ~(a1 | M3)
            - (a1 | M4)
            + 3 * (a1 & M3)
            + 4 * (a1 & M4)
            - (-2 * (a1 | M4)))                          # -(-2 * x) = +2*x,但保持和反编译一致

    s = Solver()
    s.add(ULE(a1, 0x10000000))                           # 范围约束
    s.add(expr == target)                                 # 等式约束

    # ─── 获取所有解 ───
    # .check() 只返回一个解。想要全部解,需要"排除已找到的,再搜"。
    solutions = []
    while s.check() == sat and len(solutions) < 20:      # 20 是安全上限,防止死循环
        val = s.model()[a1].as_long()
        solutions.append(val)
        s.add(a1 != val)                                  # 关键: 把已找到的值从解空间排除

    print(f"[Checker4] {len(solutions)} 个解: {[f'0x{v:08x}' for v in solutions]}")
    return solutions                                      # 返回列表,让 main() 用 MD5 筛


# =============================================================================
# main — 组装 flag + MD5 筛选
# =============================================================================
def main():
    import hashlib

    c1 = solve_checker1()
    c2 = solve_checker2()
    c3 = solve_checker3()
    c4_list = solve_checker4()                            # 可能多个解!

    # 从 IDA 反编译 main() 的 0x15f5 处提取的期望 MD5:
    #   s2[0] = 0xCEC174DE78F9D5F2LL   →  字节序: F2 D5 F9 78 DE 74 C1 CE
    #   s2[1] = 0xD23FCA6134F59F7ALL   →  字节序: 7A 9F F5 34 61 CA 3F D2
    #   合并: F2D5F978DE74C1CE7A9FF53461CA3FD2
    expected_md5 = "f2d5f978de74c1ce7a9ff53461ca3fd2"

    print()
    print("=" * 50)
    for c4 in c4_list:
        flag = f"flag{{{c1:08x}-{c2:08x}-{c3:08x}-{c4:08x}}}"
        actual = hashlib.md5(flag.encode()).hexdigest()
        if actual == expected_md5:
            print(f"&#9989; FLAG: {flag}")
            print(f"   MD5: {actual}  ← 匹配程序内硬编码的期望值")
            return flag
        else:
            print(f"&#10060; 淘汰: {flag}  (MD5 不匹配)")

    print("未找到匹配的 flag!")
    return None


if __name__ == "__main__":
    main()



通过网盘分享的文件:rechall.zip
链接: https://pan.baidu.com/s/1KtcbwB0CSj_d5NayyPMVIg?pwd=vjet 提取码: vjet

免费评分

参与人数 2威望 +1 吾爱币 +21 热心值 +2 收起 理由
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
shiyue2925 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

haitang0513 发表于 2026-8-1 16:26
大佬好有耐心啊,这种题目对我来说就像高考数学最后一题一样直接放了
PTbest 发表于 2026-8-1 20:33
michaelpj 发表于 2026-8-1 23:24
 楼主| FZZZP 发表于 2026-8-2 09:15
michaelpj 发表于 2026-8-1 23:24
这是哪个平台的题目

看雪的reverse
https://kctf.kanxue.com/challenges?category=5
UtsuhoKirisame 发表于 2026-8-2 20:39
已认真学习
SakuraZzzz 发表于 2026-8-3 09:03
re的题已严肃学习
66gh 发表于 2026-8-3 09:34
写的比较详细
iDecode 发表于 2026-8-4 15:31
haohaoxuexi,tiantianxiangshang
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-11-5 10:42

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表