[Malware] 어셈블리와 리버스 엔지니어링
Assembly와 레지스터, 함수 호출, XREF와 Control Flow를 연결해 코드의 의미를 해석하는 방법을 정리합니다.
코드 분석 — 역공학
소스코드가 없는 Binary를 분석해 프로그램의 구조와 내부 동작을 파악하는 과정입니다.
- 분석 대상: 프로그램 구조, Function, Control Flow, API Call, Data, Algorithm / Hidden Behaviour
- 분석 방법: 코드에서 근거를 찾고 가설을 세운 뒤 교차 검증
- 주요 과제: 난독화와 Anti-analysis 기법 식별·우회, 숨겨진 기능 확인
분석 흐름은 Binary → Assembly → Function → Control Flow → Behaviour로 이어집니다.
각 명령어의 의미를 읽는 것에서 시작해 함수의 역할과 실행 조건을 해석합니다.
역공학이 필요한 이유
Sandbox에서 C2 통신이 보이지 않아도 통신 기능이 없다고 단정할 수는 없습니다.
- 실행 조건 미충족: 특정 행위를 시작하는 조건이 충족되지 않았을 수 있음
- C2 Offline: 통신 기능은 있어도 서버가 응답하지 않을 수 있음
- Configuration 암호화: 통신 주소나 설정이 암호화되어 바로 드러나지 않을 수 있음
- Anti-VM / Anti-Sandbox: 분석 환경을 감지해 실행을 중단할 수 있음
- 숨겨진 기능 확인: 문자열이나 관찰된 행위만으로 드러나지 않는 기능이 있을 수 있음
- API 호출 목적 확인: 특정 API를 사용한 이유와 호출 전후 흐름을 코드에서 확인해야 함
역공학에서는 코드를 읽어 기능의 존재와 실행 조건을 확인합니다.
코드에 있는 기능과 이번 실행에서 관찰한 행위를 구분해야 합니다.
Source Code → Machine Code
컴파일은 Source Code를 실행 가능한 Machine Code로 변환하는 과정입니다.
역공학에서는 Machine Code를 읽을 수 있는 형태로 바꿔 프로그램의 동작을 해석합니다.
- 컴파일·실행 흐름
- Source Code → Machine Code → CPU에서 실행
- 분석 흐름
- Machine Code → Disassembly → Decompiled Code → Analyst Interpretation
Disassembly는 기계어를 Assembly 명령어로 보여줍니다.
Decompiled Code는 분석하기 쉽도록 고수준 언어 형태로 재구성한 결과입니다.
분석은 원래 Source Code를 그대로 복원하는 과정이 아닙니다.
도구가 보여주는 코드와 실행 흐름을 근거로 분석가가 의미를 판단해야 합니다.
컴파일 과정 참고: Clang — Assembling a Complete Toolchain
디스어셈블러 vs 디컴파일러
두 도구는 Machine Code를 분석가가 읽을 수 있는 형태로 보여줍니다.
차이는 결과를 표현하는 수준에 있습니다.
| 구분 | 디스어셈블러 | 디컴파일러 |
|---|---|---|
| Input | Machine Code | Machine Code |
| Output | Assembly | C-like Pseudocode |
| 표현 | 기계어 명령어에 가까움 | 고수준 언어로 재구성한 표현 |
| 장점 | 개별 명령어와 분기 확인 | 전체 Logic을 빠르게 이해 |
| 한계 | 읽고 해석하기 어려움 | 원래 Source Code가 아님 |
Decompiled Code ≠ Original Source Code
디컴파일 결과는 Logic을 이해하는 데 도움이 됩니다.
다만 읽기 쉽다는 이유로 원본 코드와 같다고 판단해서는 안 됩니다.
해석이 불확실한 부분은 Assembly와 대조합니다.
리버스 엔지니어링 필수 핵심 요소
Assembly를 읽으려면 값이 저장되는 위치와 명령어의 실행 흐름을 함께 봐야 합니다.
- 레지스터: CPU 내부에서 연산에 필요한 값이나 주소를 저장
- 메모리: 프로그램의 코드와 데이터를 저장
- 스택: 함수 호출 정보와 임시 데이터 등을 저장하는 메모리 영역
- 명령어: 데이터 이동, 연산, 함수 호출, 분기 등의 동작을 지시
- 플래그: 비교·연산 결과 등의 상태를 기록. 조건 분기에서 사용
- Control Flow: 조건과 분기에 따라 명령어가 실행되는 흐름
스택은 메모리의 일부입니다.
레지스터·메모리·스택에서 값이 어떻게 이동하는지 따라가면 명령어와 함수의 역할을 해석할 수 있습니다.
레지스터
수업에서 다룬 x86의 32-bit 레지스터와 x64의 64-bit 레지스터를 비교합니다.
R8과 R9는 x64에 추가된 레지스터입니다.
| x86 | x64 | 역할 |
|---|---|---|
| EAX | RAX | 연산 및 반환값 |
| EBX | RBX | 범용 Register |
| ECX | RCX | 범용 Register |
| EDX | RDX | 범용 Register |
| — | R8 | 범용 Register |
| — | R9 | 범용 Register |
| ESP | RSP | Stack Pointer |
| EBP | RBP | 함수의 Stack Frame 기준점으로 사용 가능 |
| EIP | RIP | Instruction Pointer. 다음 실행 명령어의 주소 |
x64에서도 EAX 등의 이름을 사용합니다.
EAX는 RAX의 하위 32-bit를 가리킵니다.
Windows x64에서는 1~4번째 Argument가 정수·포인터일 때 RCX·RDX·R8·R9를 각각 사용합니다.
함수 호출을 분석할 때는 이 레지스터에 전달된 값도 확인해야 합니다.
참고: Microsoft — x64 Architecture
인자 전달 참고: Microsoft — x64 Calling Convention
어셈블리 명령어
수업에서 다룬 기본 명령어와 역할입니다.
| 명령어 | 역할 |
|---|---|
mov | 데이터 이동 |
lea | 주소 계산 |
push | Stack에 값 저장 |
pop | Stack에서 값 꺼내기 |
call | 함수 호출 |
cmp | 값 비교 |
test | 비트 검사 |
jmp | 무조건 분기 |
je / jne | 조건 분기 |
ret | 함수에서 복귀 |
cmp는 뺄셈 결과로 값을 비교합니다.
test는 AND 결과로 비트를 검사합니다.
두 명령어는 연산 결과를 피연산자에 저장하지 않고 플래그에 반영합니다.
je는 Zero Flag가 1일 때, jne는 0일 때 분기합니다.
함수 호출 — Function Calls
여러 API의 조합과 호출 순서를 함께 확인해야 합니다.
Function 안에서 호출하는 API는 함수의 목적을 추론하는 단서입니다.
| API | 확인할 동작 |
|---|---|
CreateFileW | 파일 생성 또는 열기 |
WriteFile | 데이터 쓰기 |
RegSetValueExW | Registry 값 생성 또는 수정 |
InternetConnectW | 외부 서버 연결 준비 |
API ≠ Behaviour
CreateFileW는 Configuration File, Dropped Payload, Log File이나 일반적인 임시 파일에 접근할 때도 사용됩니다.
API 이름만으로 악성 행위를 확정할 수는 없습니다.
- 어떤 File Path를 사용하는가?
- 어떤 Data를 Write하는가?
- 어떤 Function에서 호출되는가?
- 호출 전후에 어떤 API가 있는가?
- 이후 Process 실행이나 Registry 변경으로 이어지는가?
API Name → Capability Hypothesis
API + Context → Behaviour Interpretation
Function Call을 볼 때는 Argument에 전달되는 값도 확인합니다.
API가 할 수 있는 일과 해당 코드에서 사용한 목적을 구분해야 합니다.
리버스 엔지니어링 도구 — Ghidra
Ghidra는 NSA에서 공개한 리버스 엔지니어링 도구입니다.
하나의 Binary를 여러 표현으로 확인할 수 있습니다.
- Disassembler: Assembly 명령어 확인
- Decompiler: 고수준 Pseudocode로 Function 구조 확인
- String: 분석할 문자열과 사용 위치 확인
- Function Graph: Function 내부의 Control Flow 확인
Function Graph는 Window → Function Graph에서 확인합니다.
Assembly, Decompiled Code와 Graph를 함께 보면서 실행 경로를 해석합니다.
참고: Ghidra 공식 저장소
리버스 엔지니어링 도구 — x32dbg / x64dbg
x32dbg와 x64dbg는 Windows x86/x64 프로그램을 분석하는 디버거입니다.
- Binary의 Function과 Assembly 확인
- 실행 중 Register와 Stack 값 확인
- 조건 분기와 실행 경로 확인
코드에서 세운 해석을 실행 중 값과 흐름에 대조할 수 있습니다.
참고: x64dbg 공식 사이트
String
Interesting String은 코드 분석의 시작점이 될 수 있습니다.
String을 찾은 뒤에는 어디에서 사용되는지 확인해야 합니다.
Cross References — XREF
XREF로 String을 참조하는 Code와 관련 Function으로 이동합니다.
- Interesting String 확인
- XREF로 String을 사용하는 Code 위치 확인
- 관련 Function의 API Call과 Arguments 확인
- Code Context를 근거로 Behaviour Hypothesis 정리
분석 흐름: String → XREF → Code/Function → API Call → Arguments → Behaviour Hypothesis
코드에서 세운 Behaviour Hypothesis를 실제로 관찰한 Behaviour로 단정해서는 안 됩니다.
String 자체보다 그 String을 사용하는 Code Context가 중요합니다.
Control Flow
악성코드는 항상 모든 코드를 실행하지 않습니다.
조건을 검사한 뒤 결과에 따라 다른 Branch로 진행할 수 있습니다.
- 인터넷 사용 가능 여부
- 디버거 실행 여부
- VM으로 식별되는지
- Username 조건
- Language / Region 조건
- 이미 악성코드가 실행됐는지
분석에서는 어떤 Condition을 검사하는지 확인합니다.
Sandbox에서 행위가 보이지 않았다는 사실만으로 기능이 없다고 판단해서는 안 됩니다.
각 Branch에서 호출되는 Function을 따라가며 Condition → Branch → Behaviour를 연결합니다.
Loops
수업에서 다룬 코드입니다.
조건이 충족될 때까지 반복하는 흐름입니다.
loop:
call Sleep
call CheckServer
test eax, eax
je loop
Sleep: 일정 시간 대기CheckServer: 함수 호출 후 EAX 값 확인test eax, eax: EAX가 0인지 검사je loop: EAX가 0이면 다시 반복
Periodic C2 / Beaconing을 가설로 세울 수 있습니다.
다만 함수 이름과 반복 구조만으로 통신 행위를 확정할 수는 없습니다.
CheckServer 내부 코드와 Network / Dynamic Evidence를 추가로 확인해야 합니다.
중요한 점
코드에서 찾은 단서와 실제로 관찰한 행위를 구분해야 합니다.
- String ≠ Behaviour
- API ≠ Behaviour
- Decompiler Output ≠ Original Source Code
- Possible Behaviour ≠ Observed Behaviour
Context + Control Flow + Evidence = Interpretation
분석 결과를 설명할 때는 어떤 Evidence가 해석을 뒷받침하는지 확인합니다.
Decompiler의 한계
Decompiler는 Function 구조와 Condition, Loop를 빠르게 이해하는 데 도움이 됩니다.
다만 출력에는 재구성과 추론이 포함됩니다.
- Variable names reconstructed: 변수명은 분석 결과를 바탕으로 재구성
- Types inferred: 데이터 타입은 추론한 결과
- Optimisation: 최적화로 원래 Logic의 표현이 달라질 수 있음
- Obfuscation: 난독화 때문에 출력이 부정확하거나 이해하기 어려울 수 있음
해석이 불확실한 부분은 실제 Binary와 Assembly를 대조해 확인합니다.
최종 정리
- Reverse Engineering은 Binary 내부 Logic을 이해하는 과정입니다.
- Assembly는 명령어를 외우기보다 Evidence를 읽는 데 사용합니다.
- String과 API는 분석의 시작점입니다. 그 자체가 Behaviour는 아닙니다.
- XREF로 Evidence를 사용하는 Code와 관련 Function으로 이동합니다.
- Control Flow와 Arguments를 함께 확인해야 Function의 목적을 해석할 수 있습니다.
- Decompiler와 AI 결과는 분석가가 실제 Evidence와 대조해 검증해야 합니다.
분석 흐름: Binary → Evidence → Function → Logic → Behaviour → Verification
OXLOADER의 실행 조건과 CASTLESTEALER 전달 과정은 별도 글로 정리했습니다.