[Malware] 어셈블리와 리버스 엔지니어링

Assembly와 레지스터, 함수 호출, XREF와 Control Flow를 연결해 코드의 의미를 해석하는 방법을 정리합니다.

코드 분석 — 역공학

소스코드가 없는 Binary를 분석해 프로그램의 구조와 내부 동작을 파악하는 과정입니다.

분석 흐름은 Binary → Assembly → Function → Control Flow → Behaviour로 이어집니다.
각 명령어의 의미를 읽는 것에서 시작해 함수의 역할과 실행 조건을 해석합니다.

역공학이 필요한 이유

Sandbox에서 C2 통신이 보이지 않아도 통신 기능이 없다고 단정할 수는 없습니다.

역공학에서는 코드를 읽어 기능의 존재와 실행 조건을 확인합니다.
코드에 있는 기능과 이번 실행에서 관찰한 행위를 구분해야 합니다.

Source Code → Machine Code

컴파일은 Source Code를 실행 가능한 Machine Code로 변환하는 과정입니다.
역공학에서는 Machine Code를 읽을 수 있는 형태로 바꿔 프로그램의 동작을 해석합니다.

Disassembly는 기계어를 Assembly 명령어로 보여줍니다.
Decompiled Code는 분석하기 쉽도록 고수준 언어 형태로 재구성한 결과입니다.

분석은 원래 Source Code를 그대로 복원하는 과정이 아닙니다.
도구가 보여주는 코드와 실행 흐름을 근거로 분석가가 의미를 판단해야 합니다.

컴파일 과정 참고: Clang — Assembling a Complete Toolchain

디스어셈블러 vs 디컴파일러

두 도구는 Machine Code를 분석가가 읽을 수 있는 형태로 보여줍니다.
차이는 결과를 표현하는 수준에 있습니다.

구분디스어셈블러디컴파일러
InputMachine CodeMachine Code
OutputAssemblyC-like Pseudocode
표현기계어 명령어에 가까움고수준 언어로 재구성한 표현
장점개별 명령어와 분기 확인전체 Logic을 빠르게 이해
한계읽고 해석하기 어려움원래 Source Code가 아님

Decompiled Code ≠ Original Source Code

디컴파일 결과는 Logic을 이해하는 데 도움이 됩니다.
다만 읽기 쉽다는 이유로 원본 코드와 같다고 판단해서는 안 됩니다.
해석이 불확실한 부분은 Assembly와 대조합니다.

리버스 엔지니어링 필수 핵심 요소

Assembly를 읽으려면 값이 저장되는 위치와 명령어의 실행 흐름을 함께 봐야 합니다.

스택은 메모리의 일부입니다.
레지스터·메모리·스택에서 값이 어떻게 이동하는지 따라가면 명령어와 함수의 역할을 해석할 수 있습니다.

레지스터

수업에서 다룬 x86의 32-bit 레지스터와 x64의 64-bit 레지스터를 비교합니다.
R8과 R9는 x64에 추가된 레지스터입니다.

x86x64역할
EAXRAX연산 및 반환값
EBXRBX범용 Register
ECXRCX범용 Register
EDXRDX범용 Register
—R8범용 Register
—R9범용 Register
ESPRSPStack Pointer
EBPRBP함수의 Stack Frame 기준점으로 사용 가능
EIPRIPInstruction 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주소 계산
pushStack에 값 저장
popStack에서 값 꺼내기
call함수 호출
cmp값 비교
test비트 검사
jmp무조건 분기
je / jne조건 분기
ret함수에서 복귀

cmp는 뺄셈 결과로 값을 비교합니다.
test는 AND 결과로 비트를 검사합니다.
두 명령어는 연산 결과를 피연산자에 저장하지 않고 플래그에 반영합니다.
je는 Zero Flag가 1일 때, jne는 0일 때 분기합니다.

함수 호출 — Function Calls

여러 API의 조합과 호출 순서를 함께 확인해야 합니다.
Function 안에서 호출하는 API는 함수의 목적을 추론하는 단서입니다.

API확인할 동작
CreateFileW파일 생성 또는 열기
WriteFile데이터 쓰기
RegSetValueExWRegistry 값 생성 또는 수정
InternetConnectW외부 서버 연결 준비

API ≠ Behaviour

CreateFileW는 Configuration File, Dropped Payload, Log File이나 일반적인 임시 파일에 접근할 때도 사용됩니다.
API 이름만으로 악성 행위를 확정할 수는 없습니다.

API Name → Capability Hypothesis
API + Context → Behaviour Interpretation

Function Call을 볼 때는 Argument에 전달되는 값도 확인합니다.
API가 할 수 있는 일과 해당 코드에서 사용한 목적을 구분해야 합니다.

리버스 엔지니어링 도구 — Ghidra

Ghidra는 NSA에서 공개한 리버스 엔지니어링 도구입니다.
하나의 Binary를 여러 표현으로 확인할 수 있습니다.

Function Graph는 Window → Function Graph에서 확인합니다.
Assembly, Decompiled Code와 Graph를 함께 보면서 실행 경로를 해석합니다.

참고: Ghidra 공식 저장소

리버스 엔지니어링 도구 — x32dbg / x64dbg

x32dbg와 x64dbg는 Windows x86/x64 프로그램을 분석하는 디버거입니다.

코드에서 세운 해석을 실행 중 값과 흐름에 대조할 수 있습니다.

참고: x64dbg 공식 사이트

String

Interesting String은 코드 분석의 시작점이 될 수 있습니다.
String을 찾은 뒤에는 어디에서 사용되는지 확인해야 합니다.

Cross References — XREF

XREF로 String을 참조하는 Code와 관련 Function으로 이동합니다.

  1. Interesting String 확인
  2. XREF로 String을 사용하는 Code 위치 확인
  3. 관련 Function의 API Call과 Arguments 확인
  4. Code Context를 근거로 Behaviour Hypothesis 정리

분석 흐름: String → XREF → Code/Function → API Call → Arguments → Behaviour Hypothesis

코드에서 세운 Behaviour Hypothesis를 실제로 관찰한 Behaviour로 단정해서는 안 됩니다.
String 자체보다 그 String을 사용하는 Code Context가 중요합니다.

Control Flow

악성코드는 항상 모든 코드를 실행하지 않습니다.
조건을 검사한 뒤 결과에 따라 다른 Branch로 진행할 수 있습니다.

분석에서는 어떤 Condition을 검사하는지 확인합니다.
Sandbox에서 행위가 보이지 않았다는 사실만으로 기능이 없다고 판단해서는 안 됩니다.
각 Branch에서 호출되는 Function을 따라가며 Condition → Branch → Behaviour를 연결합니다.

Loops

수업에서 다룬 코드입니다.
조건이 충족될 때까지 반복하는 흐름입니다.

loop:
    call Sleep
    call CheckServer
    test eax, eax
    je loop

Periodic C2 / Beaconing을 가설로 세울 수 있습니다.
다만 함수 이름과 반복 구조만으로 통신 행위를 확정할 수는 없습니다.
CheckServer 내부 코드와 Network / Dynamic Evidence를 추가로 확인해야 합니다.

중요한 점

코드에서 찾은 단서와 실제로 관찰한 행위를 구분해야 합니다.

Context + Control Flow + Evidence = Interpretation

분석 결과를 설명할 때는 어떤 Evidence가 해석을 뒷받침하는지 확인합니다.

Decompiler의 한계

Decompiler는 Function 구조와 Condition, Loop를 빠르게 이해하는 데 도움이 됩니다.
다만 출력에는 재구성과 추론이 포함됩니다.

해석이 불확실한 부분은 실제 Binary와 Assembly를 대조해 확인합니다.

최종 정리

  1. Reverse Engineering은 Binary 내부 Logic을 이해하는 과정입니다.
  2. Assembly는 명령어를 외우기보다 Evidence를 읽는 데 사용합니다.
  3. String과 API는 분석의 시작점입니다. 그 자체가 Behaviour는 아닙니다.
  4. XREF로 Evidence를 사용하는 Code와 관련 Function으로 이동합니다.
  5. Control Flow와 Arguments를 함께 확인해야 Function의 목적을 해석할 수 있습니다.
  6. Decompiler와 AI 결과는 분석가가 실제 Evidence와 대조해 검증해야 합니다.

분석 흐름: Binary → Evidence → Function → Logic → Behaviour → Verification

OXLOADER의 실행 조건과 CASTLESTEALER 전달 과정은 별도 글로 정리했습니다.

참고 자료