我最近在玩 Google 新发布的 Gemma E4B 开放权重本地模型,令我惊讶的是,它在本地离线逆向工程场景中取得了很大的成功。我想写这个教程,告诉大家本地 AI 现在已经足以胜任许多基础的逆向任务,并且情况很可能会从现在开始迅速改善。
逆向一个新二进制文件时,最繁琐的部分往往在最开始——你对重要的函数和变量毫无头绪。逆向工程师有许多入门技巧,包括查看字符串引用、二进制差异比较,或者匹配相似函数。
AI 在这方面非常擅长,我个人在使用 OpenAI API 来标记二进制文件或清理反编译器输出方面取得了很好的成效。然而,使用这些 API 存在几个缺点:
成本 — 反编译和反汇编会产生大量 token。API 按 token 收费,因此较大的二进制文件分析起来可能花费不少。如果你处理的是每周都会更新的大型目标,包含众多二进制文件,这些成本会迅速累积,对业余逆向工程师来说难以承受。
隐私 — 使用远程 API 时,AI 的主机方能够洞察你在做什么。这在某些专业场景中是不可接受的。
控制权 — 依赖远程 API 时,你无法控制提供的是哪些模型,也无法控制模型的质量。如果你在关键任务中依赖它们,当它们变慢或宕机而你又急需时,或者输出质量下降到无法使用的地步,就会成为问题。
运行自己的本地 AI 模型可以解决其中一些问题:
成本 — 运行本地模型通常比依赖托管服务便宜得多。虽然本地模型可能比优质服务商提供的模型更小、更慢,但如果它足够好且运行时间合理,那么为了省钱而运行自己的模型是明智的,尤其是在处理大量数据执行简单任务时。
隐私 — 在本地运行模型时,不会发生网络调用,你完全掌控自己的隐私。没有人能看到你在自己的机器上用模型做什么。
控制权 — 开放权重模型的好处在于,没有人能把它从你手中夺走。OpenAI 或 Anthropic 有一天可能会使它们的 SotA 模型不可用,要么通过涨价,要么直接移除 API。但有了开放权重模型,无论如何,你都能掌控自己的命运。
不过,本地 AI 模型也有其缺点:
规模 — 模型越大,越智能。然而,大多数大型模型无法放入消费级硬件。因此,如果你使用本地模型,很可能运行的模型比 SotA(State-of-the-Art,顶尖)模型小 10 到 100 倍。这种规模的缩减直接导致模型智能度下降,使其不适合许多在 SotA 模型(如 ChatGPT/Codex 或 Claude)中习以为常的任务。
速度 — 本地模型在你的机器上运行速度可能比使用 AI API 时慢。同样,这是由于消费级硬件的限制以及 API 提供商能够使用的一些技巧通常对你不可用。
配置 — 运行本地模型就像试图在一台翻新笔记本上安装 Linux,而不是走进 Apple 商店买一台全新的 Macbook Air。Codex 和 Claude Code 的体验是 AI 的 Apple 商店体验。本地 AI 模型的体验则像是一个戴着软呢帽的家伙在车库里捣鼓一台不稳定的电脑,试图让它正常工作。至少,你需要考虑以下事项:
这并非易事,许多人因此放弃,认为本地 AI 模型不堪重任,因为他们从未找到合适的硬件/模型/设置/提示/集成框架组合来让它们完成自己的任务。虽然在很多情况下他们是对的,但我希望这篇教程至少能揭示本地模型已取得多大进展,它们如何帮助逆向工程,并激励人们尝试本地 AI。
(存档密码是 crackmes.one)。我在本仓库中提供了一个备用链接,以防原始链接失效。
使用 Ghidra 12.04 进行反汇编和反编译。你需要安装 OpenJDK 21 才能使用它: https://github.com/nationalsecurityagency/ghidra
在编写本教程的过程中,我使用 Claude Code 通过 vibe-coding 方式开发了一个 Ghidra 插件,用于使用 AI 重命名函数和变量。你可以从这里下载该插件: https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
安装方法:将压缩文件 ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip 移动到 ${GHIDRA_HOME}\Extensions\Ghidra,然后通过运行 ${GHIDRA_HOME}\ghidraRun.bat 启动 Ghidra。要激活插件,在 Ghidra 初始界面顶部菜单中选择 File -> Install Extensions,然后在插件浏览器中勾选 FastAIRenamerPlugin 旁的复选框,点击 Ok。Ghidra 会提示你重启,请立即重启。
要配置插件,下次 Ghidra 启动时,在顶部菜单中进入 Tools -> Run Tool -> CodeBrowser。Ghidra 会显示“检测到新扩展。是否要配置它们?”。点击“是”,然后再次勾选 FastAIRenamerPlugin 旁的复选框,点击 Ok。当 CodeBrowser 打开后,在顶部菜单中点击 Window -> Fast AI Renamer,然后点击 Config 按钮。在此处你可以配置你的 AI 模型。完成后关闭插件窗口和空的 CodeBrowser 窗口。
注意:如果你在加载插件时遇到任何问题,你可能需要在 Ghidra 中启用开发者模式(File -> Configure -> 勾选 Developer 旁的复选框)。
注意:你随时可以通过以下方式检查插件是否加载:进入 CodeBrowser,点击 File -> Configure -> Ghidra Core -> 点击蓝色配置按钮 -> 搜索“FastAIRenamer” -> 确保其旁的复选框已勾选。
注意:要卸载插件,首先打开 CodeBrowser,File -> Configure -> Ghidra Core -> 点击蓝色配置按钮 -> 搜索“FastAIRenamer” -> 取消勾选 -> 确定。关闭 CodeBrowser,然后在初始 Ghidra 窗口中,File -> Install Extensions -> 取消勾选“FastAIRenamer”。最后关闭 Ghidra 并从 ${GHIDRA_HOME}\Extensions\Ghidra 中删除 ghidra_12.0.4_PUBLIC_20260427_FastAIRenamerPlugin.zip。为确保扩展已被删除,下次运行 Ghidra 时,在初始窗口进入 Help -> Runtime Information -> Extension Points -> 搜索“FastAIRenamer”,确保没有内容显示。呼……
最后,确保你已安装 Visual Studio 或其他 C++ 开发环境,以便在需要时能够通过 vibe-coding 为 crackme 编写解决方案。
每个人的本地 AI 设置会因硬件和预算的不同而有很大差异。对于我来说:
我本人运行的是 Nvidia GTX 3080,安装了 Cuda 13.2。你可以通过在 Windows 终端中运行 nvcc --version 来检查你的 Cuda 版本。
我使用的是来自 bartowski 的量化模型 https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf。也许是我的迷信,但对于分析性任务,我尽量使用最低程度的量化,如果可能则完全避免量化。
我使用 llama.cpp 作为推理服务器,具体版本是:llama-b8893-bin-win-cuda-13.1-x64。
我使用以下 CLI 设置运行 llama.cpp。```
..\llama-b8893-bin-win-cuda-13.1-x64\llama-server.exe ^
--port 8090 ^
--threads 12 ^
--n-gpu-layers 256 ^
--no-mmap ^
--model "google_gemma-4-E4B-it-Q8_0.gguf" ^
--ctx-size 32768 ^
--temp 1.0 ^
--top-k 64 ^
--top-p 0.95 ^
--offline
这会产生 `75 tokens/sec`,对于本地AI来说相当快速。
## 没有硬件?没问题
如果你没有可用的硬件或配置遇到困难,但仍想跟随本教程,你可以获取一个OpenRouter账号并使用其中一个免费API:
https://openrouter.ai/models/?q=free
Google目前(限时)免费提供 `Gemma 4 31B` 和 `Gemma 4 26B-A4B`:
https://openrouter.ai/google/gemma-4-31b-it:free
https://openrouter.ai/google/gemma-4-26b-a4b-it:free
...但请记住,这些免费API速度慢、不可靠,并且你发送给它们的所有数据很可能会被记录到它们的内部分析中,并用于它们的下一次训练运行。不过,就本教程而言,这些API应该能让你跟得上。
要配置Ghidra插件使用你的OpenRouter账号,打开插件配置并输入以下内容:```
Base URL: https://openrouter.ai/api/
API Key: <your OpenRouter API Key>
Model Name: qwen/qwen3-235b-a22b-2507
This example will run the plugin against the qwen3 model here
解压并运行 crackmepls.exe,你应该会看到一个标准的登录界面。输入任意随机密码,你会收到一条“访问被拒绝”的消息:```
User: marko
Pass: 123
Access denied
打开 Ghidra,运行 `ghidraRun.bat`。在工具栏中选择 `File -> New Project`,保留 `Non-Shared Project` 选定,点击 `Next >>`,然后选择一个空的项目目录并给项目命名。点击 `Finish`。
接下来,点击 `File -> Import File`,然后选择 `crackmepls.exe` 将其添加到项目中。Ghidra 会弹出关于该文件的详细信息,告诉你这是一个 `Portable Executable (PE)` 文件,适用于 `x86:LE:64:default:windows`。直接点击 `OK` 接受,不做任何更改。稍等片刻后,它会再次弹出一些文件详细信息,同样点击 `OK` 接受。最后,双击项目中的 `crackmepls.exe` 打开代码浏览器,开始反汇编。
一开始,Ghidra 会弹出一条消息,显示 `crackmepls.exe has not been analyzed. Would you like to analyze it now?`。点击 `Yes`,然后在下一个窗口中点击 `Analyze` 按钮,等待 Ghidra 定位、反汇编并反编译二进制文件中的所有函数。分析过程中可能会收到一些关于找不到 PDB 文件的错误,只需点击 `Ok` 忽略即可。
一旦 Ghidra 完成文件分析,你应该会看到类似下面的界面:

这是标准的 MSVC 入口点。双击 `FUN_14000200c` 并向下滚动。
你应该会在反编译器窗口中看到对 `__p___argv`、`__p___argc` 的引用,以及一个接受它们作为参数的函数调用 `FUN_140001290`。这很可能就是 crackme 的 `main()` 函数,所以双击它。

进入函数 `FUN_140001290` 后,在反编译器窗口中稍微向下滚动。你会看到对字符串 `User:` 和 `Pass:` 的引用,以及对 `basic_istream`(输入流)和 `basic_ostream`(输出流)的引用。

这些字符串与我们在第一次运行 crackme 时看到的打印和输入语句相匹配,因此我们知道正在 crackme 的 `main()` 函数中。
现在,真正繁琐的工作才刚刚开始:我们需要坐下来,将所有变量名和函数调用重命名为有意义的名称,同时分析二进制文件的行为,以便解决 crackme。过去,逆向工程师必须在反汇编中手动完成这项工作,但幸好科技和税收让我们拥有了这个便捷的反编译器,它可以编写脚本与 AI 协作。
因此,与其真正动手,不如让我们放松下来,关掉大脑,让本地 AI 为我们完成所有工作。
在设置本教程的过程中,你应该已经安装了一个用于函数和变量重命名的 Ghidra 插件。现在是时候使用它了。在 Ghidra 顶部的工具栏中,点击 `Window`,然后点击 `Fast AI Renamer` 打开该插件。

你应该会看到插件界面,里面有一些按钮和一个文本区域。首先点击“Config”按钮,然后确保所有配置都能正确与你的本地 AI 通信。我的电脑上配置如下:

点击“Save”关闭配置窗口,然后点击 `Rename Variables` 按钮。会弹出一个带有龙图案的进度窗口,你可能会听到电脑开始吃力运转,因为它正在运行本地 AI 模型来重命名反编译器窗口中的所有变量:

AI 完成后(我的电脑大约需要 10-20 秒),你应该会在插件的文本区域中看到它重命名的变量描述,并且变量本身也已在反编译器窗口中重命名:

所有 AI 模型,尤其是小型本地模型,本质上都不可靠,所以你在最后一步可能会看到一些错误。稍后我们会再次提及这一点。同时,你可以随时再次点击 `Rename Variables` 按钮重新运行 AI,直到获得没有错误且令你满意的结果。
## 解决 Crackme
此时,我们得到了一份经过良好注释的反编译器输出,所有变量名都已重命名为有意义的名称。传统上,逆向工程师现在可以阅读这份代码并开始起草解决该 Crackme 的方案。不过,我觉得尝试让本地 AI 为我们解决这个 crackme 会很有趣。记住,今天我们不会动脑子。
首先,我让 AI 点击 `Rename ALL Functions and Variables` 按钮,为整个二进制文件完成标记工作。这会为这个 crackme 函数中使用的所有函数调用分配名称,从而进一步清理反编译器的输出。在我的机器上,这大约需要 15 分钟。
如果你使用我列在“本地 AI 设置”中的 CLI 参数运行 `llama.cpp`,你可以通过在浏览器中导航到 [http://localhost:8090/](http://localhost:8090/) 打开一个与本地模型的聊天界面。
在这里,我输入了 crackme 中该函数的反编译器输出(你可以通过插件 UI 中的“Decompile Function”按钮获取,或者直接从右侧的反编译器窗口中复制),并让 AI 为我们编写一个解决方案:
======================================================================
======================================================================
======================================================================
我希望你能帮助我为某个 crackme 编写一个解决方案。以下是来自一个 crackme 的反编译器清单,用户在该函数中输入用户名和密码,如果提供了正确的密码,则授予访问权限。密码在函数中计算得出。```
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
/* **Reasoning:**
The function takes user input (a username/input and a password) via standard input. It processes
the user input by calculating a complex, custom checksum/hash. It then compares the provided
password input against a target buffer (likely a stored hash or secret). Finally, it determines
and outputs whether "Access granted" or "Access denied," indicating the function serves as an
authentication routine. */
undefined8 authenticate_user(undefined8 param_1,undefined8 param_2,undefined8 max_len)
{
uint user_checksum;
int iVar1;
undefined8 ****temp_free_ptr;
ulonglong user_input_byte_index;
char *result_message;
undefined1 *status_buffer;
undefined8 ****data_buffer_ptr;
ulonglong pass_input_length_1;
bool access_granted;
undefined8 uStack_d0;
undefined1 status_buffer_small [8];
undefined1 status_buffer_large [32];
undefined8 ***allocated_ptrs [2];
size_t compare_length;
ulonglong ptr_metadata_size;
undefined8 ***user_input_buffer;
undefined8 uStack_78;
ulonglong user_input_length;
ulonglong user_input_length_param;
undefined8 ***pass_input_buffer;
undefined8 uStack_58;
size_t pass_input_length;
ulonglong pass_input_length_param;
ulonglong checksum_seed;
code *code_pointer;
undefined8 ***pass_input_buffer_ptr;
undefined8 ***user_input_buffer_ptr;
ulonglong user_input_length_1;
status_buffer = status_buffer_small;
checksum_seed = MAGIC_VALUE_1 ^ (ulonglong)status_buffer_small;
user_input_byte_index = 0;
uStack_78 = 0;
user_input_length = 0;
user_input_length_param = 0xf;
user_input_buffer = (undefined8 ****)0x0;
uStack_58 = 0;
pass_input_length = 0;
pass_input_length_param = 0xf;
pass_input_buffer = (undefined8 ****)0x0;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"User: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&user_input_buffer,max_len);
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,"Pass: ");
extract_token_from_stream((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,
(longlong *)&pass_input_buffer,max_len);
user_input_length_1 = user_input_length_param;
user_input_buffer_ptr = user_input_buffer;
user_checksum = 0;
pass_input_length_1 = user_input_byte_index;
if (user_input_length != 0) {
do {
temp_free_ptr = &user_input_buffer;
if (0xf < user_input_length_param) {
temp_free_ptr = (undefined8 ****)user_input_buffer;
}
user_checksum =
((int)user_input_byte_index + 1) *
(int)*(char *)((longlong)temp_free_ptr + user_input_byte_index) +
(int)pass_input_length_1;
user_checksum = user_checksum * 8 ^ user_checksum;
user_input_byte_index = user_input_byte_index + 1;
pass_input_length_1 = (ulonglong)user_checksum;
} while (user_input_byte_index < user_input_length);
}
int_to_string_dynamic(allocated_ptrs,user_checksum * 0x539 ^ 0x5a5a);
pass_input_length_1 = pass_input_length_param;
pass_input_buffer_ptr = pass_input_buffer;
temp_free_ptr = &pass_input_buffer;
if (0xf < pass_input_length_param) {
temp_free_ptr = (undefined8 ****)pass_input_buffer;
}
data_buffer_ptr = allocated_ptrs;
if (0xf < ptr_metadata_size) {
data_buffer_ptr = (undefined8 ****)allocated_ptrs[0];
}
if (compare_length == pass_input_length) {
if (compare_length == 0) {
access_granted = true;
}
else {
iVar1 = memcmp(data_buffer_ptr,temp_free_ptr,compare_length);
access_granted = iVar1 == 0;
}
}
else {
access_granted = false;
}
if (0xf < ptr_metadata_size) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0];
status_buffer = status_buffer_small;
if (0xfff < ptr_metadata_size + 1) {
temp_free_ptr = (undefined8 ****)allocated_ptrs[0][-1];
data_buffer_ptr =
(undefined8 ****)((longlong)allocated_ptrs[0] + (-8 - (longlong)temp_free_ptr));
status_buffer = status_buffer_small;
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
temp_free_ptr = data_buffer_ptr;
status_buffer = status_buffer_large;
}
}
*(undefined8 *)(status_buffer + -8) = 0x140001424;
free(temp_free_ptr);
}
result_message = "Access granted\n";
if (!access_granted) {
result_message = "Access denied\n";
}
*(undefined8 *)(status_buffer + -8) = 0x140001443;
formatted_string_output((basic_ostream<char,struct_std::char_traits<char>_> *)cout_exref,result_message);
if (0xf < pass_input_length_1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr;
if (0xfff < pass_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)pass_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)pass_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x14000147d;
free(temp_free_ptr);
}
if (0xf < user_input_length_1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr;
if (0xfff < user_input_length_1 + 1) {
temp_free_ptr = (undefined8 ****)user_input_buffer_ptr[-1];
data_buffer_ptr =
(undefined8 ****)((longlong)user_input_buffer_ptr + (-8 - (longlong)temp_free_ptr));
if ((undefined8 ****)0x1f < data_buffer_ptr) {
code_pointer = (code *)swi(0x29);
(*code_pointer)(5);
status_buffer = status_buffer + 8;
temp_free_ptr = data_buffer_ptr;
}
}
*(undefined8 *)(status_buffer + -8) = 0x1400014b7;
free(temp_free_ptr);
}
*(undefined8 *)(status_buffer + -8) = 0x1400014c9;
return 0;
}
请阅读反编译器清单,然后完成下面的 C++ 代码,为给定的用户名计算出正确的密码:``` #include #include #include #include
int main() { std::string username; std::cout << "User: "; std::cin >> username;
uint32_t password_value = 0;
std::cout << "Pass: " << password_value << "\n";
return 0;
}
## 生成的解决方案
第一次生成的解决方案是错误的。我已经将其包含在这个仓库的 [tut1.txt](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/master/tut1.txt) 中。乍一看,这个解决方案 _看起来_ 是正确的,但当我们尝试时却失败了。人工智能从根本上来说是不可靠的,这是一个完美的例子。
没关系,我们可以简单地打开一个新的聊天窗口,复制粘贴上面的提示,然后尝试让AI为我们生成一个新的解决方案。在我的例子中,我第二次尝试就得到了一个可用的解决方案:```
#include <cstdio>
#include <cstdint>
#include <string>
#include <iostream>
#include <sstream>
#include <iomanip>
/**
* @brief Simulates the custom checksum calculation defined in the crackme function.
*
* @param username The input string (username).
* @return int The final calculated user_checksum.
*/
int calculate_username_checksum(const std::string& username) {
// user_checksum is treated as an 'int' (32-bit signed integer) during the core calculation.
int user_checksum = 0;
// pass_input_length_1 acts as the state variable and is a ulonglong (64-bit).
// It starts at 0, as pass_input_length_1 = user_input_byte_index (which is 0) initially.
uint64_t state_pass_length_1 = 0;
for (size_t i = 0; i < username.length(); ++i) {
// i = user_input_byte_index
// Step 1: Calculate the intermediate value T1.
// The decompiler implies this calculation happens using standard int math
// (truncating/wrapping if intermediate values exceed 32 bits).
// (int)user_input_byte_index + 1
int index_term = (int)(i + 1);
// (int)*(char *)((longlong)temp_free_ptr + user_input_byte_index)
// This is just the ASCII value of the character.
int char_term = (int)username[i];
// (int)pass_input_length_1
// The state is cast down to an int for this arithmetic operation.
int state_term = (int)state_pass_length_1;
// T1 = ((int)user_input_byte_index + 1) * (int)char + (int)state
int intermediate_t1 = index_term * char_term + state_term;
// Step 2: Apply the XOR/Multiplication hash function.
// user_checksum = user_checksum * 8 ^ user_checksum;
// Note: The original code structure suggests the update happens here,
// but since we are recalculating the hash step-by-step, we use the definition:
// user_checksum = T1 * 8 ^ T1;
// Since the original code overwrites user_checksum completely with the hash
// of the current character, we use the T1 derived above.
int current_checksum = intermediate_t1;
current_checksum = current_checksum * 8 ^ current_checksum;
// Update the final running checksum
user_checksum = current_checksum;
// Step 3: Update the state variable for the next iteration.
state_pass_length_1 = (uint64_t)user_checksum;
}
return user_checksum;
}
int main()
{
std::string username;
std::cout << "User: ";
std::cin >> username;
// 1. Calculate the final checksum using the username
int final_user_checksum = calculate_username_checksum(username);
// 2. Calculate the final hash/secret value
// FinalHashValue = user_checksum * 0x539 ^ 0x5a5a
// Note: The arithmetic here is done on the final 32-bit 'int' checksum.
uint32_t final_hash_value = (uint32_t)final_user_checksum * 0x539 ^ 0x5a5a;
// 3. Convert the final hash value into a string (as implied by int_to_string_dynamic)
// Since the password needs to match the memory contents (memcmp), it must be the string representation.
std::stringstream ss;
ss << final_hash_value;
std::string required_password = ss.str();
// We output the required password string.
std::cout << "Pass: " << required_password << "\n";
return 0;
}
我将生成了解决方案的聊天记录作为solution_chat.html包含在这个仓库中。它包含了推理流,我认为对于一个在本地运行的小模型来说,这非常酷。
无论如何,我们可以用Visual Studio编译解决方案,然后运行它来生成一个有效的用户名/密码组合。然后我们将用户名/密码输入到crackme中,并验证我们已解决它:

gg
正如我们在本教程中多次看到的,AI天生不可靠。考虑以下两个截图:


第一个截图来自教程前面部分,而第二个截图是在同一个步骤中拍摄的,只是AI重命名的另一次运行。注意在反编译器窗口中,上图中密码输入缓冲区被标记为&pass_input_buffer,而下图中同一个缓冲区被标记为&username_buffer_1。在下图中,AI在欺骗我们,我们把这些谎言称为“幻觉”。
所有AI都会对你撒谎和产生幻觉。模型越小,幻觉越频繁。模型越大,你越幸运,它们越少产生幻觉。你付出的越多,你越幸运。然而,没有哪个AI模型能免于幻觉,你在使用它们时绝不能忘记这一点。
AI最适合于你可以验证输出,或者输出风险足够低以至于不需要完美准确性的任务。对于逆向工程,AI可以帮助我们快速窥探未知的二进制文件,节省大量繁琐的手动工作。但正如我们在它做的糟糕重命名中所看到的,它也可能把我们引向错误的方向。始终谨慎对待AI生成的结果,并在可能的情况下验证它们。
使用AI的最佳方式是自动验证其结果。例如,如果你让AI编写代码,最好有一个脚本来测试AI的输出与你手动编写的测试套件。对于AI为我们的crackme生成解决方案的情况,我们可以编写一个测试套件,自动测试每个AI生成的解决方案对crackme的适用性,然后接受一个能为数千个不同用户名生成正确密码的解决方案。然后我们可以循环运行AI,直到它写出通过我们测试的解决方案。
我们用相对较小的模型(Gemma 4 E4B)解决了这个crackme。该模型有80亿个参数,与现有的SotA模型不同,后者有数千亿甚至数万亿个参数。较小的模型不能很好地推理,会更频繁地产生幻觉,并且随着上下文窗口填满而更快地变得无用。
在真正的逆向工程练习中,我们可能必须逆向工程那些超过我们小模型上下文窗口的函数,或者那些逻辑过于复杂以至于其有限的智能无法推理的函数。我们需要在使用模型时熟悉其局限性,以便提前了解它们将在何处以及如何失败。
随着消费级硬件与模型架构同步发展,我相信我们将能够在本地运行更大、更好的模型,这将使更高级的逆向工程任务触手可及。
我编写的“Fast AI Renamer”Ghidra插件目前会为它分析的每个函数编写摘要,并且能够批量导出正在处理的项目中的C和汇编代码清单。我想编写一个工具,为每个函数摘要计算一个嵌入,然后将其索引到向量数据库或SQLite中,这样我们就可以通过自然语言查询来查询数据库以查找函数。
Ghidra插件目前使用简单的工作流进行重命名,没有多轮对话,并依赖于从AI输出中解析CSV来完成任务。我这样实现是因为我认为小AI模型无法处理代理工作流中固有的长对话和多次工具调用。我可能错了,应该探索其他提示AI的方式。不过,对于小型本地模型,我认为最好尽可能限制上下文窗口大小。
我不确定Ghidra插件对提示注入的脆弱性如何,任何安全专业人员都应谨慎使用它来分析恶意软件或恶意二进制文件。一般来说,这适用于所有AI驱动的逆向工程工具,它们可能具有本地主机未经身份验证的MCP服务器,或者可用于实现代码执行的敏感功能的工具。请小心。
鉴于我们正在使用小型通用模型进行逆向工程,我想知道通过从更大、更智能的模型中提炼的对话对其进行微调,可以将其性能提升多少?
最后,Ghidra插件的C/汇编导出功能与Claude Code配合良好,我能够通过向它提供crackme的带注解的反编译,并告诉它寻找解决方案,第一次就使用Claude Code + Sonnet写出正确的解决方案。我想知道我们是否可以为Claude Code编写一些技能,使其更好地通过反编译器输出的文本文件分析二进制文件,而不必通过MCP服务器连接Ghidra来执行分析。例如,我们可以为Claude Code引入一个技能,使其能够检查二进制文件虚拟内存中的字节,这样如果它看到反汇编/反编译器列表中有引用某数据,就可以看到该内存包含的内容,而无需通过Ghidra。
感谢Kryptos制作了Crackme,以及Crackmes.one托管它。感谢他们接过了crackmes.de的接力棒(RIP)。
感谢Google发布了一个令人惊叹且精巧的本地模型。
感谢NSA花我的税钱做了一个很棒的逆向工程工具,也感谢Ryan Kurtz在Ghidra Github上提供的所有支持。