markoglasgow/gemma_crackme_tutorial
GitHub: markoglasgow/gemma_crackme_tutorial
演示如何使用Google Gemma本地AI模型配合Ghidra插件辅助逆向工程,自动化分析并求解Crackme程序。
Stars: 15 | Forks: 0
# 使用 Google 的 Gemma 4 E4B 本地 AI 模型对一个简单的 Crackme 进行逆向工程
我最近在试玩 Google 发布的全新 [Gemma E4B 开放权重本地模型](https://huggingface.co/google/gemma-4-E4B),令我惊讶的是,我发现将它用于本地离线逆向工程场景取得了巨大的成功。我想写这篇教程是为了广而告之:本地 AI 现在已经足以胜任许多基础的逆向任务,而且未来情况可能会迅速改善。
## 逆向工程与 AI
逆向一个新二进制文件最繁琐的部分之一,就是刚开始的时候,你对哪些是重要的函数和变量一无所知。逆向工程师们通常会使用许多技巧来着手分析,包括查看字符串引用、[二进制对比](https://github.com/quarkslab/qbindiff),或者[匹配相似的函数](https://github.com/quarkslab/sighthouse)。
AI 在这方面非常擅长提供帮助,而且我个人在使用 [OpenAI API](https://openai.com/api/) 标记二进制文件或清理反编译输出方面取得了极大的成功。然而,使用这些 API 也有几个缺点:
- *成本* - 反编译和反汇编会产生大量的 token。API 按 token 计费,因此分析较大的二进制文件可能会花费相当可观的 $$$。如果你处理的是一个每周更新、包含大量二进制文件的庞大目标,这些成本会迅速累积,对于业余逆向工程师来说是难以承受的。
- *隐私* - 当你使用远程 API 时,AI 的主机可以洞察你在做什么。在某些专业场景中,这是绝对不可接受的。
- *控制权* - 当你依赖远程 API 时,你[无法控制提供给你的是哪些模型](https://www.reddit.com/r/WritingWithAI/comments/1qvul6g/openai_is_quietly_removing_gpt4o_from_chatgpt_for/),或者[模型的质量如何](https://news.ycombinator.com/item?id=47778035)。如果你依赖它们处理关键任务,那么当你在需要它们时[它们变得很慢](https://www.reddit.com/r/openrouter/comments/1r4fbxe/openrouter_unreliable_slow_and_more/)或者[宕机](https://news.ycombinator.com/item?id=47779730)时,或者当它们的输出质量下降到毫无用处的时候,这就会成为一个问题。
运行你自己的本地 AI 模型可以解决其中一些痛点:
- *成本* - 运行本地模型可能比依赖托管服务便宜得多。虽然你的本地模型可能比优秀提供商的模型更小、更慢,但如果它足够好并且能在合理的时间内运行,那么运行自己的模型就可以省钱,特别是当你需要为了简单的任务去处理大量数据时,这就非常有意义了。
- *隐私* - 当你在本地运行模型时,不会发生任何网络调用,你可以完全控制你的隐私。没有人能看到你在自己的机器上使用该模型做什么。
- *控制权* - 开放权重模型的美妙之处在于没有人能把它从你身边夺走。OpenAI 或 Anthropic 可能有一天会通过涨价或明确移除其 API 的方式,使其最先进的模型变得不可用。但对于开放权重模型来说,无论好坏,你都将命运掌握在自己手中。
不过,本地 AI 模型也有其*缺点*:
- *大小* - 模型越大,就越智能。然而,大多数大模型无法安装在消费级硬件上。因此,如果你运行的是本地模型,那么你运行的模型很可能比 SotA(State-of-the-Art,最先进的)模型小 10 到 100 倍。规模的减小直接导致了模型智能的下降,这使得它们[不适合许多在 ChatGPT/Codex 或 Claude 等 SotA 模型中人们习以为常的任务](https://www.reddit.com/r/ClaudeCode/comments/1stenhn/claude_code_locally_with_ollama_sucks_or_maybe_im/)。
- *速度* - 与使用 AI API 相比,本地模型在你的机器上运行起来可能会更慢。同样,这是由于消费级硬件的局限性,以及 API 提供商可以做一些[通常对你不可用的技巧](https://aws.amazon.com/blogs/machine-learning/optimize-your-inference-jobs-using-dynamic-batch-inference-with-torchserve-on-amazon-sagemaker/)。
- *配置* - 运行本地模型就像试图在一台翻新笔记本电脑上运行 Linux,而相比之下,走进苹果商店买一台全新的 Macbook Air 则要轻松得多。Codex 和 Claude Code 的体验就像是 AI 领域的“苹果商店”体验。而本地 AI 模型的体验,就像是一个戴着软呢帽的家伙在他的车库里敲打一台破电脑试图让它运行起来。至少,你必须操心以下几件事:
- 购买合适的硬件
- GPU 驱动程序
- 选择并配置正确的推理服务器
- 选择一个能装进你的硬件、运行速度快到实用、且对于手头任务足够智能的模型
- 为你的模型使用正确的聊天模板
- 探索模型的量化方案,以在大小/速度与模型智能之间找到合适的权衡。
- 正确地提示模型
- 选择合适的驱动工具,或者在现有工具都不管用时自己制作一个。
这不是一段轻松的旅程,许多人放弃了,并认为本地 AI 模型无法胜任任务,因为他们始终没有找到合适的硬件/模型/设置/提示词/工具组合来让它们完成他们的工作。虽然在许多情况下他们是对的,但我希望这篇教程至少能揭示本地模型已经发展到了什么程度,它们能如何辅助逆向工程,并启发人们去尝试一下本地 AI。
## 逆向工程环境配置
- 该 crackme 是一个极其简单的 Windows crackme,从此处的 [Crackmes.one] 下载:
https://crackmes.one/crackme/69e13f938afd9d6c48b488fd
(解压密码是 `crackmes.one`)。我在[此代码库中](https://github.com/markoglasgow/gemma_crackme_tutorial/blob/master/crackmepls.exe.bin)提供了一个备用链接,以防原始链接失效。
- 使用 *Ghidra 12.04* 进行反汇编和反编译。你需要安装 [OpenJDK 21](https://docs.aws.amazon.com/corretto/latest/corretto-21-ug/downloads-list.html) 才能使用它:
https://github.com/nationalsecurityagency/ghidra
- 在编写本教程时,我与 Claude Code 结对编程,开发了一个 Ghidra 插件,可以使用 AI 重命名函数和变量。你可以从这里下载该插件:
https://github.com/markoglasgow/Ghidra_FastAIRenamer_Plugin
要安装它,只需将 zip 文件 `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 中启用开发者模式
*注意*:你随时可以检查插件是否已加载,方法是转到 CodeBrowser,点击 File -> Configure -> Ghidra Core -> 点击蓝色的配置按钮 -> 按“FastAIRenamer”过滤 -> 确保其名称旁边的复选框已勾选。
*注意*:要卸载该插件,首先打开 CodeBrowser,File -> Configure -> Ghidra Core -> 点击蓝色的配置按钮 -> 按“FastAIRenamer”过滤 -> 取消勾选 -> ok。关闭 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++ 开发环境,以便在时机成熟时能够结对编程出一个该 crackme 的解决方案。
## 本地 AI 设置
人们的本地 AI 设置会因他们拥有的硬件和资金而有很大差异。对我来说:
- 我个人运行的是 [Nvidia GTX 3080](https://www.techpowerup.com/gpu-specs/geforce-rtx-3080.c3621),并安装了 [Cuda 13.2](https://developer.nvidia.com/cuda-toolkit-archive)。你可以在 Windows 终端中运行 `nvcc --version` 来检查你的 Cuda 版本。
- 我正在运行 [bartowski](https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF) 提供的 [https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/blob/main/google_gemma-4-E4B-it-Q8_0.gguf](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](https://github.com/ggml-org/llama.cpp/releases/tag/b8893)。
- 我使用以下 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:
Model Name: qwen/qwen3-235b-a22b-2507
```
此示例将使用[此处](https://openrouter.ai/qwen/qwen3-235b-a22b-2507)的 qwen3 模型运行该插件
## 检查 Crackme
解压并运行 `crackmepls.exe`,你应该会看到一个标准的登录界面。输入任意随机密码,你会得到一个“Access Denied”消息:
```
User: marko
Pass: 123
Access denied
```
通过运行 `ghidraRun.bat` 打开 Ghidra。在工具栏中选择 `File -> New Project`,保持 `Non-Shared Project` 被选中并点击 `Next >>`,然后选择一个空的项目目录并为项目命名。接着点击 `Finish`。
接下来,点击 `File -> Import File`,然后选择 `crackmepls.exe` 将其添加到项目中。Ghidra 会弹出一些关于该文件的详细信息,告诉你这是一个用于 `x86:LE:64:default:windows` 的 `Portable Executable (PE)` 文件。只需点击 `OK` 接受而不更改任何内容。经过短暂的延迟后,它会弹出更多关于该文件的信息,再次点击 `OK` 接受即可。最后,在项目中双击 `crackmepls.exe` 打开 Code Browser,开始反汇编。
在最开始,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` 来打开该插件。

你应该会看到该插件的 UI 界面,包含许多按钮和一个文本区域。首先点击“Config”按钮,然后确保所有的配置都已正确设置,能够与你的本地 AI 通信。以下是我电脑上的配置界面:

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

一旦 AI 完成工作(在我的机器上大约需要 10-20 秒),你应该会在插件的文本区域中看到关于哪些变量被重命名的描述,并且反编译窗口中的变量本身也应已被重命名:

所有的 AI 模型,尤其是小型本地模型,本质上都是不可靠的,因此你可能会在这最后一步看到一些错误。我们稍后会再次讨论这个问题。与此同时,你随时可以再次点击 `Rename Variables` 按钮重新运行 AI,直到你获得没有错误且令你满意的结果为止。
## 解决 Crackme
此时,我们得到了一些标记良好的反编译输出,其中所有的变量名都被重命名为有意义的名称。传统上,逆向工程师现在可以阅读这些代码并开始为该 Crackme 起草解决方案。然而,我认为尝试让本地 AI 替我们解决这个 crackme 会是一件很酷的事情。记住,我们今天可不打算动脑子。
首先,我通过点击 `Rename ALL Functions and Variables` 按钮,让 AI 为我们完成了整个二进制文件的标记。这有助于为该 crackme 函数中使用的所有函数调用分配名称,从而进一步清理反编译输出。在我的机器上,这大约需要 15 分钟。
当你在 `Local AI Setup` 下使用我列出的 CLI 参数运行 `llama.cpp` 时,你可以在浏览器中导航至 [http://localhost:8090/](http://localhost:8090/),打开一个指向本地模型的 Chat UI。
在这里,我输入了该 crackme 函数的反编译输出(你可以通过点击插件 UI 中的“Decompile Function”按钮获取,或者直接从右侧的反编译窗口中复制),并要求 AI 为我们编写一个解决方案:
# 空行 1
======================================================================
我想让你帮我编写一个 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_> *)cout_exref,"User: ");
extract_token_from_stream((basic_istream_> *)cin_exref,
(longlong *)&user_input_buffer,max_len);
formatted_string_output((basic_ostream_> *)cout_exref,"Pass: ");
extract_token_from_stream((basic_istream_> *)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_> *)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;
}
```
# 空行 2
======================================================================
## 生成的解决方案
第一次它生成的解决方案是不正确的。我在[本代码库的 tut1.txt](tut1.txt) 中包含了这个方案。乍一看,该解决方案_看起来_是正确的,但当我们尝试时却失败了。AI 从根本上说是不可靠的,这就是一个完美的例子。
没关系,我们可以简单地打开一个新的聊天窗口,复制粘贴上面的提示词,并尝试让 AI 为我们生成一个新的解决方案。就我而言,我在第二次尝试时得到了一个可行的解决方案:
```
#include
#include
#include
#include
#include
#include
/**
* @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](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 生成解决方案的情况下,我们可以编写一个测试套件,针对该 crackme 自动测试每个 AI 生成的解决方案,然后接受那个能为成千上万个不同用户名生成正确密码的方案。然后我们可以循环运行 AI,直到它编写出通过我们测试的解决方案。
## 小型模型的局限性
我们使用一个相对较小的模型(Gemma 4 E4B)解决这个 crackme。该模型拥有 80 亿个参数,不像[拥有数千亿甚至数万亿参数](https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro)的 SotA 模型那样。较小的模型将无法很好地进行推理,会更频繁地产生幻觉,并且随着其上下文窗口被填满,会更快地变得毫无用处。
在真实的逆向工程练习中,我们可能需要对大于我们小型模型上下文窗口的函数进行逆向工程,或者其中包含的逻辑对其有限的智能来说过于复杂而难以推理。我们在使用模型时需要熟悉其局限性,以便我们提前了解它们会在哪里让我们失望以及是如何让我们失望的。
随着消费级硬件与模型架构的并驾齐驱的发展,我相信我们将能够在本地运行更大、更好的模型,这将使我们的本地模型能够触及更高级的逆向工程任务。
## 未来工作
我编写的“Fast AI Renamer”Ghidra 插件目前会为其分析的每个函数编写摘要,并且它能够批量导出正在处理的项目的 C 和 asm 列表。我想编写一个工具来为每个函数摘要计算 [embedding](https://huggingface.co/google/embeddinggemma-300m),然后将其索引到 [Vector database](https://github.com/datastax/jvector) 或 [SQLite](https://github.com/sqliteai/sqlite-vector) 中,接着我们就可以根据自然语言查询对数据库进行检索以寻找函数。
该 Ghidra 插件目前使用的是不带多轮对话的简单工作流进行重命名,并依赖于从 AI 输出中解析 CSV 来完成其任务。我之所以这样实现,是因为我认为小型 AI 模型无法处理智能体工作流中固有的长对话和多次工具调用。我可能是错的,探索提示 AI 的替代方法值得尝试。尽管如此,对于小型本地模型,我认为最好尽可能限制上下文窗口的大小。
我不确定该 Ghidra 插件对提示注入的抵御能力如何,任何安全专业人员在用它分析恶意软件或充满敌意的二进制文件时都应该保持警惕。总的来说,这适用于所有由 AI 驱动的逆向工程工具,这些工具可能拥有未经身份验证的 localhost MCP 服务器,或者提供可用于实现代码执行的敏感函数的工具。务必小心。
鉴于我们正在使用一个用于逆向工程的小型通用模型,我想知道如果使用从更大、更智能的模型中[蒸馏]()出来的对话对其进行微调,其性能能有多大程度的提升?
最后,该 Ghidra 插件的 C/asm 导出功能与 Claude Code 配合得很好,我能够在第一次尝试时就使用 Claude Code + Sonnet 为该 crackme 编写了正确的解决方案,只需将标记好的 crackme 反编译代码喂给它并告诉它找出解决方案即可。我想知道我们是否可以为 Claude Code 编写一些技能,使其更好地通过反编译器输出的文本文件来分析二进制文件,而不是让它通过 MCP 服务器连接到 Ghidra 来执行分析。例如,我们可以向 Claude Code 引入一个 技能,这将允许它检查二进制文件虚拟内存中的字节,这样如果它看到反汇编/反编译列表中引用了某些数据,它就可以查看该内存包含的内容,而无需通过 Ghidra。
## 感谢与致谢
感谢 [Kryptos](https://crackmes.one/user/Kryptos) 制作了该 Crackme,并感谢 [Crackmes.one](https://crackmes.one) 提供托管。我感激他们接过了 crackmes.de 停下脚步的地方继续前行(RIP)。
感谢 Google 发布了令人惊叹且实用便捷的本地模型。
感谢 NSA 使用我的税金制作了一个酷炫的逆向工程工具,也感谢 [Ryan Kurtz](https://github.com/ryanmkurtz) 在 Ghidra Github 上提供的所有支持。
标签:AI辅助安全, API分析, API逆向, Crackme, DLL 劫持, DNS 反向解析, Gemma 4 E4B, Google Gemma, JS文件枚举, LLM, Unmanaged PE, URL提取, Vectored Exception Handling, Wayback Machine, 二进制分析, 二进制比对, 云安全运维, 云资产清单, 人工智能, 反汇编, 反编译, 可观察性, 后端开发, 大语言模型, 安全隐私, 实时告警, 开源大模型, 技术教程, 本地AI模型, 本地离线推理, 用户模式Hook绕过, 离线大模型, 逆向工程, 零成本安全测试