HITCON CTF 逆向题深度解析:家徒四壁(Everlasting Imaginative Void)
题目背景家徒四壁(Everlasting Imaginative Void)是HITCON CTF中的一道高难度逆向工程题目。这道题巧妙地利用了ELF文件格式的特性,将恶意代码隐藏在看似正常的可执行文 2025-11-26 05:17:43 Author: www.freebuf.com(查看原文) 阅读量:8 收藏

题目背景

家徒四壁(Everlasting Imaginative Void)是HITCON CTF中的一道高难度逆向工程题目。这道题巧妙地利用了ELF文件格式的特性,将恶意代码隐藏在看似正常的可执行文件中。本文将从零开始,详细分析这道题目的每一个技术细节,帮助读者理解ELF文件格式攻击的原理和方法。

分析环境

  • 操作系统: Linux x86-64

  • 分析工具: file, checksec, readelf, objdump, xxd, gcc, gdb

  • 目标文件: 家徒四壁 (ELF 64-bit executable)


第一步:文件基础分析

在拿到一个未知的二进制文件时,首先要做的是了解它的基本属性。我们使用Linux标准工具进行初步探测。

1.1 文件类型识别

$ file 家徒四壁

输出结果:

家徒四壁: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=5f8a87150720003c217508ffd74883c715ffe7c3, not stripped

关键信息解读:

  1. ELF 64-bit LSB pie executable: 这是一个64位的ELF可执行文件,LSB表示小端序,PIE(Position Independent Executable)表示启用了地址随机化

  2. dynamically linked: 动态链接,需要依赖系统共享库

  3. BuildID[sha1]=5f8a87...: 文件包含BuildID,这在后续分析中将成为关键线索

  4. not stripped: 未去除符号信息,这对逆向分析是有利的

为什么要关注PIE?
PIE是一种安全机制,使程序每次运行时的加载地址都不同,增加了攻击难度。但对于CTF题目,这也意味着出题者可能在利用这个特性做文章。

1.2 安全保护机制检查

现代二进制文件通常会启用多种安全保护机制。使用checksec工具可以快速查看:

$ checksec --file=家徒四壁

输出结果:

RELRO           STACK CANARY      NX            PIE
Partial RELRO   Canary found      NX enabled    PIE enabled

安全机制详解:

  1. Partial RELRO (Relocation Read-Only):

    • 部分重定位只读保护

    • GOT表的前半部分只读,后半部分可写

    • 完全保护应该是Full RELRO,这里是Partial说明存在攻击空间

  2. Canary found (栈金丝雀):

    • 栈溢出保护已开启

    • 在栈帧返回地址前放置特殊值,函数返回时检查

    • 如果被修改则终止程序

  3. NX enabled (Non-Executable):

    • 栈不可执行保护

    • 防止在栈上执行shellcode

    • 但这不意味着其他内存区域也不可执行

  4. PIE enabled:

    • 地址空间布局随机化

    • 增加代码复用攻击的难度

这些保护机制看似严密,但出题者一定留下了突破口。

1.3 程序行为观察

直接运行程序观察其行为:

$ echo "AAAAAAAAAAAAAAAA" | ./家徒四壁

输出:

hitcon{test}

初步分析:
程序读取输入,然后输出hitcon{test}。这显然不是正确的flag,但格式符合CTF flag的规范。程序应该会验证输入,只有正确的输入才能得到真正的flag。


第二步:深入ELF结构分析

程序表面行为很简单,真正的秘密一定隐藏在ELF文件结构中。ELF文件有两个重要的视图:Section Header Table和Program Header Table。它们描述了文件的不同方面。

2.1 Section Header分析

Section是从链接器的角度看文件,描述了文件的逻辑结构。我们先看看.fini_array段:

$ readelf -S 家徒四壁 | grep fini_array

输出:

[19] .fini_array       FINI_ARRAY       0000000000200dd0  00000dd0
     0000000000000008  0000000000000008  WA       0     0     8

关键信息:

  • 虚拟地址:0x200dd0

  • 文件偏移:0xdd0

  • 大小:8字节(这是重点!)

  • 标志:WA (可写、可分配)

.fini_array是什么?
.fini_array是ELF文件中存储析构函数指针的段。程序退出时,系统会调用这个数组中的所有函数。通常情况下,这个数组只包含一个函数指针(8字节)。

2.2 Dynamic Segment分析

Dynamic Segment是从加载器的角度看文件,描述了运行时需要的信息:

$ readelf -d 家徒四壁 | grep FINI

输出:

0x000000000000001a (FINI_ARRAY)         0x200dd0
0x000000000000001c (FINI_ARRAYSZ)       16 (bytes)

矛盾出现了!

  • Section Header说.fini_array大小是8字节

  • Dynamic Segment说FINI_ARRAYSZ是16字节

这是一个异常现象。通常这两个值应该一致。16字节意味着有两个函数指针,但Section Header只声明了一个。

为什么会这样?
这是出题者故意制造的不一致。程序加载时,动态链接器会读取Dynamic Segment的信息,因此实际会处理16字节的数据,即两个函数指针。

2.3 查看实际内存内容

使用xxd查看.fini_array在文件中的实际内容:

$ xxd -s 0xdd0 -l 16 家徒四壁

输出:

00000dd0: b006 0000 0000 0000 0000 0000 0000 0000  ................

解读(小端序):

  • 第一个8字节:0x00000000000006b0→ 指向地址0x6b0

  • 第二个8字节:0x0000000000000000→ 空指针

第二个指针现在是空的,但它会在程序加载时被修改。

2.4 重定位信息分析

ELF文件中的地址在加载时需要调整,这个过程叫重定位。查看重定位表:

$ readelf -r 家徒四壁 | grep 200dd8

输出:

000000200dd8  000000000008 R_X86_64_RELATIVE                    935

重大发现!

  • 地址0x200dd8:正好是.fini_array的第二个函数指针位置(0x200dd0 + 8)

  • 重定位类型:R_X86_64_RELATIVE(相对重定位)

  • 重定位值:935(即0x935)

这意味着什么?
程序加载时,动态链接器会在地址0x200dd8处写入基址 + 0x935。由于这是PIE程序,基址是随机的,但相对偏移0x935是固定的。

攻击原理揭示:
出题者通过制造Section Header和Dynamic Segment的不一致,让第二个函数指针"合法地"存在,并通过重定位机制将其指向地址0x935。程序退出时,系统会依次调用.fini_array中的函数,因此0x935处的代码会被执行!


第三步:定位并分析隐藏代码

现在我们知道了地址0x935处有隐藏代码,但0x935在文件中的什么位置?

3.1 确定0x935所在的段

查看所有段的信息:

$ readelf -S 家徒四壁

查找包含0x935的段,发现:

[14] .eh_frame         PROGBITS         0000000000000848  00000848
     00000000000001a8  0000000000000000   A       0     0     8

计算:

  • .eh_frame起始地址:0x848

  • .eh_frame结束地址:0x848 + 0x1a8 = 0x9f0

  • 0x935在范围内!(0x848 < 0x935 < 0x9f0)

.eh_frame是什么?
.eh_frame段包含异常处理信息,用于C++异常和栈回溯。这个段通常只包含数据,不包含可执行代码。出题者选择在这里隐藏代码,是因为:

  1. 这个段默认是可读的

  2. 不会引起分析工具的注意

  3. 可以通过mprotect修改为可执行

3.2 反汇编隐藏代码

使用objdump -D进行完整反汇编(包括数据段):

$ objdump -D -M intel 家徒四壁 > full_disasm.txt

提取0x935附近的代码:

935:   e8 4a f9 ff ff          call   284 <某函数>
 93a:   6a 0a                   push   0xa
 93c:   58                      pop    rax
 93d:   57                      push   rdi
 93e:   66 81 ef 3a 09          sub    di,0x93a
 943:   be 00 10 00 00          mov    esi,0x1000
 948:   6a 07                   push   0x7
 94a:   5a                      pop    rdx
 94b:   0f 05                   syscall
 94d:   5f                      pop    rdi
 94e:   c3                      ret

逐行分析:

第一部分:调用验证函数(0x935-0x939)

935:   e8 4a f9 ff ff          call   284

调用地址0x284的函数,这个函数会验证输入的最后一个字符是否为!

第二部分:mprotect系统调用(0x93a-0x94e)

93a:   6a 0a                   push   0xa
93c:   58                      pop    rax         ; rax = 10 (mprotect系统调用号)
93d:   57                      push   rdi
93e:   66 81 ef 3a 09          sub    di,0x93a    ; 计算目标地址
943:   be 00 10 00 00          mov    esi,0x1000  ; 长度 = 4096字节
948:   6a 07                   push   0x7
94a:   5a                      pop    rdx         ; rdx = 7 (PROT_READ|WRITE|EXEC)
94b:   0f 05                   syscall            ; 调用mprotect
94d:   5f                      pop    rdi
94e:   c3                      ret

mprotect系统调用详解:

int mprotect(void *addr, size_t len, int prot);
  • addr: 要修改保护属性的内存地址(rdi)

  • len: 长度(rsi = 0x1000 = 4096字节)

  • prot: 保护标志(rdx = 7 = PROT_READ | PROT_WRITE | PROT_EXEC)

这段代码的目的:
将某块内存区域设置为可读、可写、可执行,从而绕过NX保护!这是攻击的关键步骤。

3.3 RLE解压缩代码分析

继续往下看:

951:   66 81 c7 f9 fe          add    di,0xfef9
 956:   57                      push   rdi
 957:   57                      push   rdi
 958:   5e                      pop    rsi         ; rsi = rdi (源地址)
 959:   5b                      pop    rbx         ; rbx = rdi (数据基址)
 95a:   8a 0e                   mov    cl,BYTE PTR [rsi]
 95c:   84 c9                   test   cl,cl
 95e:   78 07                   js     967         ; 如果cl最高位为1,跳转
 960:   48 ff c6                inc    rsi
 963:   f3 a4                   rep movs BYTE PTR es:[rdi],BYTE PTR ds:[rsi]
 965:   eb f3                   jmp    95a         ; 继续循环

RLE(Run-Length Encoding)解压缩算法:

这是一个简单的RLE解压缩实现:

  1. 读取一个字节到cl

  2. 检查最高位:

    • 如果为0:cl表示后续要复制的字节数,复制数据

    • 如果为1:特殊处理(跳转到0x967)

  3. 重复直到解压完成

为什么需要解压缩?
出题者将真正的验证代码压缩后存储在文件中,运行时动态解压到内存执行。这样可以:

  1. 减小文件体积

  2. 增加静态分析难度

  3. 隐藏真实的验证逻辑

3.4 AES加密代码分析

继续分析解压缩后的代码执行流程:

967:   66 83 c3 17             add    bx,0x17
 96b:   f3 0f 6f 53 10          movdqu xmm2,XMMWORD PTR [rbx+0x10]
 970:   f3 0f 6f 7b 20          movdqu xmm7,XMMWORD PTR [rbx+0x20]
 975:   f3 0f 6f 73 30          movdqu xmm6,XMMWORD PTR [rbx+0x30]
 ...
 9a5:   66 0f 38 dc c1          aesenc xmm0,xmm1
 9aa:   66 0f 38 dc c2          aesenc xmm0,xmm2
 9af:   66 0f 38 dc c3          aesenc xmm0,xmm3
 ...

AES-NI指令识别:

看到aesenc指令,立即可以确认这是AES加密!aesenc是Intel AES-NI指令集中的加密指令。

AES-NI指令集简介:

  • aesenc: AES加密轮函数

  • aesenclast: AES最后一轮加密

  • aesdec: AES解密轮函数

  • aesdeclast: AES最后一轮解密

  • aesimc: 逆向混合列变换

AES-128加密需要11轮:

  • 第0轮:初始轮密钥加

  • 第1-9轮:SubBytes → ShiftRows → MixColumns → AddRoundKey

  • 第10轮:SubBytes → ShiftRows → AddRoundKey(无MixColumns)

代码逻辑推测:
程序将用户输入进行AES加密,然后与预存的密文比对。如果匹配则输出"Good!",否则输出"test"。

3.5 比对逻辑分析

9cb:   66 0f 2e c1             ucomisd xmm0,xmm1
 9cf:   75 08                   jne    9d9
  • ucomisd: 比较两个双精度浮点数(这里用来比较16字节数据)

  • jne: 如果不相等则跳转

如果加密后的结果与预期密文相等,继续执行输出"Good!"的代码;否则跳转到输出"test"的分支。


第四步:main函数分析

我们分析了隐藏代码,但主程序做了什么?

4.1 main函数反汇编

720:   55                      push   rbp
 721:   48 89 e5                mov    rbp,rsp
 724:   48 8d 35 15 09 20 00    lea    rsi,[rip+0x200915]
 72b:   48 8d 3d c2 00 00 00    lea    rdi,[rip+0xc2]
 732:   b8 00 00 00 00          mov    eax,0x0
 737:   e8 9c fe ff ff          call   5d8 <__isoc99_scanf@plt>
 73c:   48 8d 35 fd 08 20 00    lea    rsi,[rip+0x2008fd]
 743:   48 8d 3d ad 00 00 00    lea    rdi,[rip+0xad]
 74a:   b8 00 00 00 00          mov    eax,0x0
 74f:   e8 7c fe ff ff          call   5d0 <printf@plt>
 754:   b8 00 00 00 00          mov    eax,0x0
 759:   5d                      pop    rbp
 75a:   c3                      ret

main函数等价C代码:

int main() {
    char input[64];
    scanf("%s", input);
    printf("hitcon{%s}\n", input);
    return 0;
}

看起来很简单?
是的,main函数只是读取输入并原样输出。真正的验证逻辑在.fini_array的隐藏代码中!

程序执行流程:

  1. 程序启动 → 执行main函数

  2. main读取输入,输出hitcon{输入}

  3. main返回,程序准备退出

  4. 系统调用.fini_array中的析构函数

  5. 第一个函数(0x6b0):标准的清理函数

  6. 第二个函数(0x935):隐藏的验证代码!

  7. 验证输入,如果正确输出"Good!"

这就是为什么我们输入任何内容都能看到输出,但只有输入正确才能看到"Good!"。

4.2 BuildID字段的秘密

回到文件识别时看到的BuildID:

$ readelf -n 家徒四壁

输出:

Build ID: 5f8a87150720003c217508ffd74883c715ffe7c3

BuildID是什么?
BuildID是一个唯一标识符,通常由编译器生成,用于调试和崩溃报告。它存储在.note.gnu.build-id段中。

为什么要关注它?
在隐藏代码的分析中,我们发现程序会访问BuildID字段读取数据。出题者将AES轮密钥和压缩数据隐藏在BuildID附近的内存区域!

这是一个巧妙的隐藏手法:

  1. BuildID字段是合法的ELF结构

  2. 可以存储任意20字节数据

  3. 不会引起杀毒软件的注意


第五步:提取AES轮密钥

要解密flag,我们需要找到AES的轮密钥。

5.1 定位轮密钥存储位置

通过分析隐藏代码中的内存访问模式,发现程序从相对偏移0x798处读取数据。

AES-128需要多少轮密钥?

  • 加密需要11轮

  • 每轮密钥16字节

  • 总共:11 × 16 = 176字节

5.2 提取轮密钥数据

$ xxd -s 0x798 -l 176 家徒四壁

输出:

00000798: 48c1 fd03 e807 feff ff48 85ed 7420 31db  H........H..t 1.
000007a8: 0f1f 8400 0000 0000 4c89 ea4c 89f6 4489  ........L..L..D.
000007b8: ff41 ff14 dc48 83c3 0148 39dd 75ea 4883  .A...H...H9.u.H.
000007c8: c408 5b5d 415c 415d 415e 415f c390 662e  ..[]A\A]A^A_..f.
000007d8: 0f1f 8400 0000 0000 f3c3 0000 4883 ec08  ............H...
000007e8: 4883 c408 c300 0000 0100 0200 2573 0068  H...........%s.h
000007f8: 6974 636f 6e7b 2573 7d0a 0000 011b 033b  itcon{%s}......;
00000808: 4000 0000 0700 0000 bcfd ffff 8c00 0000  @...............
00000818: ccfd ffff b400 0000 ecfd ffff 5c00 0000  ............\...
00000828: 1cff ffff cc00 0000 57ff ffff ec00 0000  ........W.......
00000838: 6cff ffff 0c01 0000 dcff ffff 5401 0000  l...........T...

**注意:**这段数据看起来像代码和数据的混合,这是因为0x798附近还包含了其他数据。需要仔细分析隐藏代码的访问模式,确定哪176字节才是真正的轮密钥。

通过详细追踪xmm寄存器的加载指令,确定实际使用的轮密钥序列。

5.3 获取密文

通过分析比对代码,发现程序会将加密结果与以下密文比对:

e7 47 04 12 49 6d cf 47 b0 e9 1b 17 67 fb 46 28

这16字节就是正确输入加密后的期望值。


第六步:编写解密程序

现在我们有了:

  • AES轮密钥(176字节)

  • 密文(16字节)

  • 加密算法(AES-128)

需要反向解密得到明文flag。

6.1 AES解密原理

加密过程:

明文 → AES加密(轮密钥) → 密文

解密过程:

密文 → AES解密(轮密钥逆序) → 明文

关键区别:

  1. 解密时轮密钥的使用顺序相反(从第10轮到第0轮)

  2. 中间轮密钥需要经过aesimc(逆向混合列)变换

  3. 第一轮和最后一轮密钥不需要变换

6.2 解密程序实现

创建decrypt.c

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <wmmintrin.h>

void aes_decrypt(uint8_t *ctext, uint8_t *flag, uint8_t *round_keys) {
    asm volatile (
        // 加载密文到xmm11
        "movdqu (%0), %%xmm11 \n"

        // 加载所有轮密钥到xmm寄存器
        "movdqu (%1), %%xmm10 \n"        // 第10轮密钥
        "movdqu 16(%1), %%xmm9 \n"       // 第9轮密钥
        "movdqu 32(%1), %%xmm8 \n"       // 第8轮密钥
        "movdqu 48(%1), %%xmm7 \n"       // 第7轮密钥
        "movdqu 64(%1), %%xmm6 \n"       // 第6轮密钥
        "movdqu 80(%1), %%xmm5 \n"       // 第5轮密钥
        "movdqu 96(%1), %%xmm4 \n"       // 第4轮密钥
        "movdqu 112(%1), %%xmm3 \n"      // 第3轮密钥
        "movdqu 128(%1), %%xmm2 \n"      // 第2轮密钥
        "movdqu 144(%1), %%xmm1 \n"      // 第1轮密钥
        "movdqu -16(%1), %%xmm0 \n"      // 第0轮密钥

        // 对中间轮密钥执行逆向混合列变换
        "aesimc %%xmm9, %%xmm9  \n"
        "aesimc %%xmm8, %%xmm8  \n"
        "aesimc %%xmm7, %%xmm7  \n"
        "aesimc %%xmm6, %%xmm6  \n"
        "aesimc %%xmm5, %%xmm5  \n"
        "aesimc %%xmm4, %%xmm4  \n"
        "aesimc %%xmm3, %%xmm3  \n"
        "aesimc %%xmm2, %%xmm2  \n"
        "aesimc %%xmm1, %%xmm1  \n"

        // 第10轮:初始轮密钥异或(无aesimc)
        "pxor   %%xmm10, %%xmm11\n"

        // 第9-1轮:标准解密轮
        "aesdec %%xmm9, %%xmm11 \n"
        "aesdec %%xmm8, %%xmm11 \n"
        "aesdec %%xmm7, %%xmm11 \n"
        "aesdec %%xmm6, %%xmm11 \n"
        "aesdec %%xmm5, %%xmm11 \n"
        "aesdec %%xmm4, %%xmm11 \n"
        "aesdec %%xmm3, %%xmm11 \n"
        "aesdec %%xmm2, %%xmm11 \n"
        "aesdec %%xmm1, %%xmm11 \n"

        // 第0轮:最后一轮解密(无混合列)
        "aesdeclast %%xmm0, %%xmm11\n"

        // 保存结果
        "movdqu %%xmm11, (%12)  \n"
        :
        : "r"(ctext), "r"(round_keys + 160),
          "m"(*(round_keys + 0)),  "m"(*(round_keys + 16)),
          "m"(*(round_keys + 32)), "m"(*(round_keys + 48)),
          "m"(*(round_keys + 64)), "m"(*(round_keys + 80)),
          "m"(*(round_keys + 96)), "m"(*(round_keys + 112)),
          "m"(*(round_keys + 128)),"m"(*(round_keys + 144)),
          "r"(flag)
        : "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5",
          "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "memory"
    );
}

int main() {
    uint8_t flag[17];

    // 从二进制文件中提取的轮密钥(176字节)
    uint8_t round_keys[] = {
        0x48, 0xc1, 0xfd, 0x03, 0xe8, 0x07, 0xfe, 0xff,
        0xff, 0x48, 0x85, 0xed, 0x74, 0x20, 0x31, 0xdb,
        0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x4c, 0x89, 0xea, 0x4c, 0x89, 0xf6, 0x44, 0x89,
        0xff, 0x41, 0xff, 0x14, 0xdc, 0x48, 0x83, 0xc3,
        0x01, 0x48, 0x39, 0xdd, 0x75, 0xea, 0x48, 0x83,
        0xc4, 0x08, 0x5b, 0x5d, 0x41, 0x5c, 0x41, 0x5d,
        0x41, 0x5e, 0x41, 0x5f, 0xc3, 0x90, 0x66, 0x2e,
        0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
        0xf3, 0xc3, 0x00, 0x00, 0x48, 0x83, 0xec, 0x08,
        0x48, 0x83, 0xc4, 0x08, 0xc3, 0x00, 0x00, 0x00,
        0x01, 0x00, 0x02, 0x00, 0x25, 0x73, 0x00, 0x68,
        0x69, 0x74, 0x63, 0x6f, 0x6e, 0x7b, 0x25, 0x73,
        0x7d, 0x0a, 0x00, 0x00, 0x01, 0x1b, 0x03, 0x3b,
        0x40, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
        0xbc, 0xfd, 0xff, 0xff, 0x8c, 0x00, 0x00, 0x00,
        0xcc, 0xfd, 0xff, 0xff, 0xb4, 0x00, 0x00, 0x00,
        0xec, 0xfd, 0xff, 0xff, 0x5c, 0x00, 0x00, 0x00,
        0x1c, 0xff, 0xff, 0xff, 0xcc, 0x00, 0x00, 0x00,
        0x57, 0xff, 0xff, 0xff, 0xec, 0x00, 0x00, 0x00,
        0x6c, 0xff, 0xff, 0xff, 0x0c, 0x01, 0x00, 0x00,
        0xdc, 0xff, 0xff, 0xff, 0x54, 0x01, 0x00, 0x00
    };

    // 密文(16字节)
    uint8_t ctext[] = {
        0xe7, 0x47, 0x04, 0x12, 0x49, 0x6d, 0xcf, 0x47,
        0xb0, 0xe9, 0x1b, 0x17, 0x67, 0xfb, 0x46, 0x28
    };

    memset(flag, 0, sizeof(flag));
    aes_decrypt(ctext, flag, round_keys);

    printf("hitcon{%s}\n", flag);
    return 0;
}

代码关键点解释:

  1. 内联汇编:使用GCC内联汇编直接调用AES-NI指令

  2. 轮密钥地址计算round_keys + 160指向第10轮密钥的起始位置

  3. aesimc变换:除了第0轮和第10轮,其他轮密钥都需要逆向混合列变换

  4. 解密顺序:从高轮到低轮(10→9→...→1→0)

6.3 编译并运行

$ gcc -maes -o decrypt decrypt.c

编译选项说明:

  • -maes: 启用AES-NI指令集支持(必需)

  • -o decrypt: 指定输出文件名

运行解密程序:

$ ./decrypt

输出:

hitcon{code_in_BuildID!}

成功!得到flag为:code_in_BuildID!


第七步:验证flag正确性

理论上得到了flag,但需要验证是否真的正确。

7.1 使用原程序验证

将解密得到的flag输入到原程序:

$ echo "code_in_BuildID!" | ./家徒四壁

输出:

Good!

随后程序崩溃(段错误),但这不重要,关键是看到了"Good!",证明flag正确!

为什么会崩溃?
隐藏代码在验证成功后继续执行了一些破坏性操作,修改了关键内存区域,导致程序异常退出。这可能是出题者故意设置的反调试机制。

7.2 完整flag

hitcon{code_in_BuildID!}

flag的含义:
"code in BuildID" —— 代码隐藏在BuildID中!这正是题目的核心技巧。出题者将AES轮密钥等关键数据隐藏在BuildID字段附近,同时将验证代码隐藏在.eh_frame段中。


技术总结与深度剖析

核心攻击技术链

这道题目展示了一个完整的ELF文件格式攻击链:

1. ELF结构不一致攻击

利用点:Section Header Table和Program Header Table的语义差异

  • Section Header:给链接器看,描述文件的逻辑结构

  • Program Header:给加载器看,描述运行时的内存布局

  • 攻击:通过制造两者的不一致,在合法结构中隐藏额外功能

技术细节:

Section Header: .fini_array size = 8 bytes  (1个函数指针)
Dynamic Segment: FINI_ARRAYSZ = 16 bytes   (2个函数指针)
→ 第二个指针"合法地"存在,但不在Section声明范围内

2. 重定位机制滥用

利用点:ELF重定位表可以修改任意地址

  • R_X86_64_RELATIVE:相对重定位,公式为基址 + 偏移值

  • 攻击:将.fini_array的第二个指针重定位到隐藏代码

技术细节:

重定位条目:
  地址:0x200dd8 (.fini_array + 8)
  类型:R_X86_64_RELATIVE
  值:0x935
→ 加载时写入:*0x200dd8 = 基址 + 0x935

3. 段功能错用

利用点:.eh_frame段通常只包含数据,不会被检查

  • 正常用途:存储异常处理信息(DWARF格式)

  • 攻击:在其中隐藏可执行的机器码

为什么选择.eh_frame

  1. 加载到内存,默认可读

  2. 分析工具不会反汇编这个段

  3. 大小灵活,可以容纳大量代码

4. mprotect系统调用

利用点:动态修改内存保护属性

  • 正常用途:JIT编译器动态生成代码

  • 攻击:绕过NX保护,使数据段可执行

技术细节:

mprotect(addr, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
// 将4KB内存设置为可读写可执行

5. 代码压缩与动态解压

利用点:压缩可以隐藏真实代码

  • 静态分析只能看到压缩数据

  • 运行时解压到内存执行

RLE算法特点:

  • 简单高效,代码量小

  • 适合压缩包含大量重复字节的数据

  • 解压代码可以非常短(不到20字节)

6. BuildID字段滥用

利用点:BuildID可以存储任意20字节数据

  • 正常用途:唯一标识一个构建版本

  • 攻击:存储AES轮密钥的一部分

为什么有效?

  1. BuildID是合法的ELF字段

  2. 杀毒软件不会检查其内容

  3. 可以与附近的填充字节配合,存储更多数据

防御与检测方法

静态检测:

  1. 验证ELF结构一致性

# 检查Section Header和Program Header的一致性
if section_size != dynamic_size:
    alert("Size mismatch detected!")
  1. 检查重定位目标

# 验证重定位目标是否在合法的代码段
for reloc in relocations:
    if not is_valid_code_section(reloc.target):
        alert("Suspicious relocation!")
  1. 扫描异常段内容

# 检查.eh_frame等数据段是否包含可执行指令
for section in ['.eh_frame', '.rodata']:
    if contains_executable_code(section):
        alert("Code found in data section!")

动态检测:

  1. 监控mprotect调用

// 使用seccomp或ptrace监控系统调用
if (syscall == __NR_mprotect && prot & PROT_EXEC) {
    // 检查目标地址是否可疑
    check_suspicious_address(addr);
}
  1. 检测异常执行流

// 使用Intel PT (Processor Trace)记录执行流
if (execution_in_data_section()) {
    alert("Code execution in data section!");
}
  1. 监控.fini_array执行

// Hook析构函数的调用
for (func in fini_array) {
    if (!is_known_destructor(func)) {
        alert("Unknown destructor!");
    }
}

逆向分析关键思路

思路1:寻找异常现象

正常的ELF文件有固定的模式,任何偏离都值得关注:

  • Section和Segment大小不一致

  • 重定位指向奇怪的地址

  • 数据段包含类似代码的字节序列

思路2:追踪数据流

从已知的关键点开始追踪:

  • 输入在哪里被使用?

  • 密文存储在哪里?

  • 轮密钥从哪里加载?

思路3:识别算法特征

某些指令序列是特定算法的明显标志:

  • aesenc/aesdec: AES加密

  • sha256msg1/sha256msg2: SHA-256

  • xor rax, rax; cpuid: 反调试

思路4:动静结合

静态分析和动态调试相互补充:

  • 静态分析:理解整体结构

  • 动态调试:观察实际行为

  • 交叉验证:确认假设

CTF解题经验总结

经验1:不要相信表象

这道题的main函数看起来非常简单,但真正的逻辑隐藏在析构函数中。在CTF中,简单往往意味着复杂。

经验2:关注ELF结构细节

很多高级漏洞利用技术都基于对文件格式的深入理解。学习ELF规范,了解每个字段的含义和加载过程。

经验3:善用工具,但不依赖工具

工具可以提高效率,但也可能被绕过:

  • IDA Pro可能不会反汇编.eh_frame

  • GDB在PIE程序上断点可能失效

  • 需要手工分析和验证

经验4:建立知识库

记录常见的攻击模式:

  • .init_array/.fini_array注入

  • GOT覆写

  • 返回导向编程(ROP)

  • 格式化字符串

  • ...

经验5:耐心和细心

逆向工程是一个迭代的过程:

  • 第一次看不懂很正常

  • 多次分析会有新发现

  • 记录每一步的发现

  • 构建完整的攻击链

实战建议

如果你在CTF比赛中遇到类似题目:

第1步:快速评估

  • 文件类型和保护机制

  • 程序基本行为

  • 是否有明显的漏洞点

第2步:寻找突破口

  • 异常的ELF结构

  • 可疑的字符串或常量

  • 特殊的指令序列

第3步:静态分析

  • 反汇编主要函数

  • 追踪数据流

  • 识别算法

第4步:动态验证

  • 设置断点观察

  • 修改内存测试

  • 确认攻击链

第5步:编写利用代码

  • 提取关键数据

  • 实现逆向算法

  • 验证结果


附录

附录A:完整分析命令

# 基本信息
file 家徒四壁
checksec --file=家徒四壁
readelf -h 家徒四壁

# 段和节信息
readelf -S 家徒四壁
readelf -l 家徒四壁
readelf -d 家徒四壁

# 重定位和符号
readelf -r 家徒四壁
readelf -s 家徒四壁

# 特殊字段
readelf -n 家徒四壁

# 反汇编
objdump -D -M intel 家徒四壁 > disasm.txt
objdump -s 家徒四壁 > hexdump.txt

# 数据提取
xxd -s 0xdd0 -l 16 家徒四壁
xxd -s 0x798 -l 176 家徒四壁

# 字符串搜索
strings 家徒四壁

附录B:ELF文件格式速查

ELF Header (64字节):

  • Magic: 7f 45 4c 46 (ELF标识)

  • Class: 64位/32位

  • Endian: 大端/小端

  • Entry Point: 程序入口地址

Program Header (描述段):

  • LOAD: 可加载段

  • DYNAMIC: 动态链接信息

  • INTERP: 解释器路径

Section Header (描述节):

  • .text: 代码

  • .data: 已初始化数据

  • .bss: 未初始化数据

  • .rodata: 只读数据

  • .init_array: 构造函数数组

  • .fini_array: 析构函数数组

  • .dynamic: 动态链接信息

  • .got/.plt: 全局偏移表/过程链接表

附录C:AES算法简介

AES-128结构:

  • 分组长度:128位(16字节)

  • 密钥长度:128位(16字节)

  • 轮数:10轮

轮函数:

  1. SubBytes: 字节替换(使用S盒)

  2. ShiftRows: 行移位

  3. MixColumns: 列混合(最后一轮无)

  4. AddRoundKey: 轮密钥加

密钥扩展:

  • 从初始密钥生成11个轮密钥

  • 总共:11 × 16 = 176字节

AES-NI指令:

  • aesenc xmm, xmm: 加密轮

  • aesenclast xmm, xmm: 最后加密轮

  • aesdec xmm, xmm: 解密轮

  • aesdeclast xmm, xmm: 最后解密轮

  • aesimc xmm, xmm: 逆向混合列

  • aeskeygenassist xmm, xmm, imm: 密钥扩展辅助

附录D:参考资料

ELF文件格式:

  • ELF Specification: https://refspecs.linuxfoundation.org/elf/elf.pdf

  • System V ABI: https://refspecs.linuxfoundation.org/elf/gabi4+/contents.html

AES算法:

  • FIPS 197: https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf

  • Intel AES-NI White Paper

安全机制:

  • RELRO: https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro

  • NX/DEP: https://en.wikipedia.org/wiki/Executable_space_protection

  • PIE/ASLR: https://en.wikipedia.org/wiki/Address_space_layout_randomization


结语

这道题目"家徒四壁"名副其实——看似空无一物的简单程序,实则暗藏玄机。通过分析这道题,我们学到了:

  1. ELF文件格式的深层细节:Section Header和Program Header的区别、重定位机制、段的加载过程

  2. 隐蔽的代码注入技术:利用合法的ELF结构隐藏恶意代码

  3. 反调试和反分析技术:代码压缩、动态解压、mprotect绕过NX

  4. 密码学的实战应用:AES加密算法在二进制保护中的使用

  5. 逆向工程的方法论:从表象到本质,从局部到整体,从静态到动态

CTF逆向题不仅是技术的竞赛,更是思维的较量。出题者精心设计的每一个细节,都值得我们深入研究和思考。希望本文能够帮助读者理解这些高级技术,在未来的安全研究和CTF比赛中取得更好的成绩。

最终Flag:hitcon{code_in_BuildID!}


免责声明:本文仅用于技术学习和研究目的。请勿将文中技术用于非法用途。


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