바이너리 디프 (Binary Diff)
ACTION REQUIRED (읽은 후 즉시 실행)
NOW:../field-journal/precedent-reverse.md읽기 — 본 스킬의 작업이 승인된 통상 작업인지 확인NOW: 현재 태스크가 본 스킬의 적용 범위에 해당하는지 확인NEXT:../tool-index.md읽기, 도구 가용성 및 실제 경로 확인NEXT: 도구가 없으면 bootstrap 호출, 경로를 추측하지 말 것ACT: "워크플로우" 첫 단계로 진입하여 실행, 확인 상태에 머물지 말 것
적용 범위
다음 시나리오에 해당하는 태스크에서 본 스킬을 사용합니다.
- 커널/드라이버에 PDB 누락 — 이전 버전 ntoskrnl.exe의 기호는 있고, 새 버전 PDB가 Microsoft에서 내려간 경우, 이전 버전 기호로 새 버전의 비-내보내기 함수 주소를 유도해야 함
- 프로그램 업데이트 후 기호 마이그레이션 — 이전에 리버스 엔지니어링한 프로그램이 업데이트되어, 전체를 다시 리버스하지 않고 이전 결과를 일괄 마이그레이션하려는 경우
- 보호 메커니즘 업데이트 — 이전 버전에 완전한 리버스 결과가 있고, 새 버전에서 동일 함수의 새 오프셋을 빠르게 찾아야 하는 경우
- "이전 버전 기호 + 새 버전 무기호"의 모든 바이너리 비교 시나리오
다른 스킬과의 분업
| 시나리오 | 사용할 스킬 |
|---|---|
| 처음부터 바이너리 리버스 | ida-reverse/ 또는 radare2/ |
| 이전 결과를 새 버전으로 마이그레이션 | 본 스킬 |
| 완전히 다른 두 바이너리 비교 | BinDiff / Diaphora (전통 도구) |
핵심 장점
전통적인 방식과 비교:
| 방식 | 200개 함수 비용 | 시간 | 정확도 |
|---|---|---|---|
| 수동으로 IDA 두 개 열고 비교 | 무료지만 수명 소모 | 수 시간 | 높음 |
| BinDiff 자동 매칭 | 무료 | 빠름 | 중간 (구조 변화 시 실패) |
| 전적으로 Agent(CC/Codex)에 위임 | 50-100위안 | 느림 | 높음 |
| 본 스킬 (LLM 일괄 비교) | ~1위안 | ~10초/함수 | 높음 |
핵심 원리
이전 버전 함수 (기호 있음) 새 버전 동일 함수 (기호 없음)
↓ ↓
역어셈블리 + 의사 코드 내보내기 역어셈블리 + 의사 코드 내보내기
↓ ↓
└──────── LLM 구조화 비교 ────────┘
↓
YAML 출력 (기호 매핑 테이블)
↓
프로그램적 파싱 → 새 IDB에 일괄 적용
핵심 포인트:
- 프롬프트는 고정 템플릿이며 프로그램적으로 채워짐
- 입출력 형식이 정해져 있어 프로그램적으로 파싱 가능
- LLM은 "두 코드 조각을 보고 대응 관계를 찾는" 이 단계만 담당
- 시간 비용과 토큰 비용이 극히 낮음
프롬프트 템플릿
표준 비교 프롬프트
I have disassembly outputs and procedure code of the same function.
This is the function for reference:
**Disassembly for Reference**
```c
{disasm_for_reference}
Procedure code for Reference
{procedure_for_reference}
This is the function you need to reverse-engineering:
Disassembly to reverse-engineering
{disasm_code}
Procedure code to reverse-engineering
{procedure}
What you need to do is to collect all references to "{symbol_name_list}" in the function you need to reverse-engineering and output those references as YAML.
Example:
found_vcall: # This is for indirect call to virtual function or virtual function pointer fetching.
- insn_va: '0x180777700' # Always be the instruction with displacement offset
insn_disasm: call [rax+68h] # Always be the instruction with displacement offset
vfunc_offset: '0x68'
func_name: ILoopMode_OnLoopActivate
- insn_va: '0x180777778' # Always be the instruction with displacement offset
insn_disasm: mov rax, [rax+80h] # Always be the instruction with displacement offset
vfunc_offset: '0x80'
func_name: INetworkMessages_GetNetworkGroupCount
found_call: # This is for direct call to non-virtual regular function.
- insn_va: '0x180888800'
insn_disasm: call sub_180999900
func_name: CLoopMode_RegisterEventMapInternal
- insn_va: '0x180888880'
insn_disasm: call sub_180555500
func_name: CLoopMode_SetSystemState
found_funcptr: # This is for non-virtual regular function pointer.
- insn_va: '0x180666600' # Must load/reference the function pointer target address
insn_disasm: lea rdx, sub_15BC910 # Must load/reference the function pointer target address
funcptr_name: CLoopMode_OnClientPollNetworking
found_gv: # This is for reference to global variable.
- insn_va: '0x180444400'
insn_disasm: mov rcx, cs:qword_180666600 # Must load/reference the global variable
gv_name: g_pNetworkMessages
- insn_va: '0x180333300'
insn_disasm: lea rax, unk_180222200 # Must load/reference the global variable
gv_name: s_EventManager
found_struct_offset: # This is for reference to struct offset. NOTE THAT virtual function pointer should not be here! virtual function pointer should ALWAYS be in found_vcall !
- insn_va: '0x1801BA12A' # Always be the instruction with displacement offset
insn_disasm: mov rcx, [r14+58h] # Always be the instruction with displacement offset
offset: '0x58'
size: 8
struct_name: CResourceService
member_name: m_pEntitySystem
If nothing found, output an empty YAML. DO NOT output anything other than the desired YAML. DO NOT collect unrelated symbols.
### 변수 설명
| 변수 | 출처 | 설명 |
|------|------|------|
| `{disasm_for_reference}` | 이전 버전 IDA 내보내기 | 기호가 있는 역어셈블리 |
| `{procedure_for_reference}` | 이전 버전 IDA 내보내기 | 기호가 있는 의사 코드 |
| `{disasm_code}` | 새 버전 IDA 내보내기 | 기호가 없는 역어셈블리 |
| `{procedure}` | 새 버전 IDA 내보내기 | 기호가 없는 의사 코드 |
| `{symbol_name_list}` | 이전 버전에서 추출 | 새 버전에서 찾아야 할 기호 목록 |
## 워크플로우
### 전체 프로세스
```text
Step 1: 데이터 준비
- 이전 버전 바이너리를 IDA에 로드 (PDB/기호 있음)
- 새 버전 바이너리를 IDA에 로드 (기호 없음)
- 두 버전에서 동일한 앵커 함수 찾기 (내보내기 함수, 문자열 참조 등)
Step 2: 일괄 내보내기
- 이전 버전에서 내보내기: 앵커 함수의 역어셈블리 + 의사 코드 (기호 이름 포함)
- 새 버전에서 내보내기: 동일 앵커 함수의 역어셈블리 + 의사 코드 (기호 이름 없음)
Step 3: LLM 비교
- 프롬프트 템플릿에 데이터 채우기
- LLM API 호출 (권장: deepseek는 저렴, 초대형 함수는 gpt 사용)
- 반환된 YAML 파싱
Step 4: 결과 적용
- YAML의 기호 매핑을 새 IDB에 일괄 적용
- idapro_rename 또는 IDAPython 스크립트를 사용하여 일괄 이름 변경
Step 5: 반복
- 1차 마이그레이션된 함수가 새로운 앵커가 됨
- 이 함수들로 진입하여 내부 호출을 계속 비교
- 모든 대상 함수를 커버할 때까지 반복
앵커 선택 전략
| 앵커 유형 | 신뢰도 | 설명 |
|---|---|---|
| 내보내기 함수 | 가장 높음 | 이름은 변하지 않음, 주소는 변할 수 있음 |
| 문자열 참조 | 높음 | 문자열 내용은 변하지 않음, 참조 위치는 변할 수 있음 |
| 상수/매직 넘버 | 중간 | 특징값은 변하지 않음 |
| 코드 패턴 | 중간 | 함수 구조는 유사하나 주소는 모두 변함 |
일괄 처리 제안
- 한 번에 1개 함수만 비교 (컨텍스트 폭발 방지)
- 중간 함수 (<200줄)는 deepseek 사용
- 초대형 함수 (>500줄)는 gpt-4o 또는 claude 사용
- 동시 호출로 속도 향상 (10-20 동시)
- 결과 캐싱하여 중복 호출 방지
출력 형식
YAML 출력의 5가지 기호 유형
| 유형 | 의미 | 필드 |
|---|---|---|
found_vcall | 가상 함수 호출 (간접 call) | vfunc_offset, func_name |
found_call | 직접 함수 호출 | insn_va, func_name |
found_funcptr | 함수 포인터 참조 | insn_va, funcptr_name |
found_gv | 전역 변수 참조 | insn_va, gv_name |
found_struct_offset | 구조체 오프셋 참조 | offset, struct_name, member_name |
파싱 후 적용 동작
found_call → idapro_rename(addr=call_target, name=func_name)
found_vcall → idapro_set_comments(addr=insn_va, comment="vcall: {func_name} @ +{offset}")
found_funcptr → idapro_rename(addr=funcptr_target, name=funcptr_name)
found_gv → idapro_rename(addr=gv_addr, name=gv_name)
found_struct_offset → idapro_set_comments(addr=insn_va, comment="{struct_name}.{member_name}")
대표적인 사용 예시
시나리오 1: ntoskrnl.exe에 PDB 누락
보유: ntoskrnl.exe 10.0.26100.2000 + 완전한 PDB
목표: ntoskrnl.exe 10.0.26100.2605 (PDB가 내려감)
필요: PspSetCreateProcessNotifyRoutine의 새 주소 찾기
단계:
1. 두 버전을 모두 IDA에 로드
2. 내보내기 함수 PsSetCreateProcessNotifyRoutine 찾기 (두 버전 모두 있음)
3. 이전 버전에서 PspSetCreateProcessNotifyRoutine을 호출 (기호 있음)
4. 새 버전에서 sub_140822108을 호출 (기호 없음)
5. LLM이 즉시 식별: sub_140822108 = PspSetCreateProcessNotifyRoutine
6. 일괄 적용
시나리오 2: 애플리케이션 업데이트 후 마이그레이션
보유: target.exe v1.0의 완전한 리버스 결과 (200개 이상 함수 명명됨)
목표: target.exe v1.1 (모든 기호 유실)
필요: 200개 함수 이름 일괄 마이그레이션
단계:
1. 이전 버전에서 명명된 모든 함수의 역어셈블리+의사 코드 내보내기
2. 새 버전에서 내보내기 함수/문자열을 통해 해당 앵커 찾기
3. LLM 비교 일괄 호출
4. YAML 파싱, 일괄 rename
5. 반복 심화
LLM 선택 제안
| 모델 | 적합한 시나리오 | 비용 | 속도 |
|---|---|---|---|
| DeepSeek V3 | 중소 함수 (<200줄), 일괄 처리 | 매우 낮음 | 빠름 |
| GPT-4o | 초대형 함수, 복잡한 제어 흐름 | 중간 | 빠름 |
| Claude Sonnet | 중대형 함수, 추론 필요 | 중간 | 빠름 |
| Claude Opus | 매우 복잡한 함수, 심층 이해 필요 | 높음 | 느림 |
권장 전략: 기본값 DeepSeek, 컨텍스트 초과 또는 결과 부정확 시 자동 업그레이드.
주의사항
- 전체 바이너리를 LLM에 던지지 말 것 — 한 번에 하나의 함수만 비교
- 앵커는 반드시 신뢰할 수 있어야 함 — 앵커 자체가 잘못 정렬되면 이후 모두 헛수고
- 결과는 인간이 샘플 검사 필요 — LLM이 100% 정확하지 않으므로 핵심 기호는 검증 필수
- 중간 결과 캐싱 — 중복 호출로 토큰 낭비 방지
- 컨텍스트 제한 주의 — 초대형 함수 (>1000줄 역어셈블리)는 분할하거나 큰 컨텍스트 모델 사용
온디맨드 부트스트랩 (On-Demand Bootstrap)
도구 의존성
| 도구 | 용도 | 자동 설치 가능 |
|---|---|---|
| IDA Pro | 역어셈블리/의사 코드 내보내기 | ✗ (상용 소프트웨어) |
| Python | 스크립트 실행, API 호출 | ✓ |
| PyYAML | LLM이 반환한 YAML 파싱 | ✓ (pip install pyyaml) |
| LLM API | 비교 수행 | API 키 필요 |
설명
본 스킬의 핵심은 무거운 도구 설치에 의존하지 않으며, 주로 다음에 의존합니다:
- IDA Pro 보유 (
ida-reverse/스킬로 관리) - Python + requests/httpx (API 호출)
- LLM API 엔드포인트
라우팅 컨텍스트
상위 진입점: skills/SKILL.md (총괄), routing.md
트리거 조건: 이전 버전의 기호/리버스 결과가 있고, 새 버전으로 마이그레이션이 필요한 경우
하위 진출점:
- 바이너리 열기 필요 →
ida-reverse/ - 빠른 정찰로 버전 차이 확인 필요 →
radare2/
동급 관련 모듈: ida-reverse/ (데이터 내보내기 및 기호 적용 모두 IDA를 통해 수행)
태스크 완료 자가 점검 (완료 선언 전 MUST 통과)
- 워크플로우의 각 단계를 (읽기만 하지 않고) 실행했습니까?
-
tool-index를 기반으로 실제 도구 경로를 사용했습니까? - 재현 가능한 증거(명령어/스크립트/스크린샷/리포트)를 생성했습니까?
- RULES에서 요구하는 체크리스트 항목을 완료하고 기록했습니까?