본문 바로가기
리버싱(Reversing)/'악성코드 분석 시작하기' 정복

[악성코드 분석]#4 어셈블리어와 디스어셈블리 - part(2/3)

by 아기미믹 2023. 7. 11.

책 '악성코드 분석 시작하기' 표지

이 글의 모든 내용은 책 '악성코드 분석 시작하기'를 스스로 공부하며 정리한 내용임을 밝힙니다.

 

CPU 레지스터

어셈블리어를 공부하면 반드시 알아야할 CPU 레지스터는 굉장히 다양하고 여러가지 이름을 가진 레지스터들이 등장해서 처음 어셈블리어를 입문한 사람의 골머리를 썩힌다... 이 기회에 책에 없는 내용이라도 정리해본다.

다음 나열된 것들은 리버싱에 주로 사용되는 레지스터이다.

 

1. 범용 레지스터

- 데이터와 주소를 모두 저장할 수 있는 레지스터로 제일 많이 사용되고 자주 등장하는 레지스터이다.

레지스터 이름(64bit/32bit) 설명
rax / eax 사칙연산에 사용되는 레지스터로 어떤 계산의 결과값을 주로 저장한다.
rbx / ebx 주로 메모리 주소를 저장하는데 사용된다.
rcx / ecx 주로 루프 카운터로 쓰인다. (반복문에서 인덱스값)
rdx / edx rax/eax와 같이 연산의 결과를 저장하는 공간으로 쓰인다. (확장 공간)
rsp / esp 현재 스택의 주소값을 저장하는 레지스터이다. (스택의 최상단 주소값)
rbp / ebp 현재 수행되는 함수의 Base Point. 보통 스택의 주소값을 가진다.
ebp 값을 기준으로 계산되는 것은 다음과 같다.
- 지금 수행되는 함수의 지역변수 주소값 (예, ebp-4, ebp-8 등)
- 지금 수행되는 함수의 매개변수 주소값 (예, ebp+8, ebp+c 등)
※ 자연스럽게 함수의 첫 번째 매개변수 값은 ebp+8이 된다.
왜냐하면, 현재 수행되고 있는 함수가 호출될 때(이전에 수행되던 함수에서), 함수의 호출이 끝나면 돌아올 주소(함수 다음 코드 라인)와 이전에 수행되던 함수의 ebp가 순서대로 스택에 PUSH되기때문
rsi / esi 보통 데이터 전송 시 출발지 주소를 의미하는 레지스터이다.
rdi / edi 보통 데이터 전송 시 목적지 주소를 의미하는 레지스터이다.

- 범용 레지스터의 구조는 다음과 같이 이루어져있다. 64bit 운영체제의 경우, 64bit 공간의 레지스터를 활용하는데 이를 통해 32bit, 16bit, 8bit의 기반 프로그램도 실행할 수 있게 된다.

rax의 구성

rax, rbx, rcx, rdx 모두 각각 eax/ebx/ecx/edx, ax/bx/cx/dx와 같이 이름을 가진다.

 

2. 주소 레지스터

- EIP: CPU 프로세서가 지속적으로 올바르게 동작할 수 있도록 다음 명령어의 주소를 담는 레지스터이다.

 

3. EFLAGS 레지스터

- 산술연산 결과에 따라 값을 가질 수 있는 상태 레지스터로 대표적인 것은 다음과 같다.

CF(Carry Flag): 산술연산 수행 결과가 자리 올림이나 자리 내림이 발생할 때, 1의 값을 가진다.

ZF(Zero Flag): 산술연산 수행 결과가 0이면, 1의 값을 가진다.

OF(Overflow Flag): 산술연산 수행 결과가 가용 가능한 비트수를 초과하였을 때, 1의 값을 가진다.

 

어셈블리 명령어

1. 어셈블리 기본 명령어

어셈블리어에는 다양한 명령어가 존재한다. 정말 신기하게도 이런 명령어를 조합하면 평소 우리가 고급언어로 작성하는 모든 로직(logic)을 표현 가능하다는 것이다. 명령어 또한 한 방에 정리해보자.

명령어 문법 설명
mov mov 목적지, 출발지 출발지의 값을 목적지로 복사한다.
lea lea 목적지, 출발지 출발지의 주소에 존재하는 값을 목적지의 주소에 복사한다.
add add 목적지, 출발지 목적지의 값에서 출발지의 값을 더한 후, 목적지에 저장한다.
sub sub 목적지, 출발지 목적지의 값에서 출발지의 값을 뺀 후, 목적지에 저장한다.
inc inc 목적지 목적지의 값을 1만큼 증가
dec dec 목적지 목적지의 값을 1만큼 감소
mul mul 목적지 목적지의 값을 eax(또는 ax, al 등)와 곱하고 그 결과를 eax와 edx에 저장한다.
div div 목적지 목적지의 값을 eax와 edx로 나눈 후, 몫을 eax에 나머지를 edx에 저장한다.
not not 목적지 목적지 값의 모든 비트를 반전시킨다.
and and 목적지, 출발지 목적지출발지의 값을 and 연산 후, 목적지에 값을 저장한다.
(두 비트 모두 1이면 1, 그 외는 0)
or or 목적지, 출발지 목적지 출발지의 값을 or 연산 후, 목적지에 값을 저장한다.
(두 비트 중 하나라도 1이면 1, 그 외는 0)
xor xor 목적지, 출발지 목적지 출발지의 값을 xor 연산 후, 목적지에 값을 저장한다.
(두 비트가 서로 다르면 1, 같으면 0)
shl / shr shl/shr 목적지, 카운트 목적지의 값을 카운트만큼 비트 이동한다.
(범위 초과시 삭제, 최하위 비트는 0으로 채움)
rol / ror rol/ror 목적지, 카운트 목적지의 값을 카운트만큼 비트 이동한다.
(범위 초과시 최하위 비트로 이동함)
cmp cmp 비교1, 비교2 비교1 - 비교2하여 두 값을 비교한다. 값 저장은 이루어지지 않는며, 결과에 따라 eflags 값을 변경한다.
test test 비교1, 비교2 비교1 and 비교2하여 두 값을 비교한다. 값 저장은 이루어지지 않는며, 결과에 따라 eflags 값을 변경한다.
 jmp jmp  주소 주소로 코드를 점프하여 수행한다.
push push 을 스택의 최상단에 저장한다.
pop pop 목적지 스택의 최상단에서 값을 가져와 목적지에 저장한다.
call call 함수 주소 함수 복귀 주소를 스택에 저장하고, 함수 시작 주소로 이동한다.
push 복귀 주소
jmp 함수 주소
ret ret 함수의 호출을 종료하고 스택에서 복귀 주소를 꺼내 eip에 저장한다.
pop eip

 

2. 여러가지 어셈블리 명령어 예시

mov eax, ebx // ebx의 값을 eax로 복사. eax = ebx

mov [0x403000], eax // eax에 있는 4바이트의 값을 0x403000에서 시작하는 메모리 위치로 복사한다.

mov dword ptr [40200], 13498h // 16진수 13498값을 40200 메모리 주소에서 4바이트(Dword)만큼 복사한다.

lea ebx, [0x403000] // 0x403000 메모리 주소의 값을 ebx에 복사한다.

add eax, 42 // eax = eax+42와 동일

sub eax, 64h // eax = eax-0x64와 동일

inc eax // eax += 1와 동일

dec ebx // ebx -= 1와 동일

mul bx // bx와 ax를 곱하고, 그 결과를 dx와 ax에 저장

mul ebx // ebx와 eax를 곱하고, 그 결과를 edx와 eax에 저장

 

 

조건과 분기

if문, if-else문, if-else if-else 문에 대한 해석을 공부해보자.

어셈블리어에서 모든 조건과 분기는 비교 연산자로부터 비롯된다. cmp, test 등과 같은 비교 연산자를 통해 eflags 값을 변경 후, 그에 따른 분기를 jmp 명령어를 통해 수행한다고 이해하면 된다.

 

1. 여러가지 점프 명령어

명령어 설명 별칭 플래그
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 -

조건/분기 어셈블리를 디스어셈블할 때는 보통 조건이 반대된다고 생각하면 편하다.

예를 들어 다음과 같은 C언어 코드가 있다고 해보자.

if (x == 0) {
	x = 10;
} else {
	x = 20;
}

첫 번째 분기에서 x가 0이면 if 블록 안의 코드를 수행하고, x가 0이 아니면 else 블록 안의 코드가 수행된다.

즉, x가 0이 아닐 때(else) 점프가 일어난다고 보면 된다.

실제로 위 코드를 어셈블리어로 표현하게 되면 다음과 같다.

cmp dword ptr [ebp-4], 0
jne else
mov dword ptr [ebp-4], 10
jmp end
else:
mov dword ptr [ebp-4], 20

end:

다시 위 어셈블리어를 보고 고급언어로 변환한다고 했을 때,

cmp dword ptr [ebp-4], 0
jne else

를 보고 다음과 같이 사고하면 된다.

jne, 0을 본다. → 0이 아닐 때 점프한다. → 어셈블리와 고급언어는 반대로 표현한다. → if ([ebp-4] == 0) 와 같은 형태겠구나

 

반복문

if문은 조건에 따라 else-if 나 else 구문으로 점프하였다면, 반복문은 반복문의 끝에 도달했을 때, 조건을 검사하는 반복문의 처음으로 점프한다.

 

다음 어셈블리어를 고급언어로 변환해보자.

mov dword ptr [ebp-8], 0
mov dword ptr [ebp-4], 1

loc_401014:
    cmp dword ptr [ebp-4], 4
    jge loc_40102E
    mov eax, [ebp-8]
    add eax, [ebp-4]
    mov [ebp-8], eax
    mov ecx, [ebp-4]
    add ecx, 1
    mov [ebp-4], ecx
    jmp loc_401014
    
loc_40102E:

메모리에 대한 접근이 두 군데에서 일어난다. (ebp-8, ebp-4)

각각 변수 x와 y로 생각해주자.

1, 2번째 라인에서 4byte로 값을 복사해주는 부분이 있으므로 int라고 추정할 수 있다.

※ 4byte는 float이나 포인터를 의미할 수도 있지만, 그것은 전체적인 코드의 흐름을 보고 추정할 수 있다. x와 y를 각각 1과 0으로 초기화해준다는 점에서 정수이고(float x), 1과 0은 유효한 주소가 아님(포인터 x)에 따라 int라고 추정한다.

int x = 0;
int y = 1;

loc_401014:
    cmp dword ptr [y], 4
    jge loc_40102E
    mov eax, [x]
    add eax, [y]
    mov [x], eax
    mov ecx, [y]
    add ecx, 1
    mov [y], ecx
    jmp loc_401014
    
loc_40102E:

loc_401014:의 내용을 미루어보았을 때, 마지막에 jmp loc_401014를 통해 반복문임을 유추할 수 있다.

반복문에 대한 조건은

cmp dword ptr [y], 4

jge loc_40102E

로 어셈블리어와 고급언어는 서로 조건이 반대됨을 인지하여 다음과 같이 변환할 수 있다.

int x = 0;
int y = 1;


while(y < 4){
    mov eax, [x]
    add eax, [y]
    mov [x], eax
    mov ecx, [y]
    add ecx, 1
    mov [y], ecx
}

나머지 각 어셈블리어에 대해 해석해보자.

mov eax, [x]    // x의 값을 eax로 옮겨와서

add eax, [y]    // y의 값을 eax에 더하고

mov [x], eax   // 그 eax를 다시 x에 넣는다.

즉, x = x+y를 의미한다.

 

mov ecx, [y]    // y의 값을 ecx로 옮겨와서

add ecx, 1      // 그 ecx에 1을 더하고

mov [y], ecx    // 그 결과를 다시 y에 넣는다.

즉, y = y +1 를 의미하고 y++ 로 축약할 수 있다.

 

완성된 코드는 다음과 같다.

int x = 0;
int y = 1;

while(y < 4){
    x = x + y;
    y++;
}

결론적으로 이 코드를 해석하자면 1부터 3까지의 합을 구하는 프로그램이라고 할 수 있다.

 


어셈블리어를 공부하면서 느낀 것은 새로운 언어를 처음부터 배우는 것 같다.

마치 C언어든, C++이든, Java든, Python이든 변수, 조건문, 반복문, 함수, 배열, 구조체(클래스) 순으로 배우는 것처럼 어셈블리어 공부도 똑같다.

다만 내가 여태껏 짜왔던 코드가 좀 더 세밀하게 어떻게 동작하는지 이해할 수 있는 좋은 기회인 것 같다.

다음은 함수, 배열, 문자열, 구조체 등에 대해서 포스팅할 예정이다.