资讯动态

CTF逆向实战:UPX加壳ELF文件的识别、脱壳与Flag提取

发布时间:2026/10/21 6:45:46 来源:尧图企业网站定制
1. 拿到题目之后先别急着跑1.1 file命令一眼看穿文件身份CTF比赛里常见的套路是压缩包解压出来一个没有扩展名的文件名字就叫level2。很多新手第一反应是双击运行或者直接拖进IDA其实都不对。拿到文件的第一步永远是先做静态侦察用Kali Linux自带的file命令看它到底是什么。file level2正常无壳的ELF文件输出大概是这样的level2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]..., not stripped这一行信息量很大我逐个拆开说。ELF 64-bit这是一个64位的ELF可执行文件。后续选逆向工具时IDA/Ghidra要选64位模式gdb没有位数限制但地址都是64位的。LSB小端序。意思是最低有效字节在前读内存数据时不要反过来看。x86-64目标架构是Intel/AMD的64位指令集基于寄存器传参遵循System V AMD64 ABI后面分析调用约定时要知道参数是通过rdi、rsi这些寄存器传的。dynamically linked动态链接程序运行时依赖系统动态库。这不光是信息也意味着加壳时UPX可以把解压后的代码恢复原样脱壳后程序还能跑。not stripped符号表没有被剥离函数名、变量名都还在。这对逆向太友好了直接省去一大半猜测的功夫。另外我强烈建议在动任何配置之前先把原始文件做几份备份并记录哈希值。命令简单到不值得讨论但真出问题的时候能救命sha256sum level2 cp level2 level2.bakCTF里你面对的是个压缩包操作失误还能重新解压但如果是自己本地的一个上了壳的ELF还原成本就高了。先备份再动手是基本职业素养。1.2 跑一下看看程序在干什么静态信息掌握之后下一步是动态观察。先赋上执行权限然后在隔离环境里跑chmod x level2 ./level2这道题跑起来通常会有输入提示或者交互逻辑我用我这边模拟的一个类似题目举例程序打印一行Please input your flag:然后等待键盘输入。这种交互信息本身就是重要的线索——说明程序里有数据比较逻辑我们脱壳之后要找的很可能就是scanf/strcmp附近的代码。如果程序直接打印一串疑似flag的字符串那恭喜你这道题可能连脱壳都不用strings就能解决但这种情况在正规的Reverse题里很少见。大部分情况是程序假装跟你互动实际在内部做字符串比较或算法校验。动态观察的时候还可以带上ltrace和straceltrace ./level2 strace -f ./level2ltrace能跟踪库函数调用strace能跟踪系统调用。对于一个简单的C程序ltrace输出往往能直接暴露strcmp、printf、scanf这些函数的调用顺序。不过加壳程序在动态跟踪下表现会略有不同壳的解压过程会先于main执行所以跟踪结果会夹杂UPX运行时的一些系统调用没关系我们把壳脱掉之后再来看动态行为就清楚了。这里有个提示CTF比赛题目一般是安全的但虚拟机里跑永远是更稳妥的选择尤其是从网上下载的、环境不明确的二进制文件。Kali Linux开个快照再操作即使程序场景再诡异一个回滚全解决。1.3 用readelf和objdump观察结构file只能看到宏观信息想看内部结构就要动用readelf了。加壳前后的ELF在section layout上的差异非常明显。readelf -S level2没加壳的正常程序section列表会包含.text、.data、.bss、.rodata、.plt、.got这些。而如果看到类似下面的section名称那基本可以断定是UPX加壳[ 1] .UPX0 PROGBITS [ 2] .UPX1 PROGBITS [ 3] .UPX2 PROGBITSUPX0、UPX1这是UPX加壳后生成的特征section。其中.UPX0在文件里占据的空间通常是0字节但在内存映射中会预留很大的空间——因为UPX是把原始代码压缩存放在.UPX1里运行时再解压到.UPX0所以.UPX0就像一块临时展开区文件里是空的内存里才有内容。再用readelf -h看文件头部readelf -h level2关注Entry point address这一项。无壳程序入口点通常在.text段内部比如0x401050之类。UPX加壳后入口点会被改成壳代码的起始位置位于文件尾部比如0x4001c0附近。入口点位置异常是识别加壳的第二条线索。再配合objdump -h看段信息多一条证据链。到了这一步心里基本有数了这个文件被加壳了而且壳的特征很像UPX。2. 判壳是脱壳的第一步2.1 为什么UPX是CTF出题人的最爱壳的本质是什么打个比方原始可执行文件是一封写好的信壳是在外面套的一个信封信的内容被重新排列、压缩甚至加密过。程序运行时壳代码先接管CPU执行解压/解密逻辑把真正的原始代码恢复到内存中最后再跳到真正的入口点OEPOriginal Entry Point继续执行。UPXUltimate Packer for eXecutables是开源界使用最广泛的加壳工具。它同时支持Windows的PE、Linux的ELF、macOS的Mach-O等主流格式一条命令就能完成加壳还有对应的脱壳参数。CTF出题人喜欢用它做入门Reverse题原因也很简单UPX压缩率高加壳后文件体积减小模拟了程序变小了的实际场景。脱壳有标准工具upx -d对于新手来说是跳一跳够得着的难度。但出题人可以在UPX的壳上做手脚比如修改魔数、修改section头、换旧版本UPX让标准工具失效从而把题目难度提升到需要手动脱壳的层次。所以level2这种题目往往不是让你硬头皮逆向壳里的汇编而是考察你能否准确识别壳的类型、熟练运用脱壳手段、然后继续逆向真实的程序逻辑。壳只是一个考验你基本功的关卡不是最终目标。2.2 识别UPX壳的三种特征我用三种方式确认UPX壳逐一操作命中一个就足够引起警觉。第一种字符串特征。UPX加壳后的二进制里会保留一处很显眼的内容strings level2 | grep -i upx如果看到UPX!这三个字符那基本就是UPX了。UPX!是UPX的压缩文件标记类似ZIP文件开头的PK。第二种section特征。前面说的readelf -S如果出现.UPX0和.UPX1同样是一锤定音的证据。记住这两个名字比记什么花里胡哨的检测工具都实用。第三种入口点偏移特征。用readelf -h观察Entry point的位置再结合section表看看入口是否落在最后一个section区域内。UPX壳的入口点永远在它的解压代码里也就是靠近文件末尾的地方。而正常程序的入口点往往在.text段开头不远处。如果还想更精确可以在Kali里装一个Detect It Easy图形化拖入文件一点就出来壳的类型和特征值。不过命令行环境下strings加readelf已经足够。这里有一条注意事项魔改过的UPX会去掉UPX!字样甚至把.UPX1改成其他名字但入口点偏移和壳代码的汇编特征比如开头通常是pushad后面跟着mov esi、mov edi这种经典解压循环不会完全消失所以手动分析时眼睛要放亮。3. UPX脱壳全流程实操3.1 Kali下准备工具Kali Linux的软件源里自带UPX不过包名需要注意一下sudo apt update sudo apt install upx-ucl安装完验证版本upx --versionKali官方源里的UPX版本有时候不是最新的但这不影响常规脱壳操作。如果后续遇到旧版本UPX加壳的文件可能需要去UPX官网下载对应版本甚至更早的历史版本这部分我们后面再说。除了UPX建议把这几样工具也装好gdbKali自带。手动脱壳要靠它断点、单步、dump内存。pwntools用pip3 install pwntools安装它提供checksec脚本能快速查看程序保护机制也方便写exp。radare2Kali里自带或通过apt install radare2安装。作为命令行逆向工具它的动态调试能力在脱壳场景下很好用。Ghidra免费开源适合脱壳后的静态分析。不一定要装最新版但一定要配上Java环境。准备工作里我最想提醒的就是虚拟机快照。如果你和我一样只有一个Kali虚拟机做脱壳实验之前打一个快照后面反复折腾不会影响系统状态。CTF题目一般无害但释放所有ELF常驻内存这种事跑多了难免系统多出来一些奇怪的东西。3.2 标准解法upx -d一把梭对绝大多数UPX加壳的ELF文件标准脱壳命令就是这一条upx -d level2正常情况下输出类似Ultimate Packer for eXecutables Copyright (C) 1996-2024 UPX 4.2.4 Markus Laszlo https://upx.github.io File size Ratio Format Name -------------------- ------ ----------- ----------- 329752 - 141428 42.88% linux/amd64 level2 Unpacked 1 file.这个输出里的/是压缩率329752是原始文件大小141428是压缩后的文件大小。看到Unpacked 1 file就说明解压流程完整跑完了。脱壳之后再跑一遍file level2你会发现输出里多了not stripped相关的符号信息section列表也回到了正常的.text、.data结构。最直接的验证方式是重新运行程序./level2如果脱壳成功程序行为应该和加壳前完全一致输入输出都一样。如果脱壳后程序段错误或者行为怪异说明脱壳过程有问题大概率是UPX版本不匹配或者文件头被改过。脱壳成功的另外一个福利是可以直接用strings查flag了strings level2 | grep -E flag|ctf|{为什么脱壳前strings看不到flag脱壳后就能看到因为UPX把整个原始程序压缩了字符串常量也被压缩进壳的数据段里直接扫描原始文件当然扫不到。但程序运行时会解压还原脱壳后得到的文件就是还原后的完整程序字符串自然都恢复了。3.3 手动脱壳当一把梭失败时现实不会总按标准剧本走。有些题目会在UPX上做手脚比如修改UPX!标记、篡改section头的偏移量等导致upx -d直接报错upx: level2: NotPackedException: not packed by UPX或者说Corrupted、InvalidFormatException等等。这时候不要慌手动脱壳的思路可以统一解决这类问题。手动脱壳的核心逻辑壳程序运行起来后会自己把原始代码解压到内存然后跳到OEP。我们只要让壳正常启动、在它跳到OEP的瞬间把进程内存dump出来然后修复成一个完整ELF文件就完成脱壳了。这在理论上对所有自解密壳都有效。一个经典的手动脱壳方案是esp定律内存dump我用gdb演示一遍关键步骤。第一步查看文件入口点readelf -h level2 | grep Entry假设输出是Entry point address: 0x4001c0。第二步用gdb加载程序在入口点下断点gdb -q level2 break *0x4001c0 run程序会停在入口处。此时壳代码还没开始执行。输入以下命令观察当前指令和寄存器x/10i $pc info registersUPX的入口代码通常长这样pushad pushf mov esi, ... mov edi, ... ...这里的pushad会把所有通用寄存器压栈保存。esp定律的精髓是pushad之后栈顶存放的是壳代码执行完要返回的地址这个地址实际上指向OEP头附近。换句话说我们可以通过观察栈顶来间接定位OEP。第三步读取pushad之后栈顶的值x/8gx $rsp记下第一个8字节值比如0x401234然后在这个地址上下断点break *0x401234 continue当程序再次停到断点时意味着壳的解压流程已经完成准备进入真正的程序入口。此时$pc指向的就是OEP附近。这个技巧我实测下来对带pushad的简单壳非常管用。当然如果壳不用pushad就需要单步跟踪找跳转指令看它跳到哪个地址。第四步dump内存。在gdb里查看进程的内存映射info proc mappings会列出程序加载的各个段比如0x400000到0x402000是可执行的代码段后面可能还有数据段、堆栈段。我们需要把这些可执行和数据段都dump下来dump binary memory /tmp/level2_dump.bin 0x400000 0x500000dum p完成之后这个level2_dump.bin只是内存镜像还不是一个干净的ELF文件直接拿给IDA分析会提示文件头损坏。所以还要做最后一步修复ELF头。手动修复ELF头比较繁琐一个可行的办法是参考原始加壳文件的ELF头把其中e_entry改成OEP的地址用readelf -h查到的原有程序头信息重建section表。但说实话这个操作对新手不太友好所以我更推荐一个替代方案先用readelf -l查看加壳文件里的程序段Program Header手动计算出原始代码段的起始偏移和虚拟地址再用十六进制编辑器把dump出来的文件头补齐。过程有点费手但做一次你对ELF格式的理解会突飞猛进。如果不想手动修头可以试试另一个思路在gdb里直接让程序跑完壳的解压然后set $pc OEP继续调试。因为此时内存里已经有完整程序了我们已经可以静态分析逻辑、动态下断点了。至于要不要修复成文件取决于你想用IDA看还是用gdb调。CTF解题的话gdb调通拿到flag完全可行。手动脱壳第一次做确实抽象多折腾几次就顺了。我建议新人至少完整手动脱一次壳比看一百篇博客说upx -d一下就好要深刻得多。4. 脱壳后的逆向分析找到level2的flag4.1 把文件交给Ghidra/IDA脱壳完成后正式逆向才刚刚开始。把脱壳后的文件拖进Ghidra或IDA选择64位模式让它自动分析。Ghidra的自动分析时间久一点但免费好用IDA的F5伪代码更直观但需要你手上有能用的版本。打开后先看左侧的符号树。如果文件没被剥离符号直接双击main进入主函数。level2这类入门题的逻辑不会太复杂主函数通常就是所有秘密的汇聚点。如果没有main符号那么可以换个思路先找字符串。在IDA里按ShiftF12打开字符串窗口搜索flag、input、wrong、correct这类关键词。字符串所在的地址往往能被printf/puts/strcmp引用顺着交叉引用列表逆推就能定位到核心逻辑。Ghidra里对应的操作是在Window - Symbol Table里搜索字符串然后右键Show References看谁引用了它。4.2 从伪代码到逆向脚本讲解具体题目时有很多同学卡在找到main之后怎么拿到flag。这里我说一个典型的签到题逻辑原理是一样的undefined8 main(void) { char buf[48]; char key[] {0x66, 0x6c, 0x61, 0x67, 0x7b, 0x34, 0x23, ...}; printf(Please input your flag:); scanf(%32s, buf); for (int i 0; i 32; i) { if ((buf[i] ^ 0x33) ! key[i]) { puts(Wrong flag!); return 0; } } puts(Correct!); return 0; }这种结构在CTF入门Reverse里太常见了程序不直接strcmp你的输入和flag明文而是把flag的每个字节异或一个固定值存成key数组。你输入的内容也异或同一个值然后和key比较。为什么要这样因为如果直接strcmpstrings里就能扫到flag明文出题人觉得太简单。异或这种可逆变换既容易实现又不会让逆向难度高太多比较适合入门题。处理思路分三步。第一步把key数组完整提取出来。在IDA的伪代码视图里双击key跳转到数据段手动复制出一串十六进制值或者用r2的px命令导出。第二步写脚本逆推。因为异或是对称操作所以flag[i] key[i] ^ 0x33直接套公式key bytes([0x66, 0x6c, 0x61, 0x67, 0x7b, 0x34, 0x23]) flag bytes([c ^ 0x33 for c in key]) print(flag)把key换成实际提取的完整数组运行脚本就能得到flag。第三步验证。运行程序输入得到的flag观察到输出与预期一致。如果题目要求提交格式是flag{...}那字符串本身也应当符合这个格式校验一下就能确认脚本没写错。这里有坑要注意有些题目的比较不是逐字节异或而是先把输入做某个变换比如字符加减操作或者输入经base64之后才比较。所以拿到伪代码后先看清楚比较逻辑不要条件反射式地写异或脚本。核心方法是找到比较函数之前的那段数据变换逻辑做它的逆运算而不是套模板。4.3 验证flag与常见混淆手法拿到flag之后第一件事是在脱壳后的程序里验证而不是直接提交。虽然比赛平台会告诉你对错但本地验证能帮你快速发现是不是脚本提取的数组漏了几个字节。验证的方式很简单运行程序输入算出的flag看到代码里的成功分支输出就说明算法逆推没毛病。如果失败回头检查提取的key数组是否完整、异或值是否看错、循环条件到底是 32还是 strlen(...)。有些UPX脱壳题不会只考异或常见混淆手法还有这些移位加密(buf[i] 2) | (buf[i] 6)这种循环移位。加法/减法buf[i] i key[i]。查表替换用一个256字节的S盒做替换逆向时查反向表。多段拼接flag被拆成几段存到不同位置需要通过交叉引用拼接。遇到这些不要急逆向的核心是把数据变换过程反着写一遍。只要伪代码能看懂脚本就能写出来剩下的只是细心问题。5. 把坑填了常见问题与排查技巧5.1 常见问题速查表我把这几次实战里遇到的高频问题整理成一张表供你遇到报错时快速对照。现象可能原因排查与解决upx -d提示NotPackedException文件被魔改UPX魔数被篡改用readelf -S和strings确认是否真是UPX壳尝试不同UPX版本必要时手动脱壳upx -d提示Corrupted或解压失败加壳时的UPX版本与当前版本不兼容下载历史上的UPX版本逐一尝试常见有3.x和4.x两代脱壳后程序运行段错误dump范围不够ELF头修复不对使用info proc mappings完整dump可执行段和数据段参考原文件的程序头重建ELFstrings扫不到flagflag不是明文而是经过加密逆向main逻辑找异或/移位/查表变换写脚本逆推gdb断点下不到入口地址没加*或入口点看错gdb里断点地址必须是break *0x4001c0使用readelf -h重新确认Entry pointchecksec命令不存在pwntools未安装Kali下pip3 install pwntools或在/usr/share/相关工具目录下找程序在pushad处看不到预期汇编壳不是UPX而是其他壳检查入口处指令特征改用对应壳的脱壳方法动态跟踪ltrace看不到strcmp程序加壳后动态库调用发生在解压后先静态脱壳或手动脱壳再对脱壳后文件做动态分析这张表不一定覆盖所有情况但CTF入门题里80%的问题都能在这里找到影子。5.2 几个实战心得第一一切操作之前先备份。我见过不止一次有人直接upx -d把文件脱坏了然后又没留原文件最后只能重新下载压缩包。虽然CTF可以重新下但如果在真实分析场景可能就麻烦了。养成cp备份的习惯永远没错。第二upx -d报错不一定要死磕手动脱壳。先用strings level2 | grep -i upx确认是原版UPX然后尝试找匹配的UPX版本。UPX的兼容性问题很多时候通过换版本就能解决手动脱壳是最后的手段不是第一选择。第三脱壳成功不等于题目做完了。很多同学脱完壳长舒一口气觉得万事大吉结果在逆向逻辑阶段卡住了。恰恰相反脱壳只是开胃菜脱壳之后的那份反汇编才是真正考验逆向基本功的地方。建议花时间把main里的每一条伪代码都弄清楚而不是忙着拿脚本跑flag。第四善用交叉引用。定位关键字符串后右键查看交叉引用比纯线性阅读汇编快得多。入门阶段建议直接看伪代码进阶之后再试着理解汇编层面发生了什么。第五遇到魔改UPX检查readelf -S里是否还有.UPX0/.UPX1有时候出题人只改了魔数section名没清理干净。这种情况下可以直接用十六进制编辑器把UPX!改回去也许upx -d就能正常工作了。这个骚操作很值得记下。最后分享一个小技巧。脱壳后的文件如果不方便用GUI工具可以直接用grep -a从二进制里捞flaggrep -a -o flag{[^}]*} level2_unpacked-a把二进制当文本处理-o只输出匹配的部分。对于利好型的题目这条命令几秒钟就能出答案。但别忘了这只是捷径理解逆向过程的价值远大于捞到一个flag。我自己的体会是UPX脱壳看似是CTF里很小的一环但真的把它吃透你能同时理解ELF文件格式、程序加载过程、汇编入口点、动态调试这几个概念算是一箭多雕。level2这道题如果只靠upx -d一把梭通过了那以后遇到壳变种还是会卡住而手动脱一次壳、修复一次ELF头之后再见到什么壳都有底气去试试。这就叫基本功。下次如果遇到魔改UPX、旧版本UPX加壳或者混合壳的题目你可以把今天这套流程再套用一遍相信会比之前从容很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑