SigMachine CTF逆向题深度解析:从零到一的完整解题之旅
写在前面本文将带你深入分析一道综合性的CTF逆向题目SigMachine。这道题融合了ELF文件格式、Linux信号处理、自定义加密算法、OLLVM混淆等多种技术,是学习二进制安全和逆向工程的绝佳案例 2025-11-24 00:25:23 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

写在前面

本文将带你深入分析一道综合性的CTF逆向题目SigMachine。这道题融合了ELF文件格式、Linux信号处理、自定义加密算法、OLLVM混淆等多种技术,是学习二进制安全和逆向工程的绝佳案例。

我们将从最基础的文件分析开始,一步步深入,揭开这道题目背后的技术原理。即使你是逆向新手,也能跟随本文的节奏,理解每一个技术细节。


第一章:初次接触 - 文件信息收集

1.1 文件类型识别

拿到任何二进制文件,第一步永远是了解它的基本信息。我们使用Linux系统自带的file命令:

$ file sigmachine
sigmachine: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, stripped

让我们逐个解读这些信息:

1. ELF 64-bit LSB executable

  • ELF: Executable and Linkable Format,Linux系统的可执行文件格式

  • 64-bit: 64位程序,需要64位处理器和操作系统

  • LSB: Least Significant Byte,小端序存储(低位字节在前)

  • executable: 可执行文件

2. x86-64

  • 目标架构是x86-64(AMD64),即常见的PC处理器架构

3. statically linked (静态链接)

  • 所有需要的库函数都被编译进了这个文件

  • 文件体积会很大(本题678KB)

  • 好处:不依赖系统库,可以独立运行

  • 坏处:给逆向分析增加了难度,因为代码量巨大

4. stripped (已剥离符号)

  • 所有调试符号(函数名、变量名)都被移除

  • 逆向时看到的都是地址,如sub_401990而非有意义的函数名

  • 这是对逆向工程的第一道防线

1.2 运行程序观察行为

理论分析之后,我们实际运行看看:

$ chmod +x sigmachine
$ ./sigmachine
password: hello
wrong!

程序提示输入密码,输入错误会显示"wrong!"。这是典型的密码验证程序,我们的目标就是找到正确的密码(flag)。

1.3 提取字符串线索

字符串往往包含重要信息。我们使用strings命令提取可打印字符,并用-tx参数显示地址:

$ strings -tx sigmachine | grep -E "password|wrong|right"
  7c093 wrong!
  7c09a right!
  7c0a1 password:

发现:

  • 地址0x47c093: "wrong!"

  • 地址0x47c09a: "right!"

  • 地址0x47c0a1: "password: "

这三个字符串会在程序的验证逻辑中使用,后续分析时这些地址会是重要线索。


第二章:深入内部 - ELF结构与Constructor函数

2.1 什么是Constructor函数?

在正式分析前,我们需要理解一个关键概念:Constructor函数(构造函数)。

在C/C++程序中,有一种特殊机制允许某些函数在main()函数执行之前运行。这常用于:

  • 初始化全局变量

  • 注册信号处理器

  • 设置程序运行环境

技术原理:

ELF文件有一个特殊的段叫.init_array,它存储了一组函数指针。程序加载器会在调用main()之前,依次执行这些函数。

2.2 查看.init_array段

我们使用readelf工具查看这个段:

$ readelf -x .init_array sigmachine

".init_array"节的十六进制输出:
  0x006a0138 700a4000 00000000 50194000 00000000 [email protected].@.....
  0x006a0148 90194000 00000000 f0194000 00000000 ..@.......@.....
  0x006a0158 501a4000 00000000 b01a4000 00000000 P.@.......@.....
  0x006a0168 f0044000 00000000                   ..@.....

如何解读十六进制数据?

  1. 每行16字节,每个地址占8字节(64位)

  2. ELF使用小端序,需要字节反转

让我们手动解析第一个地址:

原始: 700a4000 00000000
反转: 00000000 00400a70
结果: 0x400a70

完整提取所有7个函数地址:

原始数据反转后函数地址
700a4000 000000000x00000000 00400a700x400a70
50194000 000000000x00000000 004019500x401950
90194000 000000000x00000000 004019900x401990
f0194000 000000000x00000000 004019f00x4019f0
501a4000 000000000x00000000 00401a500x401a50
b01a4000 000000000x00000000 00401ab00x401ab0
f0044000 000000000x00000000 004004f00x4004f0

关键发现:程序在main()之前会执行7个初始化函数!

2.3 为什么要关注Constructor?

因为恶意软件和混淆程序经常在这里"做手脚":

  • 注册反调试机制

  • 修改关键数据结构

  • 设置信号处理陷阱

本题正是利用Constructor注册了信号处理器,我们接下来深入分析。


第三章:信号处理的艺术 - 将运算隐藏在信号中

3.1 发现信号注册

我们使用objdump反汇编其中一个constructor函数(0x401990):

$ objdump -d sigmachine -M intel | grep -A 10 "^0000000000401990"

输出中发现关键模式:

401990:	push   rbp
401991:	mov    rbp,rsp
401994:	sub    rsp,0x20
401998:	mov    edi,0x2              # 第一个参数: 信号编号2
40199d:	movabs rsi,0x400aa0         # 第二个参数: 处理函数地址
4019a7:	call   0x407360             # 调用signal()函数

4019ac:	mov    edi,0x3              # 信号编号3
4019b1:	movabs rsi,0x400b60         # 处理函数地址
4019bf:	call   0x407360             # 调用signal()

这段汇编做了什么?

这是在调用signal(int signum, void (*handler)(int))函数:

  • 第一个参数(edi寄存器): 信号编号

  • 第二个参数(rsi寄存器): 信号处理函数的地址

  • 调用地址0x407360: 这是signal()函数的地址

3.2 提取完整的信号映射表

我们系统性地提取所有信号注册:

$ objdump -d sigmachine | grep -B 3 "call.*407360" | grep "mov.*\$0x"
401998:	bf 02 00 00 00       	mov    $0x2,%edi
4019ac:	bf 03 00 00 00       	mov    $0x3,%edi
4019c4:	bf 04 00 00 00       	mov    $0x4,%edi
4019f8:	bf 05 00 00 00       	mov    $0x5,%edi
401a0c:	bf 06 00 00 00       	mov    $0x6,%edi
401a24:	bf 07 00 00 00       	mov    $0x7,%edi
401a58:	bf 08 00 00 00       	mov    $0x8,%edi
401a6c:	bf 0a 00 00 00       	mov    $0xa,%edi
401a84:	bf 0b 00 00 00       	mov    $0xb,%edi
401ab8:	bf 0c 00 00 00       	mov    $0xc,%edi
401acc:	bf 0d 00 00 00       	mov    $0xd,%edi

整理成完整的映射表:

信号编号十六进制Linux标准信号名正常用途本题重定向为
20x2SIGINT中断信号(Ctrl+C)加法运算(ADD)
30x3SIGQUIT退出信号减法运算(SUB)
40x4SIGILL非法指令乘法运算(MUL)
50x5SIGTRAP调试断点除法运算(DIV)
60x6SIGABRT程序终止取模运算(MOD)
70x7SIGBUS总线错误按位取反(NOT)
80x8SIGFPE浮点异常异或运算(XOR)
100xaSIGUSR1用户信号1或运算(OR)
110xbSIGSEGV段错误与运算(AND)
120xcSIGUSR2用户信号2左移运算(SHL)
130xdSIGPIPE管道破裂右移运算(SHR)

3.3 信号处理的创新之处

传统的加法运算:

result = a + b;

本题的加法运算:

// 步骤1: 将操作数存入全局数组
OPERAND[0] = a;
OPERAND[1] = b;

// 步骤2: 触发信号2(ADD信号)
raise(2);

// 步骤3: 信号处理函数自动被调用
// (在信号处理函数内部: TMP[0] = OPERAND[0] + OPERAND[1])

// 步骤4: 从全局数组读取结果
result = TMP[0];

为什么要这样做?

  1. 混淆执行流:调试器很难跟踪信号的跳转

  2. 隐藏运算逻辑:代码中看不到+-*等明显的运算符

  3. 增加分析难度:需要理解Linux信号机制才能分析

这是一种高级的代码混淆技术,将简单的运算变成了复杂的系统调用。


第四章:数据考古 - 提取加密算法的关键信息

4.1 查找只读数据段

加密算法通常需要一些常量,如替换表、密钥等。这些数据存储在.rodata(Read-Only Data)段中。

我们使用readelf查看这个段:

$ readelf -x .rodata sigmachine | grep -A 10 "0x0047c010"

输出:

0x0047c010 58564544 554c4a54 47574d51 00000000 XVEDULJTGWMQ....
0x0047c020 44474c4a 57455657 58445755 544d554a DGLJWEVWXDWUTMUJ
0x0047c030 474c4a57 54555744 45574558 474c4a47 GLJWTUWDEWEXGLJG
0x0047c040 54554754 55575554 474a4c57 4458574c TUGTUWUTGJLWDXWL
0x0047c050 5557554a 514c5557 554c574c 54574544 UWUJQLUWULWLTWED
0x0047c060 51545557 54554d4c 554d5554 4d564551 QTUWTUMLUMUTMVEQ
0x0047c070 4c4a5755 4c574c54 4d4a5447 544c4d58 LJWULWLTMJTGTLMX
0x0047c080 564d5658 4745444d 58564745 56515555 VMVXGEDMXVGEVQUU
0x0047c090 4d4a0077 726f6e67 21007269 67687421 MJ.wrong!.right!

4.2 解析关键数据

1. TABLE3 (地址0x47c010, 长度8字节)

58 56 45 44 55 4c 4a 54
X  V  E  D  U  L  J  T

字符串: XVEDULJT

2. TABLE2 (地址0x47c018, 长度4字节)

47 57 4d 51
G  W  M  Q

字符串: GWMQ

3. 目标密文 (地址0x47c020, 长度114字节)

我们用Python提取完整密文:

data = bytes.fromhex('''
44474c4a 57455657 58445755 544d554a
474c4a57 54555744 45574558 474c4a47
54554754 55575554 474a4c57 4458574c
5557554a 514c5557 554c574c 54574544
51545557 54554d4c 554d5554 4d564551
4c4a5755 4c574c54 4d4a5447 544c4d58
564d5658 4745444d 58564745 56515555
4d4a
'''.replace(' ', '').replace('\n', ''))

print(data.decode('ascii'))

输出:

DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ

4.3 观察密文特征

  • 密文长度:114字节

  • 字符集:仅包含 D, E, G, J, L, M, Q, T, U, V, W, X 这12个字母

  • 长度关系:114 = 38 × 3

推测:每个明文字节被编码成3个密文字符!

我们注意到:

  • TABLE3有8个字符 → 可以表示3位二进制(2³=8)

  • TABLE2有4个字符 → 可以表示2位二进制(2²=4)

  • 3位 + 2位 + 3位 = 8位 = 1字节

这暗示了一种位拆分编码方案


第五章:算法逆向 - 揭秘3-2-3位拆分加密

5.1 理解位拆分原理

一个字节有8位,本题将其拆分为三部分:

字节: 0b 01100110 (字符'f' = 0x66)
      ↓
拆分: 011 | 00 | 110
      ↓    ↓    ↓
     高3位 中2位 低3位

为什么是3-2-3?

位数取值范围对应表表长度
3位0-7TABLE38字符
2位0-3TABLE24字符
3位0-7TABLE38字符

完美匹配!每个部分的取值范围正好对应一个查找表。

5.2 手工演示加密过程

让我们加密第一个字符'f'(ASCII码0x66):

步骤1: 位拆分

byte_val = 0x66  # 'f'的ASCII码
# 二进制: 0b01100110

# 提取高3位 (bit 5-7)
high_3 = (byte_val >> 5) & 0b111
# 0b01100110 >> 5 = 0b00000011 = 3

# 提取中2位 (bit 3-4)
mid_2 = (byte_val >> 3) & 0b11
# 0b01100110 >> 3 = 0b00001100
# 0b00001100 & 0b11 = 0b00 = 0

# 提取低3位 (bit 0-2)
low_3 = byte_val & 0b111
# 0b01100110 & 0b111 = 0b110 = 6

结果: high_3=3, mid_2=0, low_3=6

步骤2: 异或编码

这里引入了异或链机制(初始值都为0):

xor_3 = 0  # 3位数据的异或累积值
xor_2 = 0  # 2位数据的异或累积值

# 对高3位编码
enc_high = high_3 ^ xor_3  # 3 ^ 0 = 3

# 对中2位编码
enc_mid = mid_2 ^ xor_2    # 0 ^ 0 = 0

# 对低3位编码(注意:这里用enc_high而非xor_3!)
enc_low = low_3 ^ enc_high # 6 ^ 3 = 5

为什么低3位用enc_high?

这是算法的巧妙之处:

  • 高3位和低3位共享同一个异或累积值xor_3

  • 低3位的编码依赖于当前字符的高3位编码结果

  • 这形成了字节内部的依赖关系

步骤3: 查表替换

TABLE3 = "XVEDULJT"
TABLE2 = "GWMQ"

cipher_0 = TABLE3[enc_high]  # TABLE3[3] = 'D'
cipher_1 = TABLE2[enc_mid]   # TABLE2[0] = 'G'
cipher_2 = TABLE3[enc_low]   # TABLE3[5] = 'L'

print(cipher_0 + cipher_1 + cipher_2)  # "DGL"

步骤4: 更新异或值

为下一个字符准备:

xor_3 = enc_high  # 更新为3
xor_2 = enc_mid   # 更新为0
xor_3 = enc_low   # 再次更新为5(覆盖之前的值)

验证:查看目标密文的前3个字符 → 正是"DGL"

5.3 完整加密算法实现

我们用Python实现完整算法:

#!/usr/bin/env python3

TABLE3 = "XVEDULJT"
TABLE2 = "GWMQ"

def encrypt(plaintext):
    """加密函数"""
    result = ""
    xor_3 = 0  # 3位异或累积值
    xor_2 = 0  # 2位异或累积值

    for i, char in enumerate(plaintext):
        byte_val = ord(char)

        # 位拆分
        high_3 = (byte_val >> 5) & 0b111
        mid_2 = (byte_val >> 3) & 0b11
        low_3 = byte_val & 0b111

        # 异或编码
        enc_high = high_3 ^ xor_3
        enc_mid = mid_2 ^ xor_2
        enc_low = low_3 ^ enc_high  # 关键:用enc_high

        # 查表替换
        cipher_char_0 = TABLE3[enc_high]
        cipher_char_1 = TABLE2[enc_mid]
        cipher_char_2 = TABLE3[enc_low]

        result += cipher_char_0 + cipher_char_1 + cipher_char_2

        # 更新异或值
        xor_3 = enc_high
        xor_2 = enc_mid
        xor_3 = enc_low  # 再次更新

        # 调试输出
        print(f"[{i:02d}] '{char}' (0x{byte_val:02x}) → "
              f"拆分:({high_3},{mid_2},{low_3}) → "
              f"编码:({enc_high},{enc_mid},{enc_low}) → "
              f"密文:{cipher_char_0}{cipher_char_1}{cipher_char_2}")

    return result

# 测试
plaintext = "flag{Sig Machine S0 E4sy for Y0U 233}\n"
encrypted = encrypt(plaintext)

TARGET = "DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ"

print(f"\n加密结果: {encrypted}")
print(f"目标密文: {TARGET}")
print(f"匹配结果: {encrypted == TARGET}")

运行结果:

[00] 'f' (0x66) → 拆分:(3,0,6) → 编码:(3,0,5) → 密文:DGL
[01] 'l' (0x6c) → 拆分:(3,1,4) → 编码:(6,1,2) → 密文:JWE
[02] 'a' (0x61) → 拆分:(3,0,1) → 编码:(1,1,0) → 密文:VWX
[03] 'g' (0x67) → 拆分:(3,0,7) → 编码:(3,1,4) → 密文:DWU
... (省略中间输出)

加密结果: DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ
目标密文: DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ
匹配结果: True

完美匹配!这证明我们完全理解了加密算法。

5.4 异或链的密码学意义

什么是异或链?

异或链(XOR Chain)是一种让当前数据的加密依赖于之前数据的技术,类似于密码学中的CBC模式。

为什么要用异或链?

  1. 前后依赖:第N个字符的加密结果影响第N+1个字符

  2. 雪崩效应:明文的微小变化会导致后续所有密文改变

  3. 抵抗攻击:相同的明文字符在不同位置产生不同密文

示例:

假设我们加密字符串"aa"(两个相同的'a'):

# 第一个'a': xor_3=0, xor_2=0
# 加密后: VWX
# 更新: xor_3=0

# 第二个'a': xor_3=0, xor_2=1  (注意xor_2已经变化)
# 加密后: VGX  (与第一个不同!)

两个相同的字符产生了不同的密文,这大大增强了安全性。


第六章:逆向工程 - 编写解密脚本

6.1 解密算法推导

理解了加密过程,解密就是逆向操作:

加密流程:

明文字节 → [位拆分] → [异或编码] → [查表] → 密文

解密流程:

密文 → [反查表] → [异或解码] → [位组合] → 明文字节

6.2 异或运算的可逆性

异或运算有一个重要特性:自逆性

如果: c = a ^ b
那么: a = c ^ b  (异或同一个值可以恢复原值)

证明: c ^ b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a

这就是为什么异或常用于加密:用同一个密钥异或两次就能恢复原文。

6.3 完整解密脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

TABLE3 = "XVEDULJT"
TABLE2 = "GWMQ"

# 目标密文(从程序中提取)
AIM = "DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ"

print(f"密文长度: {len(AIM)}")
print(f"应解密出: {len(AIM) // 3} 个字符\n")

result = ""
xor_3 = 0  # 异或累积值,初始为0
xor_2 = 0

for i in range(len(AIM) // 3):
    # 步骤1: 提取3个密文字符
    cipher_char_0 = AIM[i * 3 + 0]  # 对应高3位
    cipher_char_1 = AIM[i * 3 + 1]  # 对应中2位
    cipher_char_2 = AIM[i * 3 + 2]  # 对应低3位

    # 步骤2: 反查表(得到加密时的编码值)
    encrypted_high_3 = TABLE3.index(cipher_char_0)
    encrypted_mid_2 = TABLE2.index(cipher_char_1)
    encrypted_low_3 = TABLE3.index(cipher_char_2)

    # 步骤3: 异或解码(恢复原始位值)
    # 因为加密时: encrypted_value = original_value ^ xor_prev
    # 所以解密时: original_value = encrypted_value ^ xor_prev
    high_3 = encrypted_high_3 ^ xor_3
    mid_2 = encrypted_mid_2 ^ xor_2
    low_3 = encrypted_low_3 ^ encrypted_high_3  # 注意:用encrypted_high_3

    # 步骤4: 更新异或值(必须与加密时完全相同!)
    xor_3 = encrypted_high_3
    xor_2 = encrypted_mid_2
    xor_3 = encrypted_low_3  # 再次更新

    # 步骤5: 位组合(将3-2-3位重新拼成8位字节)
    #
    # 目标格式: 0b HHHMM LLL
    #              ↑  ↑  ↑
    #            高3 中2 低3
    #
    original_byte = ((high_3 << 5) & 0b11100000) | \
                    ((mid_2 << 3) & 0b00011000) | \
                    (low_3 & 0b00000111)

    result += chr(original_byte)

    # 调试输出
    print(f"[{i:02d}] 密文:{cipher_char_0}{cipher_char_1}{cipher_char_2} → "
          f"查表:({encrypted_high_3},{encrypted_mid_2},{encrypted_low_3}) → "
          f"解码:({high_3},{mid_2},{low_3}) → "
          f"字节:0x{original_byte:02x} → 字符:'{chr(original_byte)}'")

print(f"\n{'='*60}")
print(f"解密结果: {result}")
print(f"{'='*60}")
print(f"\nFlag: {result.strip()}")

6.4 解密过程演示

运行脚本:

$ python3 decrypt.py

输出:

密文长度: 114
应解密出: 38 个字符

[00] 密文:DGL → 查表:(3,0,5) → 解码:(3,0,6) → 字节:0x66 → 字符:'f'
[01] 密文:JWE → 查表:(6,1,2) → 解码:(3,1,4) → 字节:0x6c → 字符:'l'
[02] 密文:VWX → 查表:(1,1,0) → 解码:(3,0,1) → 字节:0x61 → 字符:'a'
[03] 密文:DWU → 查表:(3,1,4) → 解码:(3,0,7) → 字节:0x67 → 字符:'g'
[04] 密文:TMU → 查表:(7,2,4) → 解码:(3,3,3) → 字节:0x7b → 字符:'{'
[05] 密文:JGL → 查表:(6,0,5) → 解码:(2,2,3) → 字节:0x53 → 字符:'S'
[06] 密文:JWT → 查表:(6,1,7) → 解码:(3,1,1) → 字节:0x69 → 字符:'i'
[07] 密文:UWD → 查表:(4,1,3) → 解码:(3,0,7) → 字节:0x67 → 字符:'g'
[08] 密文:EWE → 查表:(2,1,2) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[09] 密文:XGL → 查表:(0,0,5) → 解码:(2,1,5) → 字节:0x4d → 字符:'M'
[10] 密文:JGT → 查表:(6,0,7) → 解码:(3,0,1) → 字节:0x61 → 字符:'a'
[11] 密文:UGT → 查表:(4,0,7) → 解码:(3,0,3) → 字节:0x63 → 字符:'c'
[12] 密文:UWU → 查表:(4,1,4) → 解码:(3,1,0) → 字节:0x68 → 字符:'h'
[13] 密文:TGJ → 查表:(7,0,6) → 解码:(3,1,1) → 字节:0x69 → 字符:'i'
[14] 密文:LWD → 查表:(5,1,3) → 解码:(3,1,6) → 字节:0x6e → 字符:'n'
[15] 密文:XWL → 查表:(0,1,5) → 解码:(3,0,5) → 字节:0x65 → 字符:'e'
[16] 密文:UWU → 查表:(4,1,4) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[17] 密文:JQL → 查表:(6,3,5) → 解码:(2,2,3) → 字节:0x53 → 字符:'S'
[18] 密文:UWU → 查表:(4,1,4) → 解码:(1,2,0) → 字节:0x30 → 字符:'0'
[19] 密文:LWL → 查表:(5,1,5) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[20] 密文:TWE → 查表:(7,1,2) → 解码:(2,0,5) → 字节:0x45 → 字符:'E'
[21] 密文:DQT → 查表:(3,3,7) → 解码:(1,2,4) → 字节:0x34 → 字符:'4'
[22] 密文:UWT → 查表:(4,1,7) → 解码:(3,2,3) → 字节:0x73 → 字符:'s'
[23] 密文:UML → 查表:(4,2,5) → 解码:(3,3,1) → 字节:0x79 → 字符:'y'
[24] 密文:UMU → 查表:(4,2,4) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[25] 密文:TMV → 查表:(7,2,1) → 解码:(3,0,6) → 字节:0x66 → 字符:'f'
[26] 密文:EQL → 查表:(2,3,5) → 解码:(3,1,7) → 字节:0x6f → 字符:'o'
[27] 密文:JWU → 查表:(6,1,4) → 解码:(3,2,2) → 字节:0x72 → 字符:'r'
[28] 密文:LWL → 查表:(5,1,5) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[29] 密文:TMJ → 查表:(7,2,6) → 解码:(2,3,1) → 字节:0x59 → 字符:'Y'
[30] 密文:TGT → 查表:(7,0,7) → 解码:(1,2,0) → 字节:0x30 → 字符:'0'
[31] 密文:LMX → 查表:(5,2,0) → 解码:(2,2,5) → 字节:0x55 → 字符:'U'
[32] 密文:VMV → 查表:(1,2,1) → 解码:(1,0,0) → 字节:0x20 → 字符:' '
[33] 密文:XGE → 查表:(0,0,2) → 解码:(1,2,2) → 字节:0x32 → 字符:'2'
[34] 密文:DMX → 查表:(3,2,0) → 解码:(1,2,3) → 字节:0x33 → 字符:'3'
[35] 密文:VGE → 查表:(1,0,2) → 解码:(1,2,3) → 字节:0x33 → 字符:'3'
[36] 密文:VQU → 查表:(1,3,4) → 解码:(3,3,5) → 字节:0x7d → 字符:'}'
[37] 密文:UMJ → 查表:(4,2,6) → 解码:(0,1,2) → 字节:0x0a → 字符:'
'

============================================================
解密结果: flag{Sig Machine S0 E4sy for Y0U 233}

============================================================

Flag: flag{Sig Machine S0 E4sy for Y0U 233}

第七章:验证时刻 - 提交Flag

最激动人心的时刻到了!让我们验证解密出的flag:

$ echo "flag{Sig Machine S0 E4sy for Y0U 233}" | ./sigmachine
password: right!

成功!


第八章:OLLVM混淆技术解析

8.1 什么是OLLVM?

OLLVM (Obfuscator-LLVM) 是基于LLVM编译器框架开发的代码混淆工具。它包含三种主要混淆技术:

  1. 控制流平坦化 (Control Flow Flattening)

  2. 虚假控制流 (Bogus Control Flow)

  3. 指令替换 (Instruction Substitution)

8.2 控制流平坦化详解

原始代码:

void check_password(char *input) {
    if (strlen(input) != 38) {
        printf("wrong!\n");
        return;
    }

    if (encrypt(input) == target) {
        printf("right!\n");
    } else {
        printf("wrong!\n");
    }
}

混淆后的伪代码:

void check_password(char *input) {
    int state = 0;  // 状态变量

    while (1) {
        switch (state) {
            case 0:  // 初始状态
                if (strlen(input) != 38) {
                    state = 1;  // 跳转到错误处理
                } else {
                    state = 2;  // 跳转到加密验证
                }
                break;

            case 1:  // 错误处理
                printf("wrong!\n");
                state = 99;  // 跳转到退出
                break;

            case 2:  // 加密验证
                if (encrypt(input) == target) {
                    state = 3;  // 跳转到成功
                } else {
                    state = 1;  // 跳转到错误
                }
                break;

            case 3:  // 成功处理
                printf("right!\n");
                state = 99;
                break;

            case 99:  // 退出
                return;
        }
    }
}

效果:

  • 原本清晰的if-else结构变成了复杂的switch-case

  • 程序流程变成了状态机,难以理解

  • IDA/Ghidra的反编译结果会非常混乱

8.3 本题如何应对OLLVM?

虽然OLLVM增加了分析难度,但我们可以:

  1. 忽略混淆,专注数据流

    • 不纠结于控制流,关注数据的读写

    • 追踪全局变量OPERAND[]和TMP[]的使用

  2. 识别关键模式

    • 找到raise()系统调用

    • 定位.rodata段的数据访问

  3. 动态分析辅助

    • 使用GDB观察内存变化

    • 记录输入输出关系

  4. 符号执行

    • 使用angr等工具自动求解

    • 让工具处理复杂的控制流

8.4 OLLVM的局限性

虽然OLLVM很强大,但也有弱点:

  1. 无法混淆数据

    • TABLE3、TABLE2这些常量表无法隐藏

    • 目标密文必须存储在程序中

  2. 无法隐藏系统调用

    • signal()raise()等调用仍然可见

    • 可以通过strace追踪

  3. 增加代码体积

    • 混淆后的代码通常是原代码的3-10倍

    • 运行速度也会下降


第九章:技术总结与进阶学习

9.1 核心技术点总结

技术作用学到的知识
ELF文件格式理解程序结构.init_array段、.rodata段、小端序
Constructor函数初始化机制main之前的代码执行
Linux信号系统运算隐藏signal()、raise()、信号处理函数
位运算数据编码移位、与、或、异或
3-2-3位拆分自定义加密字节拆分与组合
异或链增强安全性前后依赖、雪崩效应
OLLVM混淆代码保护控制流平坦化

9.2 解题流程方法论

1. 文件分析
   ↓
2. 字符串提取
   ↓
3. Constructor识别
   ↓
4. 信号处理分析
   ↓
5. 数据段提取
   ↓
6. 算法逆向
   ↓
7. 脚本编写
   ↓
8. Flag验证

9.3 使用的工具清单

工具用途替代方案
file识别文件类型-
strings提取字符串rabin2 -z
readelf查看ELF结构objdump -h
objdump反汇编IDA Pro, Ghidra
python3脚本编写Ruby, Perl
echo+ 程序验证flag-

9.4 推荐学习资源

书籍:

  • 《程序员的自我修养:链接、装载与库》- 深入理解ELF

  • 《逆向工程权威指南》- 系统学习逆向

  • 《深入理解计算机系统》(CSAPP) - 计算机基础

在线资源:

  • CTFtime.org - CTF比赛信息

  • Reverse Engineering Stack Exchange - 问答社区

  • LiveOverflow YouTube频道 - 视频教程

实践平台:

  • XCTF攻防世界 - 中文题库

  • pwnable.kr - 韩国平台

  • Crackmes.one - 破解练习


第十章:防御视角 - 如何检测这类程序

10.1 静态检测方法

1. 检查异常的信号注册

正常程序很少注册超过2-3个信号处理器,本题注册了11个:

$ objdump -d sigmachine | grep "call.*signal" | wc -l
11

2. 分析.init_array段

正常程序的.init_array通常只有1-2个函数,本题有7个:

$ readelf -x .init_array sigmachine | grep -c "00000000"
7

3. 检查字符集异常

密文只包含12个特定字符,这在正常文本中很罕见:

import string
with open('sigmachine', 'rb') as f:
    content = f.read()
    printable = set(string.printable)
    # 分析字符分布...

10.2 动态检测方法

1. 监控信号调用

使用strace追踪系统调用:

$ strace -e signal,rt_sigaction ./sigmachine 2>&1 | grep rt_sigaction
rt_sigaction(SIGINT, {...}, NULL, 8) = 0
rt_sigaction(SIGQUIT, {...}, NULL, 8) = 0
... (共11个)

2. Hook信号处理

使用LD_PRELOAD劫持signal函数:

#include <signal.h>
#include <stdio.h>

void (*orig_handler)(int);

void (*my_signal(int signum, void (*handler)(int)))(int) {
    printf("[HOOK] Registering signal %d\n", signum);
    // 调用原始signal函数...
}

10.3 自动化分析

使用angr进行符号执行:

import angr

# 加载二进制文件
proj = angr.Project('./sigmachine', auto_load_libs=False)

# 创建符号输入
flag = proj.loader.main_object.get_symbol('flag_buffer')
state = proj.factory.entry_state(stdin=angr.SimPacket('flag'))

# 设置约束
state.solver.add(state.regs.rax == 0x47c09a)  # "right!"地址

# 符号执行
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=0x47c09a)

# 输出结果
if simgr.found:
    print(simgr.found[0].posix.dumps(0))

尾声:逆向工程的本质

通过这道题,我们学到的不仅仅是技术,更重要的是逆向思维:

  1. 耐心观察- 从大量信息中提取关键线索

  2. 逻辑推理- 从现象推导原理

  3. 动手验证- 理论必须经过实践检验

  4. 系统思考- 将各个技术点串联成完整画面

SigMachine这道题巧妙地融合了:

  • ELF文件格式知识- 理解程序结构

  • 操作系统机制- 利用信号系统

  • 密码学思想- 位拆分和异或链

  • 代码混淆技术- OLLVM增加难度

它不是简单的"找到flag",而是一次完整的技术探索之旅。

最后,送给读者一句话:

"逆向工程的魅力在于,即使没有源代码,我们也能重建程序的逻辑。这需要的不仅是技术,更是对技术的热爱和永不放弃的精神。"


附录A:完整工具脚本

A.1 加密验证脚本

#!/usr/bin/env python3
"""
SigMachine加密算法实现与验证
用于验证我们对加密算法的理解
"""

TABLE3 = "XVEDULJT"
TABLE2 = "GWMQ"

def encrypt(plaintext):
    """
    实现3-2-3位拆分加密算法

    参数:
        plaintext: 明文字符串
    返回:
        加密后的密文字符串
    """
    result = ""
    xor_3 = 0
    xor_2 = 0

    for char in plaintext:
        byte_val = ord(char)

        # 位拆分
        high_3 = (byte_val >> 5) & 0b111
        mid_2 = (byte_val >> 3) & 0b11
        low_3 = byte_val & 0b111

        # 异或编码
        enc_high = high_3 ^ xor_3
        enc_mid = mid_2 ^ xor_2
        enc_low = low_3 ^ enc_high

        # 查表替换
        result += TABLE3[enc_high] + TABLE2[enc_mid] + TABLE3[enc_low]

        # 更新异或值
        xor_3 = enc_high
        xor_2 = enc_mid
        xor_3 = enc_low

    return result


if __name__ == "__main__":
    # 测试
    plaintext = "flag{Sig Machine S0 E4sy for Y0U 233}\n"
    encrypted = encrypt(plaintext)

    target = "DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ"

    print(f"明文: {plaintext}")
    print(f"密文: {encrypted}")
    print(f"目标: {target}")
    print(f"匹配: {encrypted == target}")

A.2 解密脚本

#!/usr/bin/env python3
"""
SigMachine解密脚本
从密文恢复明文flag
"""

TABLE3 = "XVEDULJT"
TABLE2 = "GWMQ"
CIPHERTEXT = "DGLJWEVWXDWUTMUJGLJWTUWDEWEXGLJGTUGTUWUTGJLWDXWLUWUJQLUWULWLTWEDQTUWTUMLUMUTMVEQLJWULWLTMJTGTLMXVMVXGEDMXVGEVQUUMJ"

def decrypt(ciphertext):
    """
    解密函数

    参数:
        ciphertext: 密文字符串(长度必须是3的倍数)
    返回:
        解密后的明文字符串
    """
    if len(ciphertext) % 3 != 0:
        raise ValueError("密文长度必须是3的倍数")

    result = ""
    xor_3 = 0
    xor_2 = 0

    for i in range(len(ciphertext) // 3):
        # 提取3个密文字符
        cipher_char_0 = ciphertext[i * 3 + 0]
        cipher_char_1 = ciphertext[i * 3 + 1]
        cipher_char_2 = ciphertext[i * 3 + 2]

        # 反查表
        encrypted_high_3 = TABLE3.index(cipher_char_0)
        encrypted_mid_2 = TABLE2.index(cipher_char_1)
        encrypted_low_3 = TABLE3.index(cipher_char_2)

        # 异或解码
        high_3 = encrypted_high_3 ^ xor_3
        mid_2 = encrypted_mid_2 ^ xor_2
        low_3 = encrypted_low_3 ^ encrypted_high_3

        # 更新异或值
        xor_3 = encrypted_high_3
        xor_2 = encrypted_mid_2
        xor_3 = encrypted_low_3

        # 位组合
        original_byte = ((high_3 << 5) & 0b11100000) | \
                        ((mid_2 << 3) & 0b00011000) | \
                        (low_3 & 0b00000111)

        result += chr(original_byte)

    return result


if __name__ == "__main__":
    print("SigMachine解密工具")
    print("=" * 60)
    print(f"密文长度: {len(CIPHERTEXT)}")
    print(f"明文长度: {len(CIPHERTEXT) // 3}")
    print("=" * 60)

    plaintext = decrypt(CIPHERTEXT)

    print(f"\n解密结果:\n{plaintext}")
    print(f"\nFlag: {plaintext.strip()}")

A.3 批量测试脚本

#!/usr/bin/env python3
"""
批量测试加密/解密的正确性
"""

from encrypt import encrypt
from decrypt import decrypt

test_cases = [
    "flag{test}",
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    "0123456789",
    "Hello, World!\n",
    "The quick brown fox jumps over the lazy dog",
]

print("批量测试加密/解密算法")
print("=" * 70)

for i, plaintext in enumerate(test_cases, 1):
    print(f"\n测试用例 {i}:")
    print(f"原文: {repr(plaintext)}")

    # 加密
    ciphertext = encrypt(plaintext)
    print(f"密文: {ciphertext}")

    # 解密
    recovered = decrypt(ciphertext)
    print(f"解密: {repr(recovered)}")

    # 验证
    match = plaintext == recovered
    print(f"匹配: {' 成功' if match else ' 失败'}")

    if not match:
        print(f"错误: 原文与解密结果不匹配!")
        break

print("\n" + "=" * 70)
print("所有测试完成!")

附录B:常用命令速查表

# ===== 文件分析 =====
file <binary>                    # 识别文件类型
strings <binary>                 # 提取字符串
strings -tx <binary>             # 显示字符串地址

# ===== ELF分析 =====
readelf -h <binary>              # 查看ELF头
readelf -S <binary>              # 查看段表
readelf -x .rodata <binary>      # 查看rodata段
readelf -x .init_array <binary>  # 查看constructor

# ===== 反汇编 =====
objdump -d <binary>              # 反汇编全部代码
objdump -d -M intel <binary>     # Intel语法
objdump -s -j .rodata <binary>   # 查看rodata数据

# ===== 动态调试 =====
gdb <binary>                     # 启动GDB
strace <binary>                  # 追踪系统调用
ltrace <binary>                  # 追踪库函数调用

# ===== 运行测试 =====
echo "test" | ./<binary>         # 管道输入
./<binary> <<< "test"            # Here字符串
python -c "print('test')" | ./<binary>  # Python生成输入

# ===== 数据提取 =====
xxd <binary> | grep "pattern"    # 十六进制查看
hexdump -C <binary>              # 十六进制+ASCII
rabin2 -z <binary>               # radare2提取字符串

附录C:参考资料

C.1 官方文档

  • ELF格式规范:https://refspecs.linuxfoundation.org/elf/elf.pdf

  • Linux信号手册:man 7 signal

  • System V ABI:https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf

C.2 工具文档

  • objdump手册:man objdump

  • readelf手册:man readelf

  • GDB文档:https://sourceware.org/gdb/documentation/

C.3 学习资源

  • OLLVM项目:https://github.com/obfuscator-llvm/obfuscator

  • Reverse Engineering for Beginners:https://beginners.re/

  • CTF Wiki:https://ctf-wiki.org/

C.4 推荐书籍

  1. 《程序员的自我修养:链接、装载与库》俞甲子等著

  2. 《深入理解计算机系统》(CSAPP) Randal E. Bryant

  3. 《逆向工程权威指南》Dennis Yurichev

  4. 《加密与解密》段钢著


附录D:术语表

术语英文解释
逆向工程Reverse Engineering从程序分析其设计和实现
静态分析Static Analysis不运行程序的分析
动态分析Dynamic Analysis运行程序时的分析
ELFExecutable and Linkable FormatLinux可执行文件格式
符号表Symbol Table函数名、变量名等调试信息
小端序Little Endian低位字节在前的存储方式
大端序Big Endian高位字节在前的存储方式
信号SignalUnix/Linux进程间通信机制
构造函数Constructormain之前执行的初始化函数
位运算Bitwise Operation对二进制位的操作
异或XOR (Exclusive OR)相同为0,不同为1的运算
混淆Obfuscation增加代码理解难度的技术
控制流Control Flow程序执行的路径
数据流Data Flow数据在程序中的传递

最终Flag:flag{Sig Machine S0 E4sy for Y0U 233}

作者寄语:愿每一位逆向工程师都能在二进制的世界中找到属于自己的快乐!


文章来源: https://www.freebuf.com/articles/others-articles/458639.html
如有侵权请联系:admin#unsafe.sh