ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CTF逆向题实战:从签到到安卓,静态分析、动态调试与angr/frida拆解路径

CTF逆向题实战:从签到到安卓,静态分析、动态调试与angr/frida拆解路径 简介这份资源是面向CTF逆向工程学习者与参赛选手的实战题集收录第24至31关的逆向挑战适合已掌握汇编与调试基础、希望进阶提升的中高级选手。压缩包共352个文件约8.8MB以txt题面与说明、dll动态库、udd调试数据、exe可执行程序为主另含ini配置、chm手册、idb数据库及少量c源码与pcap抓包文件覆盖静态分析、动态调试、字符串混淆、栈帧分析、隐写术与内存取证等方向。已有2490人学习下载题目涉及IDA Pro反汇编、GDB跟踪、加密算法逆向与二进制格式解析等综合技能点可帮助读者在真实赛题环境中练习脚本自动化与解密程序构建逐步建立对软件安全性的系统认识。1. 几个 CTF 逆向题从签到到安卓一条能复现的拆题路径很多人第一次打 CTF 逆向卡住的不是不会写代码而是拿到一个二进制或者一个 APK不知道第一刀往哪切。签到题里塞个异或你盯着 IDA 的伪代码看半天安卓题里 Java 层干干净净flag 却藏在 so 里还有那种用 Flutter 或者游戏引擎打包的连入口函数都找不到。这篇笔记就围绕「几个 CTF 逆向题」这个场景把从静态分析到动态调试、从 x86 到 ARM、从 Java 到 Native 的常见拆法讲清楚。适合刚入门想系统过一遍题型的人也适合打了几年 Web 和 Misc、想补逆向这块短板的选手。下面按题目类型递进每类都给可复现的命令和参数坑也一并标出来。2. 先分清题型CTF 逆向题的四种常见形态与选型判断2.1 从文件特征快速判断该用哪套工具拿到题目附件先别急着拖进 IDA。用file和strings过一遍能省掉大量无效操作。Linux 下的 ELF、Windows 的 PE、安卓的 APK/DEX/SO、还有 Python 打包的 pyc 和 PyInstaller 产物处理路径完全不同。# 第一步看文件类型和架构 file ./challenge # 第二步抓可打印字符串找 flag 格式、提示、库名 strings -n 6 ./challenge | head -50 # 第三步如果是 ELF看动态链接和符号 readelf -d ./challenge nm -C ./challenge | grep -i mainfile输出里的ELF 64-bit LSB pie executable, x86-64告诉你这是 64 位 ELF用 IDA 64 或 Ghidra 打开。strings里如果出现pyinstaller、_MEIPASS说明是 Python 打包直接上pyinstxtractor解包比硬逆二进制快得多。readelf -d看它依赖哪些 so如果依赖一个陌生的自定义 so那核心逻辑大概率在里面。判断顺序我一般是这样先file定架构再strings找线索然后看有没有符号。 stripped 的二进制符号全没了就得靠交叉引用和字符串定位。这一步花两分钟能避免后面半小时的无效翻找。2.2 静态分析、动态调试、符号执行怎么选三种手段不是互斥的但入门阶段要有优先级。静态分析IDA/Ghidra负责理解整体逻辑动态调试gdb/lldb/x64dbg负责验证关键分支和抓运行时数据符号执行angr适合约束条件多、手动推太累的题。选型判断标准很直接如果程序逻辑是线性的、分支不多静态看完直接写逆运算脚本就行如果有大量if-else校验、每个字符独立判断用 angr 跑约束求解往往几分钟出结果如果程序有反调试、加壳、或者依赖运行时环境比如安卓的 JNI 调用那就必须动态调试。提示angr 不是万能药。程序里有大量系统调用、文件 IO、或者循环次数不确定时符号执行会爆炸。先静态确认校验逻辑是纯计算再上 angr。我见过太多新手一上来就 angr结果跑了半小时没结果回头静态一看其实是个简单异或。工具是加速器不是替代品。先看懂逻辑再决定用不用自动化。2.3 一个最小可复现的静态分析流程以一道典型的 Linux 逆向签到题为例走一遍完整流程。假设file显示是 64 位 ELFstrings里看到input your flag:和correct/wrong。# 用 objdump 快速看反汇编定位 main objdump -d -M intel ./challenge | grep -A 80 main: # 或者用 gdb 直接反汇编 main gdb -q ./challenge -ex set disassembly-flavor intel -ex disassemble main -ex quitobjdump -d -M intel指定 Intel 语法比 ATT 好读。grep -A 80 main:抓 main 函数附近 80 行。如果符号被 strip 了就找entry或者从__libc_start_main的参数里回溯 main 地址。看到反汇编后重点找cmp、test、jz、jnz这些指令它们对应校验分支。把校验逻辑翻译成伪代码再写成 Python 逆运算。这一步的核心是耐心别跳步。参数上-M intel是必须的-A后面的行数根据函数大小调一般 60 到 100 够用。3. 从签到题到异或校验手写逆运算脚本的完整步骤3.1 识别校验逻辑cmp、test 与循环结构签到题最常见的模式是读入字符串逐字符和一个固定值做异或或加减然后和硬编码的密文比较。反汇编里表现为一个循环循环体内有xor或add/sub循环外有cmp和条件跳转。; 典型异或校验循环的反汇编片段 mov eax, [rbp-0x4] ; i cdqe movzx eax, byte [rbprax-0x30] ; input[i] xor eax, 0x2a ; 和 0x2a 异或 mov [rbp-0x20], al ; 存临时结果看到xor eax, 0x2a这种立即数异或基本可以确定是逐字符变换。接下来找比较对象密文通常存在.data或.rodata段用objdump -s -j .rodata ./challenge把数据段 dump 出来。# dump .rodata 段内容 objdump -s -j .rodata ./challenge # 或者用 readelf 看段偏移再用 xxd 提取 readelf -S ./challenge | grep -A1 rodataobjdump -s -j .rodata直接以十六进制加 ASCII 显示段内容找那串看起来像密文的字节。如果密文是0x12, 0x34, ...这种记下来。参数上-j指定段名常见的有.rodata、.data、.data.rel.ro。3.2 用 Python 复现逆运算并验证拿到密文和变换规则后写逆运算脚本。异或的逆运算还是异或加减的逆运算就是反向加减。# 假设密文和变换规则已知 cipher [0x12, 0x34, 0x56, 0x78, 0x9a] # 从 .rodata dump 出来的密文 key 0x2a # 反汇编里看到的异或立即数 flag .join(chr(c ^ key) for c in cipher) print(flag) # 如果变换是加法cipher[i] input[i] key # 逆运算就是 input[i] cipher[i] - key flag2 .join(chr(c - key) for c in cipher) print(flag2)这段代码的关键是chr(c ^ key)把每个密文字节和 key 异或后转成字符。如果输出是可打印字符串且符合 flag 格式比如flag{...}就对了。参数上key必须和反汇编里看到的立即数一致别抄错。密文列表的长度要和循环次数对上。注意有些题会在异或后再做一次字节交换或者取反反汇编里表现为not或bswap。看到这些指令别漏掉逆运算要按相反顺序来。验证方法把算出的 flag 输入程序跑一遍看是否输出 correct。如果程序在本地跑不了比如是 ARM 架构用qemu-user模拟。# 用 qemu 跑 ARM 架构的 ELF qemu-arm ./challenge_arm # 输入算出的 flag看输出qemu-arm用于 32 位 ARMqemu-aarch64用于 64 位。如果程序依赖动态库加-L /usr/arm-linux-gnueabi指定库路径。这一步是最后的后悔药算错了能立刻发现。3.3 参数怎么调IDA 与 Ghidra 的实用设置IDA 和 Ghidra 是静态分析主力。IDA 打开后先ShiftF12看字符串窗口双击可疑字符串跳转到引用位置。Ghidra 则用Search - For Strings。两者都能反编译成伪代码但风格不同。IDA 的实用设置Options - General - Disassembly里勾选Auto comments能看到指令的注释。Options - Compiler选对编译器GCC/MSVC伪代码质量会好很多。Ghidra 的Decompiler窗口里右键变量可以Rename把local_18改成input可读性立刻提升。参数上IDA 的F5是反编译快捷键Ghidra 是CtrlE。如果伪代码里出现undefined4这种类型右键Retype改成char*或int。这些操作看着琐碎但能省下大量猜逻辑的时间。4. 安卓逆向题从 APK 到 so 层的拆解路径4.1 APK 解包与 Java 层快速定位安卓题的第一步是解包。apktool解资源jadx反编译 dex 看 Java 代码。# 解包 APK apktool d challenge.apk -o challenge_decoded # 用 jadx 反编译 dex 到 Java jadx -d challenge_jadx challenge.apk # 或者只导出源码 jadx --export-gradle -d challenge_jadx challenge.apkapktool d解出AndroidManifest.xml和smali目录jadx -d直接出 Java 源码。先看AndroidManifest.xml里的MainActivity那是入口。然后在 jadx 里搜flag、check、verify这些关键词。Java 层的校验通常长这样if (input.equals(decrypt(cipher)))或者if (checkFlag(input))。找到后看decrypt或checkFlag的实现。如果实现里出现native关键字说明核心逻辑在 so 里得进 Native 层。// jadx 里看到的典型 Java 层校验 public static native boolean checkFlag(String str); // 或者 public boolean verify(String input) { return input.equals(new String(Base64.decode(...))); }native方法对应 so 里的 JNI 函数命名规则是Java_包名_类名_方法名。用strings在 so 里搜这个符号或者用 IDA 打开 so 直接找。4.2 Native 层 so 的 JNI 函数定位与调试so 文件通常在lib/目录下按架构分armeabi-v7a、arm64-v8a、x86。优先看arm64-v8a因为现在大部分设备是这个架构。# 找 so 文件 find challenge_decoded -name *.so # 看 so 的导出符号 readelf -s libnative.so | grep -i java # 用 strings 找 JNI 函数名 strings libnative.so | grep -i Java_readelf -s列出符号表grep -i java过滤出 JNI 相关符号。strings找Java_开头的字符串那是 JNI 函数注册名。找到后用 IDA 打开 so跳到对应地址看逻辑。动态调试 so 需要 root 设备或者模拟器。常用做法是用fridahook JNI 函数打印参数和返回值。// frida hook JNI 函数的脚本 Java.perform(function() { var MainActivity Java.use(com.example.challenge.MainActivity); MainActivity.checkFlag.implementation function(input) { console.log(input: input); var result this.checkFlag(input); console.log(result: result); return result; }; });Java.perform确保在 Java 虚拟机加载后执行。Java.use拿到类引用.implementation重写方法。console.log打印输入和输出。参数上类名和方法名要和 jadx 里看到的一致。跑 frida 需要设备端有frida-serverPC 端frida -U -f 包名 -l script.js。提示如果 so 有反调试frida 可能附加失败。先frida -U -f 包名看能不能 attach不行就换frida-gadget注入或者用magisk隐藏 root。4.3 常见安卓题坑签名校验与反调试安卓题最常见的坑是签名校验。程序启动时检查 APK 签名不对就退出。绕过方法是 hookgetPackageInfo的signatures字段或者直接 patch smali 跳过校验。// frida 绕过签名校验 var PackageManager Java.use(android.app.ApplicationPackageManager); PackageManager.getPackageInfo.overload(java.lang.String, int).implementation function(pkg, flags) { if (flags 0x40) { // GET_SIGNATURES console.log(bypass signature check); flags flags ~0x40; // 去掉签名标志 } return this.getPackageInfo(pkg, flags); };0x40是GET_SIGNATURES标志位去掉后返回的PackageInfo里signatures为 null校验就过了。参数上overload要匹配原方法签名java.lang.String和int是参数类型。反调试的常见手段是检测frida-server进程或者ptrace。绕过方法是改端口、改进程名或者用frida-server的--realm参数。这些操作在入门阶段遇到再查不用提前背。5. 避坑与排查逆向题里最容易翻车的五个点5.1 现象反汇编里找不到 main 函数原因二进制被 strip 了符号或者入口不是标准main。ELF 的入口是_start它调用__libc_start_main真正的main地址作为参数传进去。解决在_start里找lea rdi, [ripxxx]或者mov rdi, offset main那个地址就是 main。或者用 gdb 在__libc_start_main下断点看第一个参数。gdb -q ./challenge -ex break __libc_start_main -ex run -ex info registers rdi -ex quitrdi里存的就是 main 地址。拿到地址后在 IDA 里跳过去按F5反编译。5.2 现象angr 跑了几十分钟没结果原因程序里有大量循环、系统调用或者文件 IO符号执行状态爆炸。或者校验逻辑依赖运行时环境angr 模拟不了。解决先静态确认校验逻辑是纯计算。如果是缩小 angr 的探索范围用find和avoid指定地址。如果还是慢换手动逆运算。import angr proj angr.Project(./challenge, auto_load_libsFalse) state proj.factory.entry_state() simgr proj.factory.simulation_manager(state) simgr.explore(find0x400000 0x1234, avoid0x400000 0x5678) print(simgr.found[0].posix.dumps(0))find是正确分支地址avoid是错误分支地址。auto_load_libsFalse避免加载系统库拖慢速度。地址用基址 偏移的形式PIE 程序基址通常是 0x400000。5.3 现象安卓 so 里 JNI 函数名对不上原因JNI 函数可能用RegisterNatives动态注册而不是静态命名。静态命名是Java_包名_类名_方法名动态注册在JNI_OnLoad里用函数表绑定。解决找JNI_OnLoad函数看它调用的RegisterNatives参数。或者用 frida hookRegisterNatives打印绑定关系。// hook RegisterNatives 看动态注册 var art Process.getModuleByName(libart.so); var RegisterNatives art.getExportByName(_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi); Interceptor.attach(RegisterNatives, { onEnter: function(args) { var methods args[3]; var count args[4].toInt32(); for (var i 0; i count; i) { var name methods.add(i * 24).readPointer().readCString(); var sig methods.add(i * 24 8).readPointer().readCString(); var fnPtr methods.add(i * 24 16).readPointer(); console.log(name sig - fnPtr); } } });args[3]是JNINativeMethod数组每个元素 24 字节name 指针 8 字节 sig 指针 8 字节 fnPtr 8 字节。打印出来就知道哪个函数绑到哪个地址。5.4 现象算出的 flag 输入程序还是 wrong原因可能有多个校验阶段或者输入经过了预处理比如去空格、转大写。也可能是密文提取错了或者变换顺序搞反了。解决在比较指令前下断点看实际参与比较的两个值。gdb 里break *0x地址然后x/s $rdi和x/s $rsi看字符串。gdb -q ./challenge -ex break *0x400800 -ex run -ex x/s $rdi -ex x/s $rsi -ex quitx/s以字符串形式打印寄存器指向的内存。对比两个值就知道是输入错了还是密文错了。如果是预处理问题在反汇编里找tolower、toupper、strlen这些调用。5.5 现象Flutter 或游戏引擎打包的题找不到入口原因Flutter 的 Dart 代码编译成libapp.so游戏引擎Unity/Unreal的代码在libil2cpp.so或libUE4.so都不是标准 ELF 入口。解决Flutter 题用flutter逆向工具箱里的blutter工具能还原 Dart 符号和部分逻辑。Unity 题用Il2CppDumper导出符号再用 IDA 分析。这些工具的使用超出入门范围遇到再查。注意这类题在 CTF 里属于进阶新手先跳过把标准 ELF 和安卓 Javaso 的题吃透。6. 进阶技巧用 angr 和 frida 组合拳快速拆多分支校验题多分支校验题是逆向里比较烦的一类每个字符独立判断分支几十上百个手动推逻辑容易出错。这时候 angr 的约束求解能力就体现出来了。但 angr 有个前提得知道正确分支和错误分支的地址。先用静态分析找到校验函数的地址范围。在 IDA 里看伪代码找到if (check(input[i]))这种结构记下正确分支的地址通常是继续循环的地址和错误分支的地址通常是打印 wrong 的地址。import angr import claripy proj angr.Project(./challenge, auto_load_libsFalse) # 假设 flag 长度已知比如 32 flag_len 32 flag_chars [claripy.BVS(fc{i}, 8) for i in range(flag_len)] flag claripy.Concat(*flag_chars) state proj.factory.entry_state(stdinangr.SimFileStream) # 把符号变量塞进 stdin state.posix.stdin.content flag # 或者直接作为参数传递看程序怎么读输入 simgr proj.factory.simulation_manager(state) simgr.explore(find0x400000 0x1200, avoid0x400000 0x1300) if simgr.found: found simgr.found[0] result found.solver.eval(flag, cast_tobytes) print(result)claripy.BVS创建符号变量claripy.Concat把它们拼成一个长向量。state.posix.stdin.content把符号变量作为标准输入。find和avoid指定正确和错误分支地址。found.solver.eval求解出具体值。参数上flag_len要准确太长会爆炸太短解不出。find和avoid的地址用基址 偏移PIE 程序基址 0x400000。如果程序有多个校验阶段可以分段求解每段 8 个字符。angr 跑完后用 frida 在真机或模拟器上验证。把解出的 flag 输入程序看是否通过。如果 angr 解出的值有多个约束不够加更多avoid地址缩小范围。// frida 验证 angr 解出的 flag Java.perform(function() { var MainActivity Java.use(com.example.challenge.MainActivity); var flag flag{angr_solved_this}; console.log(result: MainActivity.checkFlag(flag)); });Java.use拿到类直接调用checkFlag看返回值。如果返回 true说明 angr 解对了。这一步是最后的验证别跳过。我自己的习惯是angr 解出候选值后一定用 frida 或本地跑一遍确认。有次 angr 解出一个看着像 flag 的字符串结果程序里还有一层 Base64 解码白高兴一场。从那以后验证成了固定动作。这套组合拳的价值在于静态分析负责定位关键地址angr 负责暴力求解frida 负责运行时验证。三者配合多分支校验题从几小时缩短到几十分钟。但前提是静态分析要准地址找错了 angr 跑再久也没用。希望帮到你。本文还有配套的精品资源点击获取
返回列表