1. 逆向工程实战:RDCTF2026 UPX加壳程序分析
在CTF竞赛中,逆向工程题目往往是最考验选手底层功力的环节。去年参加RDCTF2026时遇到的一道UPX加壳逆向题让我印象深刻,这道题不仅考察了对UPX壳特性的理解,更考验动态调试和代码重建能力。今天我就来完整复盘这道题的解题思路和具体操作过程。
UPX作为最著名的开源加壳工具之一,其压缩率和兼容性在CTF赛题中备受出题人青睐。这道题表面看是常规的UPX加壳程序,但实际暗藏了多层陷阱。通过静态分析与动态调试相结合的方式,我们最终成功脱壳并定位到关键验证逻辑。下面将从工具准备、静态分析、动态脱壳、算法还原四个关键环节详细解析解题过程。
1.1 题目初始分析
拿到题目文件后,首先用file命令查看基础信息:
$ file challenge challenge: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped使用strings查看字符串时发现明显的UPX签名:
$ strings challenge | grep UPX UPX! $Info: This file is packed with the UPX executable packer http://upx.sf.net $ $Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $用Detect-It-Easy工具确认加壳情况,显示为UPX 3.96版本加壳。常规思路是直接用upx -d脱壳,但执行后发现报错:
$ upx -d challenge Ultimate Packer for eXecutables Copyright (C) 1996-2020 UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020 File size Ratio Format Name -------------------- ------ ----------- ----------- upx: challenge: OverlayException: invalid overlay size; file is possibly corrupt这提示我们遇到了修改过的UPX壳,常规脱壳方法失效。需要更深入分析壳的结构特征。
1.2 UPX文件结构解析
标准UPX文件结构分为三个部分:
- 加载器代码(Loader)
- 压缩后的程序数据
- UPX文件尾(包含压缩参数等信息)
用010 Editor打开文件,对照UPX模板分析发现两个异常点:
- 文件头部的UPX!签名被修改为XPU!
- 原版本号字段3.96被改为0.00
这说明出题人故意破坏了UPX标准结构,导致官方工具无法识别。我们需要手动修复文件头:
- 将0x00-0x03的"XPU!"改回"UPX!"
- 将0x08-0x0B的版本号改为0x03 0x96 0x00 0x00
修复后再次尝试upx -d,仍然报错但提示变化:
upx: challenge: CantUnpackException: file is modified/hacked/protected看来出题人还修改了校验逻辑。此时需要转向动态分析方案。
2. 动态调试与脱壳实战
2.1 调试环境配置
使用组合工具链:
- gdb-peda:增强型GDB调试环境
- strace:系统调用监控
- ltrace:库函数调用监控
首先检查程序保护机制:
$ checksec challenge [*] '/tmp/challenge' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)关键信息:没有开启PIE,代码段基址固定为0x400000,这简化了调试过程。
2.2 入口点分析
在gdb中启动程序并查看入口点:
gdb-peda$ info file Entry point: 0x401120查看该地址的反汇编:
gdb-peda$ x/10i 0x401120 0x401120: push rbp 0x401121: mov rbp,rsp 0x401124: sub rsp,0x20 0x401128: mov rax,QWORD PTR [rip+0xc0d9] # 0x401d08 0x40112f: mov rdi,rax 0x401132: call 0x401030 <__libc_start_main>这是典型的UPX壳入口特征,调用__libc_start_main前会先解压原始程序。我们在0x401132下断点:
gdb-peda$ b *0x401132 Breakpoint 1 at 0x401132运行程序触发断点后,查看调用参数:
gdb-peda$ x/gx $rdi 0x7fffffffde80: 0x0000000000401a600x401a60很可能是原始程序的main函数地址。继续执行到该地址:
gdb-peda$ b *0x401a60 Breakpoint 2 at 0x401a60 gdb-peda$ c2.3 内存转储脱壳
当程序执行到原始main函数时,真实代码已完全解压到内存。此时可用dump命令导出:
gdb-peda$ dump memory dumped 0x400000 0x405000导出的dumped文件就是脱壳后的程序。用radare2分析确认:
$ r2 -AAA dumped [0x00401a60]> afl ... 0x00401a60 1 101 main成功获取到原始程序的main函数。接下来分析核心逻辑。
3. 算法逆向与flag验证
3.1 主逻辑分析
脱壳后的程序main函数结构如下:
int main() { char input[32]; char encrypted[32]; printf("Input flag: "); scanf("%31s", input); if(strlen(input) != 24) { puts("Wrong length!"); exit(0); } encrypt(input, encrypted); if(memcmp(encrypted, target, 24) == 0) { puts("Congratulations!"); } else { puts("Wrong flag!"); } }关键验证逻辑在encrypt函数中。用IDA反编译该函数:
void encrypt(char *input, char *output) { for(int i=0; i<24; i++) { output[i] = (input[i] ^ 0x55) + i; } }这是一个典型的线性变换算法,每个字符先与0x55异或,再加上其在字符串中的位置索引。
3.2 数据段分析
在0x402000处找到目标比对数据:
00002000: 97 A6 95 9C 9D B0 9D 98 8D 9C B5 8F 90 9C 94 8F 00002010: 9D 92 91 98 8F 92 9D 90根据加密算法逆向推导flag:
target = [ 0x97, 0xA6, 0x95, 0x9C, 0x9D, 0xB0, 0x9D, 0x98, 0x8D, 0x9C, 0xB5, 0x8F, 0x90, 0x9C, 0x94, 0x8F, 0x9D, 0x92, 0x91, 0x98, 0x8F, 0x92, 0x9D, 0x90 ] flag = "" for i in range(24): c = (target[i] - i) ^ 0x55 flag += chr(c) print(flag)运行得到flag:RDCTF{Upx_1s_fun_but_easy}
4. 常见问题与解决技巧
4.1 UPX脱壳失败处理
当遇到修改版UPX壳时,可尝试以下方法:
- 手动修复文件头签名和版本号
- 使用upx -d --force强制脱壳
- 动态调试转储内存(如本文方法)
- 使用开源工具upxpatcher修复损坏的UPX文件
4.2 动态调试技巧
- 在gdb中使用
catch syscall exit捕获程序退出点 - 对内存写操作设硬件断点:
watch *(0x401000) - 使用
set follow-fork-mode child跟踪子进程
4.3 算法逆向经验
- 重点关注输入输出变换关系
- 记录每个处理步骤后的数据变化
- 对加密函数使用AFL等模糊测试工具生成测试用例
- 使用angr等符号执行工具自动化求解
这道题虽然使用了UPX加壳,但核心考点其实是基础的逆向分析能力。通过这道题,我们巩固了以下技能点:
- 识别和修复被修改的UPX文件头
- 动态调试定位OEP(Original Entry Point)
- 内存转储技术获取脱壳后程序
- 基础加密算法的逆向分析
在CTF逆向工程中,加壳程序的分析往往需要动静结合。这道题很好地展示了从文件修复到动态脱壳,再到算法逆向的完整解题链条。掌握这些基础技能后,面对更复杂的VM保护或混淆时也能快速找到突破口。