
1. 从一道CTF堆题看IoT安全研究的逆向起点最近在复盘一些经典的CTF题目特别是IoT方向的发现“2022春秋杯”的这道chunzhiIot是个很好的切入点。题目本身是个典型的堆利用题环境是glibc 2.33这本身就很有意思。很多人一听到IoT安全可能第一反应是硬件、固件、无线电这些但事实上大量的IoT设备漏洞挖掘和利用其起点和终点往往都在软件层面尤其是在用户态的程序分析上。这道题虽然以“IoT”为名但核心考察的是对Linux环境下二进制程序尤其是堆管理器的逆向分析和漏洞利用能力。这正是IoT安全研究中从“拿到一个陌生二进制文件”到“理解其逻辑并找到突破口”的必经之路。今天我们就以这道题为引子深入聊聊如何运用IDA和GDB这对黄金组合来拆解一个复杂的堆漏洞利用场景并从中提炼出对IoT固件分析有普适价值的方法论。2. 初探chunzhiIot程序逻辑与漏洞定位拿到一个陌生的二进制文件第一步永远是尝试运行和理解它的基本行为。对于chunzhiIot我们首先需要搭建一个与题目描述匹配的环境。题目提示glibc 2.33这是一个关键信息。不同版本的glibc其堆管理器的实现细节如tcache、fastbin的机制以及一些安全缓解措施如safe-linking有所不同。因此最好使用Docker或虚拟机创建一个包含glibc 2.33的Ubuntu 20.04或21.04环境进行调试避免因环境差异导致的分析偏差。用file命令查看文件类型用checksec查看保护机制这是标准流程。通常这类CTF题目的保护是全开的Canary, NX, PIE, RELRO。面对PIE位置无关可执行文件我们在静态分析时看到的地址都是偏移量这需要我们在动态调试时进行转换。接下来就是祭出我们的主力静态分析工具——IDA Pro。将二进制文件拖入IDA等待反编译完成。我们的首要目标是理解程序的整体逻辑。寻找主函数与菜单逻辑在IDA的Exports窗口我们可以按Name排序快速找到像main、start、libc_start_main这样的关键函数。通常main函数是分析的起点。进入main函数后IDA的图形视图能让我们快速把握程序结构一个典型的循环菜单提供诸如add、delete、edit、show等选项这是堆题的标准配置。关键数据结构分析我们需要在反编译的代码中识别出程序用于管理“对象”可能是设备、消息、用户等具体看题目设定的数据结构。通常是一个全局数组或链表。我们需要确定每个“对象”所占内存的大小、其中包含哪些字段如指针、大小、状态标志等。这一步至关重要因为后续的漏洞利用往往依赖于对数据结构布局的精确理解。漏洞点挖掘对于堆题漏洞类型通常集中在use-after-freeUAF、double free、heap overflow等。我们需要仔细审查delete释放和edit编辑功能。delete函数是否在释放内存后没有将对应的指针置空NULL留下了“悬垂指针”edit函数在编辑内容时是否没有检查输入长度导致了堆溢出或者是否允许对已经free掉的块进行编辑典型的UAFadd函数分配的大小是否可控是否存在整数溢出导致分配异常小的块从而为后续利用创造条件在chunzhiIot中经过初步分析很可能会发现一个经典的UAF漏洞程序在释放某个对象后没有清空指向该对象数据的指针并且后续的edit或show功能仍然可以使用这个“悬垂指针”来读写已释放的内存。这就是我们攻击的入口。3. 动态调试的艺术GDB实战命令与堆布局探查静态分析给了我们蓝图但真正的战斗发生在运行时。GDB是我们窥探程序运行时状态的显微镜。这里分享一些在分析此类堆漏洞时极其常用的GDB命令和技巧远不止于简单的run和break。基础准备与插件 首先使用gdb -q ./chunzhiIot以安静模式启动。强烈建议使用pwndbg或gef这类增强型GDB插件它们能可视化地显示堆块状态、内存布局、寄存器信息等效率提升十倍不止。以pwndbg为例安装后其命令如heap、bins、vis等将成为你的利器。关键调试流程与命令下断点与运行# 在main函数下断点 b main # 运行程序 r # 或者在某个关键函数如添加、删除、编辑的函数上下断点 b *add_item b *delete_item b *edit_item控制程序执行与观察堆c或continue: 继续执行直到下一个断点。n或next: 单步执行跳过函数调用。s或step: 单步执行进入函数调用。finish: 执行完当前函数并返回。当程序执行到分配或释放堆内存的指令通常是call malloc/call free之后立即使用pwndbg的heap命令查看堆状态。heap # 显示所有堆块的简要信息 bins # 详细显示所有bintcache, fastbin, unsorted bin等的内容 vis # 以更可视化的方式查看堆块精确观察内存与变量 假设我们通过逆向知道了管理对象的全局数组item_list的地址可能是0x5555555580a0这样的地址注意这是PIE开启时的运行时地址需要计算偏移。# 查看从item_list地址开始的内存假设每个条目是16字节8字节指针8字节大小 x/10gx 0x5555555580a0这条命令会以16进制格式x显示10个108字节g的数据从地址0x5555555580a0开始。你可以看到每个条目存储的指针和大小值。打印变量值如果调试信息完整可以直接p item_list[0]。但通常CTF题目strip掉了调试信息所以直接看内存更可靠。查看特定堆块内容如果你知道一个堆块的地址是0x5555555592a0想看看里面存了什么数据比如用户输入的字符串x/s 0x5555555592a0 # 以字符串格式查看 x/20wx 0x5555555592a0 # 以16进制字4字节查看20个一个实战技巧不中断地运行到指定行并打印。 题目给出的热词中有一个非常具体的问题“linux gdb如何运行到test.c的123行打印变量ab而不中断”。这反映了在复杂逻辑中定点观察的需求。在逆向工程中我们没有源代码行号但我们可以用地址替代。方法一临时断点tbreak# 假设我们想运行到地址0x555555555234可能是edit函数的某个关键判断处打印完信息后继续运行 tbreak *0x555555555234 commands x/gx $rdi # 打印rdi寄存器的值可能是第一个参数如对象指针 x/s ($rdi8) # 假设对象结构是{ptr, size}打印ptr指向的字符串 c # 继续执行 end rtbreak设置一个一次性断点触发后自动删除。commands定义了断点触发后自动执行的一系列命令。这样当程序执行到0x555555555234时会自动打印我们关心的信息然后继续运行不会中断我们的操作流。这对于在循环中观察某条指令每次执行时的状态特别有用。方法二使用advance命令如果GDB版本支持# 直接让程序运行到指定地址然后停在那里 advance *0x555555555234 # 然后手动打印变量在分析chunzhiIot的菜单循环时我们可以用tbreak在每次执行edit函数前自动打印出目标堆块的内容和其所在bin的状态从而清晰地追踪UAF漏洞触发前后堆布局的变化。处理GDB警告 热词中提到了一个警告”warning: gdb: failed to set controlling terminal: \344\270\215\345\205\201″。这通常是权限问题或环境问题。在调试时可以尝试在tmux或screen会话中启动GDB。使用gdb -tty /dev/pts/X ./chunzhiIot指定终端X是你的终端号用tty命令查看。或者最简单地使用pwntools的gdb.attach(p)来附加调试通常能避免这类终端控制问题。通过动态调试我们可以验证静态分析发现的漏洞点是否真实存在并精确观察漏洞触发时堆块是如何进入tcache或unsorted bin的指针是如何残留的。这是构思利用链的基础。4. Glibc 2.33下的堆利用策略绕过Safe-Linkingglibc 2.33引入了一个重要的安全机制——safe-linking。它对tcache和fastbin中的fd前向指针进行了异或加密。指针不再直接存储在fd位置而是存储fd ^ (address_of_the_chunk 12)。这增加了利用tcache poisoning等技术的难度。在GDB中当我们用heap或bins命令查看时pwndbg等插件通常会帮我们自动解密显示真实的fd指针。但我们在构造利用payload时必须手动进行这个加密计算。利用思路的核心转变 在safe-linking下传统的直接将目标地址写入fd的方法行不通了。假设我们通过UAF能修改一个已释放tcache块的fd指针我们想将其指向我们控制的假块例如在.bss段伪造的块。计算加密后的值假块的目标地址是fake_chunk_addr。这个tcache块自身的地址是chunk_addr。那么我们需要写入fd的值是encrypted_fd fake_chunk_addr ^ (chunk_addr 12)注意这里chunk_addr是堆块本身的地址即mem指针减去0x10的位置因为chunk头包含prev_size和size而不是用户数据区的地址。信息泄露是前提要完成上述计算我们必须知道chunk_addr。这通常要求我们能够泄露堆地址。在glibc 2.33及以后堆利用几乎总是以信息泄露开场。常见的泄露方法利用UAF的show功能如果存在UAF且能show已释放块的内容当该块被放入unsorted bin或large bin时其fd和bk指针会指向main_arena内部的地址。通过计算偏移可以泄露libc基址。如果块在tcache中其fd可能指向另一个堆块通过多次操作可以泄露堆布局。利用stdout/stdin/stderr结构体如果存在任意写或特定条件的溢出可以修改FILE结构体进行泄露但这通常更复杂。对于chunzhiIot利用链很可能遵循以下步骤步骤一布局堆与触发UAF。通过多次add和delete精心安排堆块进入tcache或unsorted bin并确保留下一个悬垂指针指向某个已释放块。步骤二泄露libc地址。利用UAF的show功能打印出某个进入unsorted bin的块的内容从中解析出main_arena的地址进而计算出libc的基址。步骤三泄露堆地址。同样利用UAF或其它漏洞泄露一个堆块的地址。例如让两个tcache块形成链表通过show第一个块可以读出第二个块的地址注意读出的值是加密的需要根据已知的堆布局进行推算或验证。步骤四计算并实施tcache poisoning。现在我们有libc基址可以计算__free_hook或system地址和堆地址。我们通过UAF修改某个tcache块的fd指针将其加密值指向我们想要的位置例如__free_hook。这里的目标地址是__free_hook我们需要知道当前tcache块的地址chunk_addr然后计算encrypted_fd __free_hook ^ (chunk_addr 12)并将这个值写入fd。步骤五分配并覆盖。后续两次malloc第一次会返回我们原来的块第二次就会返回到我们fd指向的__free_hook处。此时我们向这个块写入数据实际上就是向__free_hook写入数据例如system函数的地址。步骤六触发执行。最后释放一个内容为/bin/sh字符串的块free函数会调用__free_hook此时__free_hook已被我们覆盖为system于是就会执行system(“/bin/sh”)拿到shell。整个过程的关键在于第3、4步的信息泄露和加密fd的计算。在动态调试中你需要反复验证每一步之后堆的状态、指针的值是否与你的预期一致。5. 从CTF到真实IoT逆向分析工具的通用方法论虽然chunzhiIot是一道CTF题目但它所训练的逆向分析、漏洞定位、动态调试和利用构造能力与真实的IoT设备漏洞挖掘一脉相承。很多IoT设备的固件当你解包后会发现里面运行着一个或多个ARM/MIPS架构的二进制程序它们可能是一个网络服务、一个配置接口、甚至是一个简单的命令行工具。攻击面往往就隐藏在这些程序里。真实场景下的工具链调整架构适配IoT设备CPU架构多样ARM, MIPS, PowerPC等。你的IDA需要加载对应的处理器模块GDB也需要使用gdb-multiarch或对应架构的交叉编译版gdb如arm-linux-gnueabi-gdb。远程调试你通常无法在设备上直接运行GDB。需要在固件中寻找或交叉编译一个gdbserver放到设备上运行。在设备上启动待调试程序gdbserver :1234 ./vulnerable_binary。在你的主机上使用交叉编译版的GDB连接target remote 设备IP:1234。静态分析增强对于 stripped剥离符号的二进制IDA的Exports窗口里可能只有_start、main如果没strip掉等少数函数。你需要大量使用Strings窗口查找可能的提示信息。识别常见的库函数如strcpy,printf,malloc,free通过它们的参数传递约定如ARM的R0-R3寄存器来推断周围代码的逻辑。使用Find Crypt等IDA插件识别加密算法常数。模拟环境对于复杂的交互或没有实体设备的情况可以使用QEMU用户态模拟来运行二进制文件并结合QEMU的gdbstub进行调试。命令类似qemu-arm -g 1234 ./arm_binary然后在主机GDB中target remote :1234。分析思路的共性 无论目标是x86的CTF题目还是ARM的IoT守护进程核心分析思路不变输入/输出追踪程序从哪里接收数据网络socket、文件、串口数据经过哪些处理函数最终输出到哪里危险函数审计在反编译代码中搜索strcpy,sprintf,gets,scanf,memcpy等不安全的函数检查其长度参数是否可控。内存管理审计查找malloc/free/realloc的调用点分析其大小参数来源、释放后指针是否置空、是否存在重复释放。逻辑漏洞挖掘检查身份验证、权限检查的逻辑是否有绕过的可能。这在IoT设备中非常常见比如硬编码密码、后门账户、逻辑绕过等。回到chunzhiIot它训练了你对堆内存管理机制的深刻理解。在真实的IoT二进制中你可能遇到自定义的内存池、简单的内存分配器或者是更老版本glibc的堆但“分配-使用-释放”的基本模式和安全问题UAF、溢出的本质是相通的。熟练使用IDA进行静态逆向结合GDB进行动态验证和利用开发是通往二进制安全研究包括IoT安全的基石。我个人在分析这类题目和真实漏洞时习惯在IDA中详细注释反编译代码给函数、变量重命名并用不同的颜色高亮关键的数据流和控制流。在GDB调试时则倾向于写一些简单的Python脚本利用pwntools或gef的API来自动化一些重复的堆布局操作和状态检查这能极大提高效率尤其是在需要反复尝试不同偏移和布局的利用链构造阶段。记住工具是死的思路是活的将CTF中磨练的技巧灵活应用到更广阔的二进制安全领域才是学习的最终目的。