기초 악성 코드 분석 2

1. 리틀 엔디언과 빅 엔디언

1.1 엔디언(Endian)이란?

  • 컴퓨터 메모리에 데이터를 저장할 때 바이트(Byte)의 순서를 정의하는 방식.

1.2 리틀 엔디언(Little Endian)

정의
  • 낮은 주소낮은 바이트(Least Significant Byte) 를 저장하는 방식.
  • 즉, 데이터의 하위 바이트부터 메모리에 저장.
예시

32비트 정수 값: 0x12345678을 메모리에 저장할 때:

메모리 주소
0x0078
0x0156
0x0234
0x0312

→ 실제 메모리: 78 56 34 12

사용 환경 예시
  • Intel x86/x64 아키텍처, ARM(기본 설정), Windows OS 등에서 사용.

1.3 빅 엔디언(Big Endian)

정의
  • 낮은 주소높은 바이트(Most Significant Byte) 를 저장하는 방식.
  • 즉, 데이터의 상위 바이트부터 메모리에 저장.
예시

32비트 정수 값: 0x12345678을 메모리에 저장할 때:

메모리 주소
0x0012
0x0134
0x0256
0x0378

→ 실제 메모리: 12 34 56 78

사용 환경 예시
  • 네트워크 프로토콜(TCP/IP), IBM 메인프레임, 일부 RISC 프로세서 등에서 사용.

1.4 리틀 엔디언과 빅 엔디언의 차이점 비교

구분리틀 엔디언 (Little Endian)빅 엔디언 (Big Endian)
데이터 저장 순서하위 바이트부터상위 바이트부터
메모리 가독성사람이 보기에는 덜 직관적사람이 보기 쉽고 직관적
사용 환경Intel, Windows, ARM 등네트워크 통신, IBM 등
장점연산 효율성(간단한 CPU 설계)네트워크 전송 시 직관적
단점데이터 가독성 낮음CPU 연산 설계 복잡성 증가
nb) 리틀 엔디언의 연산 효율이 높은 이유
  • 주소 증가 방향과 CPU 연산 방향 일치
    • 하위 바이트부터 처리 → 별도 처리 X
  • 캐리(Carry) 처리 간편
    • 낮은 자릿수 → 높은 자릿수 방향 일치 (산술연산 최적화)
  • CPU 내부 구조 최적화
    • 메모리 접근과 데이터 처리의 일관성 유지 (현대 프로세서의 설계 최적화)

1.5 악성코드 분석에서의 중요성

악성코드 분석 시, 특히 역공학(리버싱, Reversing) 과정에서 메모리 덤프나 바이너리 데이터 분석 시 반드시 엔디언 방식을 고려해야 함.

  • PE파일(Windows 실행 파일)은 리틀 엔디언 사용.
  • 네트워크 패킷 분석 시 프로토콜에 따라 빅 엔디언을 사용하는 경우도 많음.


2. 윈도우 메모리 구조

윈도우 운영체제의 메모리 구조는 크게 사용자 모드(User Mode)커널 모드(Kernel Mode)로 나눠짐.

악성코드 분석 시 코드 인젝션, 프로세스 할로잉, 루트킷 등 메모리 기반 공격의 이해에 필수적임.

2.1 사용자 모드(User Mode)

정의
  • 일반적인 응용 프로그램이 실행되는 영역.
  • 메모리에 직접 접근 불가 (커널 통해 접근).
특징
  • 프로세스 간 독립된 메모리 공간 가짐.
  • 접근 위반 발생 시 응용 프로그램 충돌, 운영체제는 무사.
구성 요소
  • Stack (스택): 지역변수, 함수 호출 관리
  • Heap (힙): 동적 할당 메모리
  • Code (코드): 실행 가능한 프로그램 명령어
  • Data (데이터): 정적 데이터, 글로벌 변수 등
악성코드 분석 포인트
  • DLL Injection, Process Hollowing 분석
  • Heap Spray(힙 스프레이) 공격 탐지
  • 메모리 덤프 분석 및 리버싱 대상

2.2 커널 모드(Kernel Mode)

정의
  • 운영체제 커널, 하드웨어 제어 담당 영역.
  • 시스템 권한으로 메모리 전체 접근 가능.
특징
  • 하드웨어 직접 접근 가능
  • 시스템 서비스 제공 (파일, 네트워크, 프로세스 관리 등)
구성 요소
  • 페이징(Paging) 및 메모리 관리 시스템
  • 디바이스 드라이버(Device Drivers)
  • HAL(Hardware Abstraction Layer, 하드웨어 추상 계층)
악성코드 분석 포인트
  • 루트킷(Rootkit), 커널 익스플로잇 탐지
  • 커널 후킹(Kernel Hooking) 분석
  • 시스템 콜 후킹, 디바이스 드라이버 악성코드 탐지
💡
  • 사용자 모드 공격: 프로세스 인젝션, 메모리 변조.
  • 커널 모드 공격: 루트킷, 드라이버 기반 악성 행위.

2.3 메모리 주소 공간 구성

+---------------------+ 0xFFFFFFFF | 커널 공간 | +---------------------+ 0x80000000 (2GB) | 사용자 공간 | +---------------------+ 0x00000000
32비트 기준


+----------------------------+ 0xFFFF'FFFFFFFF'FFFF | 커널 공간 | | | +----------------------------+ 0xFFFF'80000000'0000 | 미사용 공간 | +----------------------------+ 0x0000'7FFFFFFF'FFFF | 사용자 공간 | | | +----------------------------+ 0x0000'00000000'0000
64비트 기준 / 일반적인 윈도우 설정
  • 악성코드 분석에서의 차이
    분석 항목32비트64비트
    메모리 주소 분석간단함 (낮은 주소 범위)주소 범위 큼 → 분석 어려움
    루트킷 분석제한적64비트는 드라이버 서명, PatchGuard로 보안 ↑
    디버깅 도구32비트 전용64비트 환경 별도 도구 필요


3 함수 호출 규약

3.1 함수 호출 규약이란

  • 함수 호출 시 인자 전달 방식, 스택 정리 책임, 레지스터 사용 규칙 등을 정의한 약속.
  • 주로 사용되는 호출 규약: cdecl, stdcall, fastcall, thiscall, SysV, Microsoft x64, etc.

3.2 32비트 함수 호출 규약 (x86)

  • 인자를 대부분 stack에 넣어서 전달함.
cdecl (C Declaration)
  • 인자 전달: 오른쪽 → 왼쪽 순으로 스택에 push
  • 스택 정리: 호출한 쪽(caller)이 스택 정리
  • 예시:
int sum(int a, int b); // b 먼저 push, 그 다음 a
  • 리버싱에서 가장 일반적인 규약 (특히 C 언어)
stdcall
  • 인자 전달: 스택
  • 스택 정리: 호출당한 함수(callee)가 정리
  • 주로 Windows API에서 사용
fastcall
  • 인자 전달: 일부 인자를 레지스터(ECX, EDX)로 전달, 나머지는 스택
  • 속도 개선 목적

3.3 64비트 함수 호출 규약 (x86-64)

  • 스택보다 레지스터 기반 인자 전달을 우선시함.
Microsoft x64 규약 (Windows 64비트)
  • 첫 4개의 인자 → 레지스터로 전달:

    RCX, RDX, R8, R9

  • 나머지 인자 → 스택
  • 스택 정리: 호출자
  • 스택 프레임 정렬: 16바이트 정렬
  • 예시:
void foo(int a, int b, int c, int d, int e); → RCX=a, RDX=b, R8=c, R9=d, [stack]=e
System V AMD64 규약 (리눅스/유닉스 계열)
  • 정수 인자 레지스터 순서: RDI, RSI, RDX, RCX, R8, R9
  • 나머지 인자 → 스택
  • 스택 정리: 호출자
  • 리눅스 기반 바이너리 분석 시 참고
💡

정리 !

<32비트 호출 규약>

규약인자 전달스택 정리 주체특징
cdecl스택 (Right → Left)호출자C 함수 기본
stdcall스택피호출자WinAPI
fastcall레지스터 + 스택피호출자속도 ↑

<64비트 호출 규약>

OS규약(표준)인자 레지스터 순서추가 인자스택 정리
WindowsMicrosoft x64 calling conventionRCX, RDX, R8, R9스택호출자
LinuxSystem V AMD64 ABIRDI, RSI, RDX, RCX, R8, R9스택호출자

3.4 호출 규약을 통해 추정할 수 있는 것들

함수 인자의 개수와 위치
  • 스택이냐, 레지스터냐에 따라 인자 전달 방식이 다름.
  • 악성코드가 어떤 함수에 몇 개의 인자를 전달했는지 파악해야, 그 함수가 뭘 하는지 추정 가능함.

예:

push 4 push 2 call sub_401000

→ 스택 두 개 쌓고 함수 호출 → cdecl일 확률 높음

→ 인자 2개 전달 → sub_401000(a=2, b=4) 이런 함수일 수 있겠다(추정)


스택 정리 주체 확인 → 코드 흐름 파악에 도움
  • 호출한 쪽이 정리? (cdecl)
  • 호출된 함수가 정리? (stdcall, fastcall)
  • 함수 리턴 직후에 스택이 어떻게 변화하는지 이해해야 코드 흐름 정확히 따라갈 수 있음.

예:

call sub_401000 add esp, 8 ; // caller가 스택 정리 중 → cdecl임

API 후킹, 쉘코드 분석 시 필수
  • 악성코드가 CreateProcess, WriteFile, NtCreateThreadEx 같은 API를 후킹하거나 호출할 때,

    인자가 RCX, RDX, R8, R9에 들어가는지 확인해야

    → 함수가 실제 어떤 파라미터로 실행되는지 알 수 있음

예:

mov rcx, rax // lpApplicationName mov rdx, rsp+30h // lpCommandLine call CreateProcessW

→ RCX/RDX = 첫 두 인자 → Windows x64 호출 규약임 → API 인자 추정 가능


함수 경계 찾기 (함수 시그니처 파악)
  • 호출 규약을 알면 어떤 프롤로그(시작), 에필로그(끝) 패턴을 가지는지 예측할 수 있음.
  • 이를 통해 IDA나 Ghidra 같은 툴에서 함수 구간 제대로 잡히지 않을 때 수동 분석 가능.

예: push ebp → mov ebp, esp로 시작하면 cdecl이나 stdcall일 가능성 높음


행위 기반 탐지 우회 분석
  • 어떤 악성코드들은 표준 호출 규약을 어기고 호출하기도 함 → 후킹 우회
  • 예를 들어, 인자를 레지스터가 아니라 특정 메모리 주소에 써놓고 함수 호출 → 이걸 이상 징후로 탐지할 수 있어야 함.


💡

함수를 분석할 때, 그 함수가 무슨 역할을 하고, 무슨 데이터를 가지고, 어디에 영향을 주는지를 이해하려면 호출 규약을 알아야 함

즉, 호출 규약은 함수 분석의 기본 프레임이고,

그걸 알아야 악성 행위의 흐름을 정확하게 추적 → 해석 → 대응할 수 있음


4. 실습

4.1 rev-basic-0

해당 문제는 input에 해당하는 string을 알맞게 넣으면 correct, 틀리면 wrong이 출력됨
해당 문제는 input에 해당하는 string을 알맞게 넣으면 correct, 틀리면 wrong이 출력됨
#1. 바이너리에 포함된 문자열 조회
#1. 바이너리에 포함된 문자열 조회
#2. 상호 참조를 통해 해당 문자열이 어떤 함수에서 참조되었는지 확인
#2. 상호 참조를 통해 해당 문자열이 어떤 함수에서 참조되었는지 확인
#3. Correct 문자열을 사용하는 곳을 디컴파일함
#3. Correct 문자열을 사용하는 곳을 디컴파일함
#4. sub_140001000을 분석해, flag 획득
#4. sub_140001000을 분석해, flag 획득
notion image
💡

strcmp() 에서 같은 string이면 ‘0’이 return 되어야 하는데 왜 인자가 같으면 ‘1’을 반환한다고 했을까?

의미
0보다 작음string1이 string2보다 작음
0string1이 string2와 같음
0보다 큼string1이 string2보다 큼

>> return strcmp(같으면 0 반환함) == 0 ; // 두 string이 같으면 함수 전체적으로 ‘1’을 return 함

>> c언어에서는 strcmp()==0 패턴이 표준처럼 자주 쓰인다고 함

#5. 굿
#5. 굿

4.2 rev-basic-1

#1. 단축키 n을 통해 분석한 함수의 이름을 바꿔 가독성을 높이기도 함.
#1. 단축키 n을 통해 분석한 함수의 이름을 바꿔 가독성을 높이기도 함.
#2. sub_140001000 함수를 디컴파일 한 후 분석
#2. sub_140001000 함수를 디컴파일 한 후 분석
#3. ASCII 코드임을 유추하고 번역, 단 이때 IDA 내에서 단축키 r을 사용하면 숫자를 문자로 변환해줌.
#3. ASCII 코드임을 유추하고 번역, 단 이때 IDA 내에서 단축키 r을 사용하면 숫자를 문자로 변환해줌.
#4. flag 획득
#4. flag 획득

4.3 rev-basic-3

#1. main 함수 분석
#1. main 함수 분석
라고 합니다.
라고 합니다.
#2. strcmp로 추정되는 함수도 분석
#2. strcmp로 추정되는 함수도 분석
  • byte[0] = *(a1+0) + 2*0 …
  • byte[0] == a1[0] 인지 묻는 작업 … ?
  • 즉 byte_140003000[0..22] 의 배열 정보와 입력된 값이 일치해야 함
#3. byte_140003000의 주소로 가 저장된 배열을 확인해서 추출
#3. byte_140003000의 주소로 가 저장된 배열을 확인해서 추출
#4. 역연산 과정을 통해 flag 획득
#4. 역연산 과정을 통해 flag 획득
💡

XOR 역연산

  • XOR는 비트를 비교해서 다르면 1, 같으면 0
data[i] = (a1[i] * i + 2 * i) = i * (a1[i] + 2)
원래 코드
a1[i] = (data[i] - 2 * i) / i
역방향 연산

나눗셈을 포함한 식이기 때문에 파이썬에서는 오버플로우 보정을 해주어야 함.

for i in range(24): tmp = data[i] - 2 * i tmp = tmp % 256 # 오버플로우 보정 (unsigned 8-bit) a1_i = tmp ^ i # XOR 역연산 result.append(a1_i)

XOR는 한번 더 XOR 하면 원래대로 돌아가기에 역연산에 유리 !

A ^ B = C → C ^ B = A → C ^ A = B
#5. 문자열 길이 23 !
#5. 문자열 길이 23 !
💡

역연산에 자주 등장하는 구조들

유형예시역연산 방식
직접 비교if (a[i] != 100)ASCII 해석
XORa[i] ^ 0xAB == b[i]다시 XOR
덧셈/뺄셈a[i] + 3 == b[i]a[i] = b[i] - 3
로테이션 (bit rotation)ROR(a[i], 3)ROL(b[i], 3)
해시 기반 (SHA1 등)hash(input) == target역연산 어려움, 브루트포스 필요
암호화 (AES 등)암호 알고리즘에 의한 비교키 없이는 거의 불가능, 공격 필요


참고 자료