alx-sch/renderer-reverse-engineering

GitHub: alx-sch/renderer-reverse-engineering

通过Ghidra等工具逆向分析一个无文档的预编译C图形库,重构其内部数据结构与API调用约定,并编写出可运行的宿主程序。

Stars: 0 | Forks: 0

# 逆向工程一个 C 图形库


The renderer successfully initialized, displaying its embedded "You did it! :D" animation.

本项目是一个基于 C 的**逆向工程**案例研究。起点是一个单一且预编译的“黑盒”图形库(`librender`),它没有提供任何头文件、文档或源代码。目标是:分析该二进制文件并编写一个能够成功与其交互的宿主应用程序。 ## 🚀 快速开始 ### 前置条件 - 一个 C 编译器(`cc` / `gcc` / `clang`) - **macOS:** Cocoa 和 AudioToolbox 框架(随 Xcode 附带) - **Linux:** `libX11` 和 `libasound` 开发包 ### 构建与运行 ``` git clone https://github.com/alx-sch/reverse.git reverse cd reverse make ./run_renderer ``` Makefile 会自动检测您的操作系统,并链接相应的预编译库变体: | 平台 | 库文件 | 额外链接项 | |:-------------|:--------------------------|:----------------------------------------------| | macOS x86_64 | `librender_x86_64.dylib` | `-framework Cocoa -framework AudioToolbox` | | macOS ARM64 | `librender.dylib` | `-framework Cocoa -framework AudioToolbox` | | Linux x86_64 | `librender_x86_64.so` | `-lX11 -lasound` | | Linux ARM64 | `librender.so` | `-lX11 -lasound` | ## 🔍 挑战 在只有一个编译好的共享库且完全没有文档的情况下,目标是: - **发现 API** —— 从二进制文件中识别出所有导出的函数符号。 - **推导功能** —— 确定每个函数的作用、期望的参数以及返回的内容。 - **重构内部状态** —— 该库操作一个必须由调用者分配的不透明数据结构;逆向工程其确切的内存布局(大小、字段、偏移量)。 - **实现可用的宿主程序** —— 编写 `main.c` 以正确分配状态、初始化渲染器、驱动事件循环并处理清理工作。 最终成功的标志是:渲染器能够打开一个窗口并显示其内置动画。 ## 🧠 方法论 总体策略分为三个方面: 1. 有哪些函数?(API) 2. 库需要什么数据?(结构体) 3. 必须以什么顺序调用函数?(`main` 中的逻辑) ### 第一步 —— 符号发现 在 `.dylib` 上运行 `nm -g` 以枚举导出的符号: ``` [...] 0000000000001194 T _gfx_allocate_framebuffer 0000000000000d6c T _gfx_close 0000000000001124 T _gfx_create_context 0000000000001180 T _gfx_get_height_screen 0000000000001178 T _gfx_get_width_screen 0000000000001188 T _gfx_get_window_title 0000000000000770 T _gfx_init_context 0000000000000dac T _gfx_loop 00000000000011d4 T _gfx_render 0000000000001088 T _gfx_sleep 00000000000010e0 T _gfx_time [...] ``` `gfx` 前缀证实了这是一个图形库。仅凭函数名就揭示了清晰的生命周期模式:*创建 (create) → 初始化 (init) → 循环/渲染 (loop/render) → 关闭 (close)*。`gfx_get_*` 辅助函数表明,库本身知道所需的窗口尺寸和标题;宿主程序不需要自行编造,而是向库询问。 ### 第二步 —— 反编译与结构体重构 使用 Ghidra[2](免费!)将 `librender.dylib` 加载到新项目中。在自动分析之后,每个函数的反编译 C 代码逐步揭示了结构体的布局。 #### `gfx_create_context` —— 破解的关键 这个函数是信息量最大的起点,因为它是构造函数: ``` // Ghidra's decompiled C undefined8 *gfx_create_context(undefined8 *param_1, undefined4 param_2, undefined4 param_3, undefined8 param_4) { memset(param_1, 0, 0x430); *param_1 = param_4; *(undefined4 *)(param_1 + 1) = param_2; *(undefined4 *)((long)param_1 + 0xc) = param_3; return param_1; } ``` 关键推论: - **`memset(param_1, 0, 0x430)`** —— 结构体恰好为 0x430 = **1072 字节**。这是最重要的发现:调用者必须分配这个大小。 - Ghidra 的 `undefined8` 和 `undefined4` 分别是 8 字节和 4 字节类型,这意味着 `param_1` 是一个指针,而 `param_2` / `param_3` 是 `int`。 - **`param_4`** → 偏移量 0x00 处的 8 字节(后来被确认为窗口标题指针) - **`param_2`** → 偏移量 0x08 处的 4 字节(窗口宽度) - **`param_3`** → 偏移量 0x0C 处的 4 字节(窗口高度) #### `gfx_render` —— 帧缓冲区位置 ``` void *fb = *(void **)(param_1 + 0x10); // Frame buffer at offset 0x10 ``` 第四个字段:一个指向帧缓冲区的指针,必须由调用者单独分配(宽度 × 高度 × 4 字节,用于 RGBA 像素)。 #### `gfx_loop` —— 棘手的函数 反编译的原型抛出了一个难题: ``` undefined4 gfx_loop(undefined8 param_1, double param_2, long param_3) ``` 令人困惑之处: - `param_3` 被用作偏移基址(尽管它的类型是 `long`)? - `param_1` 根本没有被使用? - `param_2` 不知何故被用作鼠标 Y 坐标的偏移量? 研究表明,反编译永远不可能 100% 准确——这里可能发生了参数标记错误。这强烈暗示 `param_1` *实际上*就是状态指针,而 `param_3` 是反编译器产生的伪影,可以安全地忽略(传入 `0L`)。 尽管存在困惑,但函数体对于它向结构体*写入的内容*是非常清晰的。通过扫描所有 `gfx` 函数中出现的每一个 `param_1 + 0x...` 模式,揭示了剩余的字段: - **偏移量 0x18** —— 键盘状态数组的起始位置(1028 字节,涵盖按键按下的标志位) - **偏移量 0x41C** —— 鼠标 X 坐标 - **偏移量 0x420** —— 鼠标 Y 坐标 - **偏移量 0x424** —— 鼠标按键位掩码(按下时置位 bit 0,释放时清零) - **偏移量 0x428** —— 指向原生窗口对象的指针(macOS 上的 NSWindow) 事件处理字段是通过 Apple 的 AppKit 文档中关于 `NSEvent.EventType`[3] 和 `NSWindow.contentView`[4] 的内容识别出来的。由于这些字段不在外部使用,它们本可以建模为原始填充(raw padding)——但正确地映射它们是一种良好的实践。 #### 最终布局 ``` Offset Size Field ────── ────── ────────────────────────────── 0x000 8 char *title 0x008 4 int windowWidth 0x00C 4 int windowHeight 0x010 8 void *frameBuffer 0x018 1028 char keyBoardState[1028] 0x41C 4 int mouseX 0x420 4 int mouseY 0x424 4 int mouseButtonState 0x428 8 void *windowPtr ────── ────── ────────────────────────────── Total: 0x430 = 1072 bytes ✓ matches memset ``` 合理性检查:偏移量 0x428 + 8 字节 = 0x430,与 `memset` 的大小完全匹配。没有剩余的填充或隐藏字段。 ### 第三步 —— 宿主实现 在结构体和 API 被完全映射之后,编写 `main.c` 就变成了如何正确安排调用顺序的问题。 #### `gfx_allocate_framebuffer` 陷阱 一个值得注意的误导信息:库导出了 `gfx_allocate_framebuffer`,但反编译显示其 `malloc` 的返回值从未被存储或传递到任何地方。调用此函数将导致立即发生**内存泄漏**。帧缓冲区必须在 `main.c` 中手动分配,并在调用 `gfx_init_context()` 之前放置在偏移量 0x10 处。 #### 事件循环 ``` while (1) { gfx_loop(g_state, 0.0, 0L); // Process events (keyboard, mouse, window close) gfx_render(g_state, 1); // Render frame; 1 = animate (scrolling text) gfx_sleep(0); // nanosleep with 0 → fast as possible without hogging CPU } ``` - **`gfx_loop(state, 0.0, 0L)`** —— 保持窗口存活并处理“关闭窗口”事件。其他事件(键盘/鼠标)被捕获到结构体中,但不在外部使用。 - **`gfx_render(state, 1)`** —— 用深色清除帧缓冲区并在其中绘制文本。传入 `1`(或任何大于 0 的值)会使文本滚动;传入 `0` 将使其静态渲染。 - **`gfx_sleep(0)`** —— FPS 可以通过传入一个值来控制。传入 `0` 让动画尽可能快地运行,同时不让 `while(1)` 循环占用整个 CPU。这里可能存在某种内部的限流机制。 #### 清理问题 程序不是通过跳出循环退出的——而是在收到“关闭窗口”事件后直接从 `gfx_loop` *内部*退出的。这意味着在 `while(1)` 循环之后的任何代码都是不可达的。那么如何释放已分配的内存呢? 解决方案:`atexit()`。它就像一个钩子,在程序正常退出时运行一个已注册的函数。由于它的限制(注册的函数必须是 `void func(void)`,无参数),结构体指针被设为全局变量,以便 `cleanup()` 函数能够在关闭期间访问并释放它。 运行 `leaks` 工具证实:**没有内存泄漏**。 ## 📂 项目结构 ``` reverse/ ├── main.c Host application (the deliverable) ├── Makefile Cross-platform build system (macOS + Linux) ├── lib/ │ ├── librender.dylib macOS ARM64 │ ├── librender_x86_64.dylib macOS x86_64 │ ├── librender.so Linux ARM64 │ └── librender_x86_64.so Linux x86_64 └── .assets/ └── unlocked_animation.gif Success screenshot ``` ## 🛠 使用的工具 - **Ghidra** —— 共享库的反编译和反汇编。 - **nm** —— 导出符号枚举。 - **file** —— 识别库变体的架构。 - **otool / objdump** —— 动态依赖项和加载命令检查。 - **leaks** —— 在 macOS 上进行内存泄漏验证。 ## 参考文献 [1] National Security Agency (2019). *Ghidra: A Software Reverse Engineering Framework*. https://ghidra-sre.org
[2] Apple Inc. (2026). *NSEvent.EventType*. https://developer.apple.com/documentation/appkit/nsevent/eventtype
[3] Apple Inc. (2026). *NSWindow.contentView*. https://developer.apple.com/documentation/appkit/nswindow/contentview
标签:二进制分析, 云安全运维, 云资产清单, 动态链接库, 图形库, 客户端加密, 逆向工程