
Google의 Gemma 4 E4B 로컬 AI 모델을 사용하여 Ghidra로 Windows 크랙미를 리버스 엔지니어링하는 방법에 대한 단계별 튜토리얼. 로컬 추론 설정 및 자동 함수/변수 이름 변경을 포함합니다.
저는 Google이 출시한 새로운 Gemma E4B 오픈 가중치 로컬 모델을 가지고 놀던 중, 로컬 오프라인 리버스 엔지니어링 시나리오에서 놀라운 성공을 거두는 것을 보았습니다. 이 튜토리얼을 작성하여 로컬 AI가 이제 많은 기본적인 리버싱 작업에 충분히 좋다는 사실을 알리고, 앞으로 상황이 빠르게 개선될 것이라는 점을 전파하고자 합니다.
새로운 바이너리를 리버싱할 때 가장 지루한 부분 중 하나는 처음에 중요한 함수와 변수에 대한 통찰력이 전혀 없을 때입니다. 리버서들이 시작하기 위해 사용하는 많은 트릭이 있으며, 문자열 참조 확인, 바이너리 디핑, 또는 유사 함수 매칭 등이 있습니다.
AI는 이 부분에서 매우 도움이 되며, 저는 개인적으로 OpenAI API를 사용하여 바이너리를 마크업하거나 디컴파일러 출력을 정리하는 데 큰 성공을 거두었습니다. 그러나 이러한 API를 사용하는 데는 몇 가지 단점이 있습니다:
비용 - 디컴파일 및 디스어셈블리는 엄청난 양의 토큰을 생성합니다. API는 토큰당 요금을 부과하므로 더 큰 바이너리를 분석하는 데 상당한 비용이 들 수 있습니다. 매주 업데이트되는 많은 바이너리를 가진 대규모 대상을 다루는 경우 이러한 비용은 빠르게 누적되며, 취미 리버스 엔지니어에게는 부담스럽습니다.
개인정보 - 원격 API를 사용할 때 AI 호스트는 사용자가 무엇을 하고 있는지 파악할 수 있습니다. 이는 일부 전문적인 시나리오에서는 사용이 불가능합니다.
제어 - 원격 API에 의존할 때 어떤 모델이 제공되는지, 또는 모델의 품질이 어떤지에 대해 제어할 수 없습니다. 중요한 작업에 의존하는 경우, 필요할 때 속도가 느려지거나 다운되거나 출력 품질이 쓸모없을 정도로 저하될 때 문제가 될 수 있습니다.
자체 로컬 AI 모델을 실행하면 이러한 문제점 중 일부를 해결할 수 있습니다:
비용 - 호스팅 서비스에 의존하는 것보다 로컬 모델을 실행하는 것이 훨씬 저렴할 수 있습니다. 로컬 모델이 좋은 제공업체의 모델보다 작고 느릴 수 있지만, 충분히 좋고 합리적인 시간 내에 실행된다면, 특히 단순한 작업을 위해 많은 양의 데이터를 처리하는 경우 자체 모델을 실행하여 비용을 절약하는 것이 합리적일 수 있습니다.
개인정보 - 모델을 로컬에서 실행하면 네트워크 호출이 발생하지 않으며 개인정보를 완전히 통제할 수 있습니다. 아무도 사용자가 자신의 기기에서 모델을 무엇에 사용하는지 볼 수 없습니다.
제어 - 오픈 가중치 모델의 장점은 아무도 당신에게서 그것을 빼앗을 수 없다는 것입니다. OpenAI나 Anthropic은 언젠가 가격 인상이나 API를 명시적으로 제거함으로써 SotA 모델을 사용할 수 없게 만들 수 있습니다. 그러나 오픈 가중치 모델을 사용하면 좋든 나쁘든 자신의 운명을 통제할 수 있습니다.
하지만 로컬 AI 모델에는 단점이 있습니다:
크기 - 모델이 클수록 더 똑똑합니다. 그러나 대부분의 큰 모델은 일반 소비자 하드웨어에 맞지 않습니다. 따라서 로컬 모델을 실행한다면 SotA(State-of-the-Art) 모델보다 10배에서 100배 작은 모델을 실행할 가능성이 높습니다. 이러한 크기 감소는 모델의 지능 감소로 직접 이어져 ChatGPT/Codex 또는 Claude와 같은 SotA 모델에서 사람들이 당연하게 여기는 많은 작업에 부적합하게 만듭니다.
속도 - 로컬 모델은 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로 AI를 사용하여 함수와 변수 이름을 바꾸는 Ghidra 플러그인을 바이브 코딩했습니다. 플러그인은 여기에서 다운로드할 수 있습니다: 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에서 개발자 모드를 활성화해야 할 수 있습니다 (File -> Configure -> Developer 옆 체크박스).
참고: 플러그인이 로드되었는지 확인하려면 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++ 개발 환경이 설정되어 있는지 확인하세요.
사람들의 로컬 AI 설정은 하드웨어와 사용 가능한 $$$에 따라 크게 다릅니다. 제 경우:
저는 개인적으로 Nvidia GTX 3080을 사용 중이며, Cuda 13.2가 설치되어 있습니다. Cuda 버전은 Windows 터미널에서 nvcc --version을 실행하여 확인할 수 있습니다.
저는 bartowski의 bartowski 양자화 모델을 실행하고 있습니다. 미신일 수도 있지만, 분석 작업의 경우 가능한 한 양자화를 최소화하여 실행하려고 하며, 가능하면 완전히 피하려고 합니다.
저는 추론 서버로 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
이 예제는 qwen3 모델에 대해 플러그인을 실행합니다 여기
압축을 풀고 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가 파일에 대한 세부 정보를 팝업으로 표시하며, 파일이 `windows`용 `x86:LE:64:default:windows`용 `Portable Executable (PE)` 파일임을 알려줍니다. 아무것도 변경하지 않고 `OK`를 클릭하여 수락합니다. 잠시 지연된 후 파일에 대한 추가 세부 정보가 다시 팝업되며, 다시 `OK`를 클릭하여 수락합니다. 마지막으로 프로젝트에서 `crackmepls.exe`를 더블 클릭하여 Code Browser를 열고 디스어셈블을 시작합니다.
가장 처음에 Ghidra가 `crackmepls.exe가 아직 분석되지 않았습니다. 지금 분석하시겠습니까?`라는 메시지를 표시합니다. `예`를 클릭한 다음 다음 창에서 `Analyze` 버튼을 클릭하고, Ghidra가 바이너리의 모든 함수를 찾아 디스어셈블하고 디컴파일할 때까지 기다립니다. 분석 중 PDB 파일을 찾을 수 없다는 오류가 한두 번 발생할 수 있습니다. `확인`을 클릭하고 무시하면 됩니다.
Ghidra가 파일 분석을 완료하면 아래와 같은 화면이 표시됩니다:

이것은 일반적인 MSVC 진입점입니다. `FUN_14000200c`를 더블 클릭하고 아래로 스크롤합니다.
디컴파일러 창에서 `__p___argv`, `__p___argc`에 대한 참조와 이들을 매개변수로 받는 함수 호출 `FUN_140001290`이 표시됩니다. 이는 크랙미의 `main()` 함수일 가능성이 높으므로 더블 클릭합니다.

함수 `FUN_140001290`에 들어가면 디컴파일러 창에서 아래로 약간 스크롤합니다. 문자열 `User:` 및 `Pass:`에 대한 문자열 참조와 함께 `basic_istream` (입력 스트림) 및 `basic_ostream` (출력 스트림)에 대한 참조가 표시됩니다.

이 문자열들은 크랙미를 처음 실행했을 때 본 출력 및 입력 문과 일치하므로, 우리는 크랙미의 `main()` 함수에 있다는 것을 알 수 있습니다.
이 시점에서 우리는 실제로 앉아서 모든 변수 이름과 함수 호출을 의미 있는 이름으로 바꾸고, 바이너리가 무엇을 하는지 분석하여 크랙미를 풀어야 하는 지루한 작업이 시작됩니다. 리버스 엔지니어들은 예전에는 디스어셈블리에서 이 작업을 수동으로 해야 했지만, 다행히도 기술과 세금 덕분에 AI와 함께 스크립팅할 수 있는 편리한 디컴파일러가 생겼습니다.
따라서 실제 작업을 하는 대신 편안히 앉아 뇌를 끄고 로컬 AI가 모든 작업을 대신하도록 합시다.
이 튜토리얼 설정 중에 함수 및 변수 이름 바꾸기를 돕는 Ghidra 플러그인을 설치했어야 합니다. 이제 이 플러그인을 사용할 때입니다. Ghidra의 상단 툴바에서 `Window`를 클릭한 다음 `Fast AI Renamer`를 클릭하여 플러그인을 엽니다.

플러그인 UI가 표시됩니다. 여러 버튼과 텍스트 영역이 있습니다. "Config" 버튼을 클릭하여 시작한 다음, 로컬 AI와 통신하기 위해 모든 것이 올바르게 구성되었는지 확인합니다. 제 컴퓨터에서의 설정은 다음과 같습니다:

"Save"를 클릭하여 구성 창을 닫은 다음 `Rename Variables` 버튼을 클릭합니다. 용이 있는 진행 창이 팝업되고, 컴퓨터가 로컬 AI 모델을 실행하여 디컴파일러 창의 모든 변수 이름을 바꾸기 위해 작업하면서 힘을 내는 소리가 들릴 수 있습니다:

AI가 완료되면 (제 컴퓨터에서 약 10~20초 소요) 플러그인의 텍스트 영역에 이름이 바뀐 변수에 대한 설명이 표시되고, 디컴파일러 창에서 변수 이름이 변경되어 표시됩니다:

모든 AI 모델, 특히 작은 로컬 모델은 본질적으로 신뢰할 수 없으므로 이 마지막 단계에서 오류가 발생할 수 있습니다. 이에 대해서는 나중에 다시 다루겠습니다. 그동안 `Rename Variables` 버튼을 다시 클릭하여 AI를 다시 실행할 수 있으며, 오류 없이 만족스러운 결과가 나올 때까지 실행할 수 있습니다.
## 크랙미 풀기
이 시점에서 우리는 모든 변수 이름이 의미 있는 이름으로 변경된 깔끔한 디컴파일러 출력을 얻었습니다. 전통적으로 리버스 엔지니어는 이제 이 코드를 읽고 크랙미에 대한 해결책을 작성하기 시작할 수 있습니다. 그러나 로컬 AI가 우리 대신 크랙미를 풀도록 시도하는 것이 흥미로울 것 같다고 생각했습니다. 기억하세요, 오늘은 우리의 두뇌를 사용하지 않을 것입니다.
먼저, AI가 `Rename ALL Functions and Variables` 버튼을 클릭하여 전체 바이너리에 대한 마크업을 완료하도록 했습니다. 이렇게 하면 이 크랙미 함수에서 사용되는 모든 함수 호출에 이름이 할당되어 디컴파일러 출력이 더욱 깔끔해집니다. 제 컴퓨터에서 이 작업은 약 15분이 걸립니다.
`로컬 AI 설정` 아래에 나열된 CLI 매개변수와 함께 `llama.cpp`를 사용하는 경우, 브라우저에서 [http://localhost:8090/](http://localhost:8090/)로 이동하여 로컬 모델에 대한 채팅 UI를 열 수 있습니다.
여기에서 크랙미 함수의 디컴파일러 출력을 입력했습니다 (플러그인 UI에서 "Decompile Function" 버튼을 클릭하거나 오른쪽 디컴파일러 창에서 직접 복사하여 얻을 수 있음) 그리고 AI에게 우리를 위한 해결책을 코딩해 달라고 요청했습니다:
======================================================================
======================================================================
======================================================================
크랙미 해결책 작성을 도와주세요. 아래는 크랙미의 함수 디컴파일러 목록입니다. 사용자가 사용자 이름과 암호를 입력한 후 올바른 암호를 제공하면 액세스가 허용됩니다. 암호는 함수 내에서 계산됩니다.```
/* 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는 근본적으로 신뢰할 수 없으며, 이것이 그 완벽한 예입니다.
괜찮습니다. 새 채팅을 열고 위의 프롬프트를 복사하여 붙여넣은 후 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;
}
I included the chat which generated the solution as solution_chat.html in this repo. It included the reasoning stream, which I thought was pretty cool for such a tiny model running locally.
어쨌든, Visual Studio로 솔루션을 컴파일한 후 실행하여 유효한 사용자 이름/비밀번호 조합을 생성할 수 있습니다. 그런 다음 사용자 이름/비밀번호를 크랙미에 입력하여 해결되었는지 확인합니다:

gg
이 튜토리얼 전체에서 여러 번 보았듯이, AI는 본질적으로 신뢰할 수 없습니다. 아래 두 스크린샷을 살펴보세요:


첫 번째 스크린샷은 튜토리얼의 이전 부분에서 가져온 것이고, 두 번째 스크린샷은 동일한 단계에서 AI 이름 바꾸기를 다르게 재실행한 결과입니다. 디컴파일러 창에서 위쪽 스크린샷에서는 비밀번호 입력 버퍼가 &pass_input_buffer로 레이블되어 있는 반면, 아래쪽 스크린샷에서는 동일한 버퍼가 &username_buffer_1로 레이블되어 있습니다. 아래쪽 스크린샷에서 AI는 우리에게 거짓말을 하고 있으며, 우리는 그런 거짓말을 '환각(hallucination)'이라고 부릅니다.
모든 AI는 여러분에게 거짓말하고 환각을 보입니다. 모델이 작을수록 환각이 더 자주 발생합니다. 모델이 클수록 더 운이 좋아지고 환각이 줄어듭니다. 더 많은 비용을 지불할수록 더 운이 좋아집니다. 그러나 어떤 AI 모델도 환각에 면역되지 않으며, AI 작업을 할 때 이 점을 절대 잊어서는 안 됩니다.
AI는 출력을 검증할 수 있거나 완벽한 정확성이 필요하지 않을 정도로 위험 부담이 적은 작업에 가장 적합합니다. 리버스 엔지니어링의 경우, AI는 알려지지 않은 바이너리를 빠르게 살펴보고 많은 지루한 수동 작업을 줄이는 데 도움을 줄 수 있습니다. 그러나 우리가 본 잘못된 이름 바꾸기처럼 AI는 우리를 잘못된 길로 인도할 수도 있습니다. 항상 AI 생성 결과를 주의해서 다루고, 가능하다면 항상 검증하세요.
AI를 가장 잘 사용하는 방법은 그 결과를 자동으로 검증하는 것입니다. 예를 들어, AI가 코드를 작성하게 하는 경우, AI의 출력을 수동으로 작성한 테스트 스위트에 대해 테스트하는 스크립트가 있으면 좋을 것입니다. AI가 우리의 크랙미에 대한 솔루션을 생성하는 경우, 각 AI 생성 솔루션을 크랙미에 대해 자동으로 테스트하고 수천 개의 다른 사용자 이름에 대해 좋은 비밀번호를 생성하는 솔루션을 수락하는 테스트 스위트를 작성할 수 있습니다. 그런 다음 테스트를 통과하는 솔루션을 작성할 때까지 루프에서 AI를 실행할 수 있습니다.
우리는 비교적 작은 모델(Gemma 4 E4B)로 이 크랙미를 해결했습니다. 이 모델은 80억 개의 파라미터를 가지고 있으며, 수백억에서 수조 개의 파라미터를 가진 최첨단(SotA) 모델과 다릅니다. 더 작은 모델은 추론을 잘하지 못하고, 더 자주 환각을 보이며, 컨텍스트 윈도우가 채워짐에 따라 더 빨리 쓸모없게 됩니다.
실제 리버스 엔지니어링 작업에서는 소형 모델의 컨텍스트 윈도우보다 크거나 제한된 지능으로 추론하기에 너무 복잡한 논리를 가진 함수를 리버스 엔지니어링해야 할 수도 있습니다. 모델을 사용할 때 그 한계를 잘 알고 있어야 미리 어디에서 어떻게 실패할지 이해할 수 있습니다.
소비자 하드웨어가 모델 아키텍처와 함께 발전함에 따라, 우리는 로컬에서 더 크고 더 나은 모델을 실행할 수 있게 될 것이며, 이는 더 고급 리버스 엔지니어링 작업을 로컬 모델의 범위 안에 두게 될 것입니다.
제가 작성한 'Fast AI Renamer' Ghidra 플러그인은 현재 분석하는 각 함수에 대한 요약을 작성하고, 작업 중인 프로젝트의 C 및 어셈블리 목록을 대량 내보낼 수 있습니다. 각 함수 요약에 대한 임베딩을 계산한 다음 이를 벡터 데이터베이스 또는 SQLite에 인덱싱하고, 데이터베이스를 질의하여 자연어 쿼리에 따라 함수를 찾을 수 있는 도구를 작성하고 싶습니다.
Ghidra 플러그인은 현재 이름 바꾸기를 위해 다중 턴 대화 없이 간단한 워크플로우를 사용하며, AI 출력에서 CSV를 파싱하여 작업을 수행합니다. 작은 AI 모델이 에이전트 워크플로우에 내재된 긴 대화와 여러 도구 호출을 처리할 수 없을 것이라고 생각했기 때문에 이렇게 구현했습니다. 제가 틀렸을 수도 있으며, AI를 프롬프팅하는 다른 방법을 탐색해야 합니다. 그럼에도 작은 로컬 모델의 경우 컨텍스트 윈도우 크기를 최대한 제한하는 것이 최선이라고 생각합니다.
Ghidra 플러그인이 프롬프트 인젝션에 얼마나 취약한지 확신할 수 없으며, 보안 전문가들은 악성 코드나 적대적인 바이너리를 분석하는 데 이를 사용할 때 주의해야 합니다. 일반적으로 이는 로컬호스트 인증되지 않은 MCP 서버를 가지고 있거나 코드 실행을 달성할 수 있는 민감한 함수에 사용할 수 있는 도구가 있는 모든 AI 기반 리버스 엔지니어링 도구에 적용됩니다. 주의하세요.
리버스 엔지니어링에 작은 범용 모델을 사용하고 있음을 감안할 때, 더 크고 지능적인 모델에서 증류된 대화로 미세 조정함으로써 성능이 얼마나 향상될 수 있을지 궁금합니다.
마지막으로, Ghidra 플러그인의 C/어셈블리 내보내기 기능은 Claude Code와 잘 작동하며, 크랙미의 마크업된 디컴파일을 제공하고 솔루션을 찾도록 지시하는 것만으로 Claude Code + Sonnet을 사용하여 처음에 올바른 솔루션을 작성할 수 있었습니다. Claude Code가 디컴파일러 출력의 텍스트 파일을 통해 바이너리를 분석하는 데 더 능숙해지도록, MCP 서버를 통해 Ghidra로 가서 분석을 수행하지 않도록 하는 스킬을 작성할 수 있을지 궁금합니다. 예를 들어, Claude Code에 스킬을 도입하여 바이너리의 가상 메모리에 있는 바이트를 검사할 수 있게 하여, 디스어셈블리/디컴파일러 목록에서 일부 데이터가 참조되는 것을 볼 때 Ghidra를 거치지 않고 해당 메모리에 무엇이 있는지 확인할 수 있도록 할 수 있습니다.
Kryptos님이 크랙미를 만들어 주시고, Crackmes.one에서 호스팅해 주셔서 감사합니다. crackmes.de가 중단된 곳을 이어받아 주셔서 감사합니다 (RIP).
놀랍고 멋진 로컬 모델을 출시해 주신 Google에 감사드립니다.
제 세금을 사용하여 멋진 리버스 엔지니어링 도구를 만든 NSA에 감사드리고, Ghidra Github에서 제공하는 모든 지원에 대해 Ryan Kurtz님께 감사드립니다.