이 글은 아래의 책을 읽고 공부 목적으로 작성됨.
몬나파 K A, 「악성코드 분석 시작하기」
컴퓨터 기초
컴퓨터 : 정보를 처리하는 머신.
컴퓨터의 모든 정보 : 비트로 표현.
비트(bit) : 0 또는 1을 갖는 독립 단위. 비트로 숫자, 문자, 기타 정보를 나타낼 수 있음.
바이트(byte) : 8비트 묶음. 단일 바이트는 16진수 2개로 나타낸다.
니블(nibble) : 4비트 묶음. 16진수 1개로 나타낸다.
- 워드(word) : 2바이트 묶음.
- 더블워드(dword) : 4바이트 묶음..data
- 쿼드 워드(qword) : 8바이트 묶음.
바이트 순서의 의미는 사용 방법에 따라 다르다.
메인 메모리(RAM) : 컴퓨터에서 코드와 데이터를 저장.
메인 메모리는 바이트의 배열이며, 각 바이트는 주소(address)라는 고유 번호가 부여된다. 첫 번째 주소는 0에서 시작하고, 주소와 값은 16진수로 표현한다.
데이터는 리틀 엔디안(little-endian) 포맷으로 메모리에 저장된다.
리틀 엔디안 : 하위 바이트가 하위 주소에 저장.
중앙 처리 유닛(CPU, Central Processing Unit) : 명령어를 실행.
명령어 실행 시 필요한 데이터는 메모리에서 가져온다. CPU 자체적으로 칩 내에 레지스터 집합(register set)이라는 작은 메모리 집합을 갖고 있다.
CPU에는실행할 수 있는 명령어 집합이 있다. 기계 명령어는 CPU가 가져오고 해석하고 실행하는 바이트 순서로 메모리에 저장된다.
실행 파일 컴파일 과정
- 고급 언어로 소스코드 작성.
- 컴파일러를 통해 소스코드 실행. 컴파일러가 고급 언어로 작성된 것을 목적 파일(object file) 또는 기계 코드(machine code)라 불리는 중간 형식으로 변환.
- 링커(linker)를 통해 목적 코드 전달. 링커는 목적 코드를 필요 라이브러리(DLL)와 연결해 실행할 수 있는 실행 파일로 만듦.
컴파일한 프로그램이 디스크에서 나타나는 방법 : 컴파일러가 섹션 5개(.text, .rdata, .data, .rsrc, .reloc) 생성. -> 이 섹션들을 살펴보자.
- .data : 데이터
- .rdata : 데이터, 읽기 전용 데이터 포함.
- .rsrc : 데이터, 실행 파일에서 사용하는 리소스 포함.
- .text : 기계 코드
실행 파일을 메모리에 로드할 때 일어나는 일
- 실행 파일 더블클릭 시, 프로세스 메모리를 운영 시스템에서 할당.
- 운영 시스템에서 할당한 메모리로 실행 파일을 로드. (메모리 주소가 물리 주소가 아니고 가상 주소. 가상 주소는 최종적으로 물리 메모리 주소로 변환.)
힙(heap) : 프로그램을 실행하는 동안 동적 메모리 할당을 위해 사용.
스택(stack) : 지역변수, 함수 인수, 반환 주소를 저장하는 데 사용.
프로그램 실행 시 수행되는 단계
- 프로그램을 메모리에 로드.
- CPU는 명령어를 가져와 디코딩하고 실행.
- CPU는 메모리에서 필요한 데이터를 가져옴.
- CPU는 필요하다면 입력/출력 시스템과 상호작용.
기계 코드는 프로그램의 내부 동작에 대한 정보를 포함한다. 바이트의 순서로 저장된 기계 코드를 사람이 이해하는 것은 어렵다.
-> 디스어셈블러/디버거(IDA Pro 또는 x64dbg)는 기계 코드를 하위 수준의 언어로 변환하는 프로그램이며, 변환된 명령어를 어셈블리 언어 명령어(assembly language instruction)라고 부른다.
코드 분석 관점에서 프로그램의 기능 파악은 어셈블리 명령어와 이를 해석하는 방법에 주로 의존.
CPU 레지스터
CPU는 메모리에 있는 데이터보다 레지스터에 있는 데이터에 훨씬 빠르게 접근할 수 있다. 따라서 실행을 위해 메모리에서 가져온 값을 레지스터에 임시로 저장한다.
x86 CPU는 범용 레지스터 8개(eax, ebx, ecx, edx, esp, dep, esi, edi)를 갖고 있다. 각 레지스터들의 크기는 32비트이다.
프로그램은 32비트, 16비트, 8비트로 레지스터에 접근할 수 있다.
- 하위 16비트 : ax, bx, cx, dx, sp, bp, si, di로 접근.
- 하위 8비트 : al, bl, cl, dl로 참조.
- 상위 8비트 : ah, bh, ch, dh로 접근.
예시 : eax레지스터는 4바이트 값 0xC6A93174를 포함한다. 하위 2바이트를 ax 레지스터로 접근, al 레지스터에 접근해 하위 바이트(0x74)에 접근할 수 있고, ah 레지스터에 접근해 다음 바이트(0x31에 접근할 수 있다.)
C6 A9 31 74(32bits) | EAX | |||
31 74(16bits) | AX | |||
31(8bits) 74(8bits) | AH, AL |
명령 포인터(EIP) : 실행할 다음 명령어의 다음 주소를 가지는 레지스터.
EFLAGS : 32비트 레지스터, 레지스터의 개별 비트가 플래그이다. eflags 레지스터의 비트는 계산 상태를 나타내고, CPU 동작을 제어할 때 사용한다. 계산 또는 조건 명령어를 실행하는 동안 플래그가 1 또는 0으로 설정된다.
세그먼트 레지스터(cs, ss, ds, es, fs, gs) : 이를 통해 메모리의 섹션을 추척한다.
데이터 전송 명령어
mov dst,src
mov 명령어 : 데이터를 한 위치에서 다른 위치로 옮긴다.
mov 명령어의 유형 (세미콜론(;)은 주석의 시작을 나타냄)
- 상수를 레지스터로 이동
mov eax,10 ; 10을 EAX 레지스터로 이동하며, eax=10과 동일.
mov bx,7 ; 7을 bx레지스터로 이동하며, bx=7과 동일.
mov eax,64h ; 16진수 0x64(=100)를 eax로 이동 - 레지스터에서 레지스터로 값 이동
mov eax,ebx ; ebx의 내용을 eax로 이동, eax=ebx와 동일. - 메모리의 값을 레지스터로 이동
메모리의 값을 레지스터로 옮기면 값의 주소를 사용해야 한다. 대괄호는 주소 자체가 아닌 메모리 위치에 저장된 값을 지정한다. 또한 대괄호는 레지스터, 레지스터에 추가하는 상수, 또는 레지스터에 추가되는 레지스터를 포함할 수 있다.
mov eax,[0x403000] ; eax는 주소 0x403000에서 저장하고 있는 '값'을 포함.
mov eax,[ebx] ; ebx 레지스터에 지정된 주소에 있는 값을 이동.
mob eax,[ebx+ecx] ; ebx+ecx에 지정된 주소에 있는 값을 이동.
mov ebx,[ebp-4] ; ebp-4에 지정된 주소에 있는 값을 이동.
+lea 명령어(load effective address) : 유효한 주소 로드하기. 값 대신 주소를 로드한다.
lea ebx,[0x403000] ; 0x403000 주소를 ebx로 로드.
lea eax,[ebx] ; 만약 ebx=0x403000이면, eax 역시 0x403000을 포함한다. - 레지스터에서 메모리로 값 이동
mov [0x403000],eax ; eax에 있는 4바이트 값을 0x40300에서 시작하는 메모리 위치로 옮긴다.
mov [ebx],eax ; eax에 있는 4바이트 값을 ebx에서 지정한 메모리 주소로 옮긴다.
산술 연산
더하기, 빼기, 곱하기, 나누기를 할 수 있다.
더하기는 add, 빼기는 sub 명령어를 이용해 수행한다. 이 명령어는 2개의 파연산자(목적지, 출발지)를 가진다.
- 더하기 : 출발지와 목적지를 더한 후 결과를 목적지에 저장한다.
add eax, 42 ; eax=eax+42
add eax, ebx ; eax=eax+ebx
add [ebx], 42 ; ebx가 지정하는 주소에 있는 값에 42를 더하기 - 빼기 : 목적지에서 풀발지를 빼고 결과를 목적지에 저장한다.
sub eax, 64h ; eax에서 16진수 0x64를 빼기 - 특수 증가와 감소 명령어
inc eax ; eax=eax+1
dec ebx ; ebx=ebx-1
곱하기는 mul, 나누기는 div 명령어를 통해 수행한다. 이 명령어는 1개의 피연산자만을 가진다.
- 곱하기 : 피연산자가 8비트면 8비트 al과 곱하고 결과를 ax레지스터에 저장. 피연산자가 16비트면 16비트 ax와 곱하고 dx와 ax 레지스터에 결과를 저장. 피연산자가 32비트면 eax와 곱하고 결과를 edx와 eax 레지스터에 저장한다. 이유는 2개의 값을 곱하면 결괏값이 입력값보다 훨씬 커질 수 있기 때문이다.
mul ebx ; ebx를 eax와 곱하고 결과는 EDX와 EAX에 저장.
mul bx ; bx를 ax와 곱하고 결과는 DX와 AX에 저장. - 나누기 : edx와 eax 레지스터에 나눌 수를 배치한다. div 명령어 실행 후 몫은 eax에 나머지는 edx에 저장한다.
div ebx ; EDX:EAX의 값을 EBX로 나눈다.
비트 연산
비트는 맨 오른쪽부터 번호가 매겨진다. 동일한 로직이 word, dword, qword에 적용.
- 가장 오른쪽 비트(least significant bit) : 0의 비트 위치 값을 가짐.
- 가장 왼쪽 비트(most significant bit) : 최상위 비트라고 부름.
비트 연산 명령어
- not : 모든 비트 반전
not eax ; eax의 모든 비트를 반전시키고 그 결과를 eax 레지스터에 저장. - and, or, xor : 연산 수행 후 결과를 목적지에 저장.
and bl, cl ; bl = bl & cl과 동일.
or eax, ebx ; eax = eax|ebx와 동일.
xor eax, eax ; eax = eax^eax와 동일, 이 명령어는 eax 레지스터를 지움(0으로). - shr(shift right), shl(shift left) : 2개의 피연산자 목적지와 카운트를 가진다. 목적지는 레지스터 또는 메모리 참조가 될 수 있다.
shl dst, count ; count에 지정한 수만큼 왼쪽으로 비트 이동. - rol(rotate left), ror(rotate right) : shift 명령어와 비슷. 이동한 비트를 제거하지 않고 다른 끝으로 회전시킨다.
rol al,2 ; al이 0x44(0100 0100)을 포함하고 있다면 rol 연산의 결과는 0x11(0001 0001)이 된다.
분기와 조건문, 반복문
순차적으로 실행하는 것이 아니라, 다른 메모리 주소(if/else문, 반복문, 함수 등)에서 코드를 실행하는 경우 분기 명령어를 이용할 수 있다. 분기는 jump 명령어를 이용해 수행하며, 조건 분기와 무조건 분기 두 종류가 있다.
1) 무조건 분기
항상 점프가 수행된다. CPU가 다른 메모리 주소에 있는 코드를 수행하도록 한다. 아래 명령어 실행 시 제어가 점프 주소로 넘어가고, 그곳부터 실행이 시작한다.
jmp <점프 주소>
2) 조건 분기
제어가 조건에 바탕을 둔 메모리 주소로 넘어간다.
cmp : 출발지에서 목적지를 뺀 후 그 결과를 저장하지 않고 플래그를 변경. 결과가 0이면 제로 플래그(zero flag)를 설정.
test : and 연산을 수행하고 결과 저장 없이 플래그를 변경.
cmp eax, 5 ; 5에서 eax를 뺀 후 플래그를 설정, 결과는 저장X
test eax, eax ; and 연산을 수행하고 플래그를 변경, 결과는 저장 X
이러한 cmp와 test 명령어 모두 조건 점프 명령어와 함께 사용하는 것이 일반적이다.
jcc <주소>
cc는 조건을 의미하며, 이러한 조건은 eflags 레지스터의 비트를 기반으로 평가한다.
eflags 비트 요약
명령어 | 설명 | 별칭 | 플래그 |
jz | 0이면 점프 | je | zf=1 |
jnz | 0이 아니면 점프 | jne | zf=0 |
jl | 작으면 점프 | jnge | sf=1 |
jle | 작거나 같으면 점프 | jng | zf=1 또는 sf=1 |
jg | 크면 점프 | jnle | zf=0 그리고 sf=0 |
jge | 크거나 같으면 점프 | jnl | sf=0 |
jc | 캐리(carry)가 1이면 점프 | jb, jnae | cf=1 |
jnc | 캐리(carry)가 1이 아니면 점프 | jnb, jae | . |
반복문 : for와 while
for : 초기화문은 한 번만 실행. 그 후 조건을 평가. 조건이 참이라면 for문 안의 코드블록 실행 후 업데이트문을 실행.
for (초기화; 조건; 업데이트문) {
코드블록
}
whlile : for 반복문과 달리 초기화와 조건 검사가 분리. 업데이트문은 반복문 안에 정의.
초기화
while (조건) {
코드 블록
업데이트문
}
어셈블리어로 이러한 조건문 및 반복문을 작성할 때는 이러한 분기를 이용할 수 있다. 자세한 예시는 책 4장의 6절과 7절을 참고.
함수
함수 : 특정 작업을 수행하는 코드 블록. 함수를 호출하면 제어가 다른 메모리 주소로 이동, 코드 실행 후 끝나면 돌아옴.
함수 컴포넌트 : 매개변수, 본문, 지역변수 -> 이러한 요소들은 스택(stack)이라 부르는 메모리의 영역에 저장.
스택(stack) : 스레드(thread)가 생성될 때 운영 시스템에서 할당하는 메모리 영역. 후입선출 구조.
- push : 데이터 입력
push source ; 스택의 맨 위에 source를 저장. - pop : 데이터 제거
pop destination ; 스택의 맨 위에서 destination으로 값을 복사(꺼냄).
esp 레지스터(스택 포인터) : 스택 생성 시 스택의 맨 위를 가리킴. 데이터 저장 시 4만큼 감소, 값을 꺼내면 4만큼 증가.
-> 스택에서 꺼낸 값들은 물리적으로 여전히 존재하지만 논리적으로 제거.
함수 호출 : call 명령어
call <호출 함수>
다음 명령어의 주소(call <호출 함수> 다음 명령어)를 스택에 저장하고, 제어가 호출 함수로 이동함. 스택에 저장하는 다음 명령어 주소를 복귀 주소(return address)라고 함.
함수 실행 후 복귀 시 ret 명령어 사용. 스택의 맨 위에 있는 주소를 가져옴. 그 주소는 eip 레지스터에 위치, 제어는 가져온 주소로 이동.
x86 레지스터 : 매개변수를 스택에 저장. 복귀 값을 eax 레지스터에 저장.
함수 실행 시
- ebp(프레임 포인터)를 스택에 저장. 함수가 종료된 후 복원을 위함이다.
- esp 값을 ebp로 복사 = 스택 맨 위를 가리키기 위함.
- 지역변수 할당을 위해 esp를 빼줌. ebp는 고정. -> ebp에서 양의 오프셋 값 = 매개변수, ebp에서 음의 오프셋 값 = 지역변수.
- eax에 반환값 설정.
- 함수 에필로그 명령어로 함수 환경 복원.
- ebp의 값을 esp로 복사 = esp는 ebp가 가리키는 주소를 가리킴.
- pop ebp로 esp는 4만큼 증가.
- ret 명령어 실행 시 스택 맨 위의 복귀 주소를 읽고 eip 레지스터에 복사.
- 제어가 복귀 주소로 이동. 이를 스택에서 빼면 esp는 4만큼 증가. -> 이 시점에서 제어는 main 함수.
스택 정리의 책임을 누가 갖고 있는지를 규정하는 규악이 있다.
- cdecl : 호출자(main 함수)는 매개변수를 오른쪽에서 왼쪽 순서로 스택에 푸시하고, 호출자 스스로가 함수 호출 후 스택을 정리하도록 함. / C 프로그램은 일반적으로 이 규약을 따름.
- stdcall : 호출자가 매개변수는 스택에 푸시(오른쪽에서 왼쪽). 피호출자(불린 함수) 스스로가 함수 호출 후 스택 정리. / 마이크로소프트 윈도우는 DLL 파일을 통해 익스포트한 함수에 이 규약을 따름.
- fastcall : 처음 몇 개의 매개변수는 레지스터에 저장해 함수에 전달, 나머지 매개변수는 오른쪽에서 왼쪽으로 스택에 저장해 전달. 피호출자가 스택을 정리. / 일반적으로 64비트 프로그램이 이 규약을 따름.
배열과 문자열
배열 : 같은 데이터 유형으로 구성된 리스트. 메모리의 연속된 위치에 저장.
어셈블리 언어에서 배열 요소 접근 : 배열의 기준 주소, 요소의 인덱스, 배열 각 요소의 크기를 사용해 주소 계산.
배열 요소에 접근하는 일반 형식 : [기준 주소 + 인덱스 * 요소의 크기 ]
문자열 : 문자의 배열. 널 종결자(null terminator)를 모든 문자의 끝에 추가. 각 요소는 메모리에서 1바이트를 차지.
문자 배열 요소에 접근하는 일반적인 형식 : str[i] = [str+i]
문자열 명령어
x86은 문자열을 연산하는 문자열 명령어를 제공. 접두사 b, w, d가 붙음. eax(값을 저장), esi(출발지 주소), edi(목적지 주소) 레지스터 사용. 문자열 연산이 끝나면 esi와 edi는 자동으로 증가하거나 감소. eflags의 디렉션 플래그(DF, Direction Flag)가 증가해야 하는지 감소해야 하는지를 결정. cld 명령어는 플래그를 지움. df=0이면 증가.
메모리에서 메모리로 이동
- movsx : 일련의 바이트를 한 메모리에서 다른 곳으로 이동할 때 사용.
- movsb : esi 레지스터에 지정한 주소에 있는 1바이트를 edi에서 지정한 주소로 옮길 때 사용.
- movsw, movsd : esi에 지정한 주소에 있는 2와 4 바이트를 edi에 지정한 주소로 옮김. 옮긴 후 esi와 edi 레지스터는 데이터 항목의 크기에 따라 1, 2, 4 바이트 증가/감소.
반복 명령어
movsx 명령어는 멀티 바이트를 복사하려면 rep 명령어를 문자열 명령어와 함께 사용해야 한다. rep 명령어는 ecx 레지스터에 의존. ecx 레지스터에 지정한 횟수만큼 문자열 명령어 반복.
rep 명령어는 movsx 명령어와 함께 사용 시, C프로그래밍에서의 memcpy() 함수가 동일한 기능.
명령어 | 조건 |
rep | ecx=0일 때까지 반복 |
repe, repz | ecx=0 또는 ZF=0일 때까지 반복 |
repne, repnz | ecx=0 또는 ZF=1일 때까지 반복 |
레지스터에서 메모리로 값 저장
- stosb : CPU 레지스터에서 edi(목적지 인덱스 레지스터)에 지정한 메모리 주소로 바이트를 옮길 때 사용. 일반적으로 rep 명령어와 함께 사용. 이 경우, C 프로그래밍의 memset() 함수와 동일.
- stosw, stosd : 데이터를 ax(2바이트)와 eax(4바이트)에서 edi에 지정한 주소로 옮김.
메모리에서 레지스터로 로딩
- lodsb : esi에 지정한 메모리 주소에서 al 레지스터로 바이트를 옮김.
- lodsw, lodsd : 2바이트와 4바이트 데이터를 esi에 지정한 메모리 주소에서 ax와 eax로 옮김.
메모리 스캐닝
- scab : 일련의 바이트에서 바이트 값의 존재 또는 부재를 찾을 때 사용. 찾고자 하는 바이트를 al 레지스터에, 메모리 주소는 edi 레지스터에 위치. 주로 repne 명령어와 함께 사용, ecx로 버퍼 길이 설정. 특정 바이트를 찾거나 ecx가 0이 될 때까지 반복.
메모리에서 값 비교
- cmpsb : esi에 지정한 메모리 주소의 바이트와 edi에 지정한 메모리 주소의 바이트를 비교해 값은 데이터를 포함하는지 확인할 때 사용. 일반적으로 repe와 함께 사용해 두 메모리 버퍼를 비교. 이 경우 ecx는 버퍼 길이 설정, ecx=0 또는 버퍼가 같지 않을 때까지 비교 반복.
구조체
구조체 : 다른 유형의 데이터를 함께 그룹화. 각 요소는 멤버라고 부름.
구조체의 각 멤버는 각자의 오프셋을 갖고 있으며 기준 주소에 상수 오프셋을 추가해 접근할 수 있음.
일반적인 접근 형식 : [기본_주소 + 상수_오프셋]
구조체와 배열 구분
- 배열 요소는 항상 동일한 데이터 유형 / 구조체는 동일한 데이터 유형을 가질 필요X
- 배열 요소는 대부분 기준 주소와 변수 오프셋으로 접근 / 구조체는 기준 주소와 상수 오프셋으로 접근
x64 아키텍처
x64 아키텍처는 x86의 확장으로 설계됨. x86 명령어 집합과 강한 유사점을 가짐.
x86과의 차이점
- 32비트 범용 레지스터(eax, ebx, ecx, edx, esi, edi, ebp, esp)가 64비트로 확장.
8개의 새 레지스터 이름은 r8, r9, r10, r11, r12, r13, r14, r15이다.
프로그램은 64비트(rax, rbx 등), 32비트(eax, ebx 등), 16비트(ax, bx 등), 8비트(al, bl 등)으로 접근 가능. - 64비트 데이터를 처리할 수 있고 모든 주소와 포인터가 64비트의 크기를 가짐.
- CPU는 실행할 다음 명령어의 주소를 저장하는 64비트 명령어 포인터(rip)를 가짐.
64비트 플래그 레지스터(rflags)를 갖지만 현재는 하위 32비트만 사용. - rip 상대 주소를 지원. 현재 명령어 포인터에서 오프셋만큼 떨어진 위치에 있는 데이터에 접근할 수 있음.
- x86은 스택에 저장된 것에 반해 첫 번째 4개 매개변수는 rcx, rdx, r8, r9에 저장돼 전달. 추가 매개변수가 있다면 스택에 저장.
64비트 윈도우에서의 32비트 실행 파일 분석
WOW64 하위 시스템 : 64비트 윈도우에서 32비트 바이너리를 실행할 수 있게 함.
실행하려면 API 함수 호출을 위해 DLL을 로드해야 함.
- 32비트 바이너리 : \Windows\system64 디렉터리에 저장.
- 64비트 바이너리 : \Windows\system32 디렉터리에 저장.
윈도우 64비트에서 32비트 악성코드 분석 시,
악성코드가 system32 디렉터리에 접근하면 이는 실제로 syswow64 디렉터리에 접근하는 것.
32비트 악성코드가 system32 디렉터리에 파일을 생성한다면 \Windows\Syswow64 디렉터리에서 그 파일을 찾을 수 있음.
-> 이러한 혼란 때문에 차이점을 알아둬야 하며, 되도록 32비트 바이너리는 32비트 윈도우에서 분석하는 것이 좋음.
'보안 > 악성코드' 카테고리의 다른 글
[6장]악의적인 바이너리 디버깅 (0) | 2025.05.13 |
---|---|
LummaStealer 악성코드 정적분석 (0) | 2025.05.07 |
[3장]동적 분석 (0) | 2025.04.08 |
[2장]정적 분석 (0) | 2025.04.01 |
[논문]"머신러닝 기반 악성 URL 탐지 기법" 정리 (0) | 2025.04.01 |