본문 바로가기

4-3. 2023-2 심화 스터디/윈도우 악성코드 분석

[2023.09.23] 인프런 윈도우 악성코드(malware) 분석 입문 과정 강의 수강 - 섹션 0의 1.1~1.5

인프런 - 윈도우 악성코드(malware) 분석 입문 과정 섹션 0. 악성코드분석 입문과정 소개 및 준비의 1.1 부터 1.5까지 수강하였다.


리버싱 기초

1.2 레지스터와 어셈블리

 

Reversing(역공학)

: 프로그램을 소스코드 단계로 되돌려 분석하는 것 , 프로그램 내에서 디버깅이나 버그 발생 시 분석하기 위한 기술

 

C언어가 어셈블리어가 되는 과정

 

C언어 ----------------------> 기계어 (바이너리 코드) ---------------------> 어셈블리어

                  컴파일                                                      디스어셈블

 

C/C++ 코드와 어셈블리 코드의 차이

  • C/C++은 간결하게 동작을 지정하지만, 어셈블리어는 한 가지 동작까지 세세하게 지정한다.

어셈블리의 명령 포맷

  • 주로 IA-32를 사용한다.
  • 어셈블리의 기본 형태 : 명령어(opcode) + 인자(operand 1~2)

레지스터 : CPU가 사용하는 변수

 

레지스터 종류

레지스터 역할
EAX 각종 연산에 쓰임, 가장 많이 쓰이는 변수, 주로 리턴 값을 저장
EDX 각종 연산에 쓰이는 변수
ECX for 문에서 i의 역할, ECX는 미리 값을 정해놓고 0이 될 때까지 진행, 변수로 사용해도 무방
EBX 목적이 없는 레지스터, 공간이 필요할 때 덤으로 사용
ESI,EDI 문자열이나 각종 반복데이터를 처리 또는 메모리를  옮기는 데 사용
ESP 스택 포인터
EBP 베이스 포인터
EIP 인스트럭션 포인터
  • 스택 : push, pop 명령어 사용, FILO(First In Last Out) 라는 데이터 형태를 가짐
  • 메모리를 스택처럼 사용하기 위해 스택/베이스 포인터 사용
  • EIP : 지시를 내리는 포인터. 사용자가 함부로 변경 불가능

 

레지스터- 레지스터 단위

'n bit 운영체제에서 n bit는 cpu가 한 번에 처리할 수 있는 데이터의 양을 의미

  • 32 bit 운영체제 → 32비트 길이의 데이터를 한 번에 처리 가능
  • 64 bit 운영체제 → 64비트 길이의 데이터를 한 번에 처리 가능

   31                                                                                                                                                                                                 0

<------------------------------------ ------------------------------EAX ----------------------------------- ---------------------------------->
    <--------------------------------AX ---------------------------------->
    AH(high) AL(low)

 

 

32bit 16bit 상위 8bit 하위 8bit
EAX AX AH AL
EDX DX DH DL
ECX CX CH CL
EBX BX BH BL

 


 

1.3 어셈블리 명령어와 함수 호출 구조

 

어셈블리 명령어

  • PUSH, POP : PUSH-스택에 데이터 삽입, POP-스택에서 데이터를 꺼냄
  • PUSHAD, POPAD : PUSHAD - 모든 레지스터에 있는 데이터를 삽입, POPAD - 모든 레지스터에 있는 데이터를 꺼냄
  • MOV : source를 가져옴
  • LEA : source의 주소를 가져옴
  • ADD : 데이터를 더하는 명령
  • SUB : 데이터를 빼는 명령
  • INT : 인터럽트 명령, CPU 정지
  • CALL : 함수 호출
  • INC,DEC : i++,i—
  • AND, OR, XOR : 비트연산
  • NOP : 실행하지 않는다는 의미
  • CMP, JMP : CMP-두개의 인를 비교할 때 사용. JMP-점프명령어

 

함수 프롤로그

push ebp             // ebp 삽입(이전함수의 ebp를 스택에 저장하기 위함)
mov ebp, esp         // esp 값을 ebp에 저장
sub esp, 50h         // esp 50h 위치를 올림(지역 변수 저장 위치)
  • main 함수에서 plus 함수 실행 시 함수 프롤로그와 에필로그를 진행하게 된다. 즉, plus 함수의 스택 공간을 생성한다.
  • 이때, 스택을 다른 별도의 공간에 만들어주는 것이 아닌 main 함수의 스택 위에 plus 함수의 스택을 생성한다. 이를 스택프레임이라고 부른다.
  • 스택프레임을 관리하기 위해 함수 프롤로그와 에필로그가 존재.
  • 함수 프롤로그 : 스택 생성
  • 함수 에필로그 : 스택 삭제

 

함수의 호출 & 리턴 주소

  • 메인 함수에서 HelloFunction 함수 호출
  • LIFO 방식으로 스택을 삽입
  • 이때, main() 함수의 ebp는 hellofunction의 첫 번째 스택에 할당.
main()
{
    DWORD dwRet = HelloFunction( 0x37 , 0x38 , 0x39 ) ;
    if (dwRet)
// ......
}

⬇️

push 39h
push 38h
push 37h
call 401300h; HelloFunction

 

1.4 함수 호출 규약

 

함수의 기본 구조

  • 간단한 sum 함수의 구조
int sum(int a, int b)
{
    int c = a + b;
    return c;
}

⬇️

push ebp
mov ebp, esp (ebp에 esp 값을 덮어씌움)
push ecx      // 함수 프롤로그 

mov eax, [ebp+arg_0]    //3이 eax로 들어감 (a가 3이라고 가정)
add eax, [ebp+arg_4]    // eax(3)과 4를 더한 후  eax에 저장 (b가 4라고 가정)
mov [ebp+var_4], eax    //eax(7)값을 [ebp+var_4]위치에 옮김
mov eax, [ebp+var_4]    //[ebp+var_4]에 있던 값을 eax에 옮김

mov esp, ebp
pop ebp
return          //함수 에필로그

 

함수의 호출 규약 (main 함수의 필요 없는 파라미터를 처리하는 방법 )

 

  1. cdecl
  • 항상 call문의 다음 줄을 살펴서 스택을 정리하는 곳을 체크한다.
  • cdecl 코드는 call 이후, add esp, 8 과 같이 스택을 보정한다.
  • add esp, 8 그리고 push 문이 2개 → 4byte 파라미터가 두 개임을 추측할 수 있다.
int __cdecl sum(int a,int b)
{
    int c= a+b;
    return c;
 }

 int main(int argc,char* argv[])
 {
    sum(1,2);
    return 0;
 }

⬇️

sum :
push ebp
mov ebp, esp
push ecx
mov eax, [ebp+arg_0]
add eax, [ebp+arg_4]
mov [ebp+var_4], eax
mov eax, [ebp+var_4]
mov esp,ebp
pop ebp
retn

main:
push 2
push 1
call calling.00401000
add esp,8

 

2. stdcall

  • main 함수 내부에서 스택을 처리하지 않는다.
  • stdcall 코드는 retn 8을 사용하여 스택을 처리한다. (8byte 삭제)
  • Win32 API는 stdcall 방식을 사용한다.
int __stdcall sum(int a,int b)
{
    int c = a + b ;
    return c;
 }

⬇️

sum :
push ebp
mov ebp, esp
push ecx
mov eax, [ebp+arg_0]
add eax, [ebp+arg_4]
mov [ebp+var_4], eax
mov eax, [ebp+var_4]
mov esp,ebp
pop ebp
retn 8

main:
push 2
push 1
call calling.00401000

 

3. fastcall

  • 파라미터가 2개 이하일 경우, 인자를 push로 넣지 않고 ecx와 edx 레지스터를 이용한다.
  • 레지스터를 이용하기에 메모리를 이용하는 것보다 훨씬 빠르다.
  • 함수 호출 전, eds와 ecx 레지스터에 값을 넣는 것을 확인하면 fastcall 규약 함수임을 짐작할 수 있다.
int __fastcall sum(int a,int b)
{
    int c = a + b ;
    return c;
 }

⬇️

sum :
push ebp
mov ebp, esp
sub esp,0Ch
mov [ebp+var_C], edx
mov [ebp+var_8], ecx
mov eax, [ebp+var_8]
add eax, [ebp+var_C]
mov [ebp+var_4], eax
mov eax, [ebp+var_4]
mov esp,ebp
pop ebp
retn

main:
push ebp
mov ebp,esp
mov edx,2
mov ecx,1
call calling.00401000
xor eax, eax
pop dbp
retn

 

4. thiscall

  • 주로 C++의 클래스에서 this 포인터로 이용한다.
  • 현재 객체의 포인터에 ecx에 전달되는 특징을 가진다. (this 포인터를 ecx로 전달한다 라는 의미)
  • 다음과 같이 ecx 포인터에 오프셋 번지를 더한다. → 하나의 변수를 이용해서 다양한 데이터를 넘길 수 있다.
파라미터 주소
a ecx+x
b ecx+y
c ecx+z

 

Class CTemp
{
public:
	int MemberFunc(int a, int b);
};

⬇️

mov eax, dword ptr [ebp-14h]
push eax
mov edx, dword ptr [ebp-10h]
push edx
lea ecx,[ebp-4]
call 402000

 

1.5 C 문법과 어셈블리어

 

if문

int Temp(int a)
{
    int b =1;

    if(a=1){
       a++;
    }else{
       b++;
    }

    return b;
 }

 int main(int argc, char* argv[])
 {
 	Temp(1);
 }

⬇️

.text:00401000    push  ebp
.text:00401001    mov ebp,esp
.text:00401003    push  ecx                // 함수 프롤로그

.text:00401004    mov dword ptr [ebp-4],1  //[ebp-4]의 위치(= int b)에 1을 저장
.text:0040100B    cmp dword ptr [ebp+8],1  //[ebp+8]의 값(= int a)과 1을 비교
.text:0040100F    jnz short loc_40101C     //a와 1이 같지 않다면 40101C 주소로 점프
.text:00401011    mov eax, [ebp+8]         //a와 1이 같은 경우 [ebp+8]의 값을 eax에 옮김
.text:00401014    add eax,1                //eax에 1을 더함
.text:00401017    mov [ebp+8],eax          //eax를 [ebp+8]의 위치에 옮김
.text:0040101A    jmp short loc_401025     //401025 주소로 점프
.text:0040101C loc_40101C:
.text:0040101C    mov eax, [ebp-4]         //[ebp-4]의 값을 eax에 옮김
.text:0040101F    add ecx,1                //ecx에 1을 더함
.text:00401022    mov [ebp-4], ecx         //ecx를 [ebp-4]의 위치에 옮김
.text:00401025
.text:00401025 loc_401025:
.text:00401025    mov eax, [ebp-4]        //[ebp-4]의 값을 eax로 옮김

.text:00401028    mov esp,ebp
.text:0040102A    pop ebp
.text:0040102B    retn                    // 함수 에필로그

 

반복문

반복문은 for, while, goto 등이 있으나 컴퓨터 입장에서는 결국 카운터 레지스터를 이용한 반복이다.

int loop(int c)
{
    int d;
    for (int i=0; i<=0x100;i++)
    {
    	c--;
      d++;
    }
     return c+d;
 }

⬇️

.text:00401000         push ebp
.text:00401001         mov ebp,esp
.text:00401003         sub esp ,8               // 함수 프롤로그

.text:00401006         mov dword ptr [ebp-8],0  //[ebp-8]의 위치(=int i)에 0을 저장
.text:0040100D         jmp short loc_401018     //401018 주소로 점프
.text:0040100F         mov eax,[ebp-8]          //[ebp-8]의 값(=int i)을 eax에 옮김
.text:00401012         add eax,1                //eax 값(=int i)을 1 증가
.text:00401015         mov [ebp-8], eax          //eax의 값을 [ebp-8](=int i)에 옮김
.text:00401018         cmp dword ptr [ebp-8], 100h //[ebp-8]의 값(=int i)과 0x100 을 비교
.text:0040101F         jg short loc_401035         // 만약, 0x100보다 i의 값이 클 경우 401035 주소로 점프
.text:00401021         mov ecx,[ebp+8]      //[ebp+8]의 값(= int c)을 ecx에 옮김
.text:00401024         sub ecx,1            //ecx의 값을 1 감소
.text:00401027         mov [ebp+8], ecx     //ecx의 값을 [ebp+8](= int c)에 옮김
.text:0040102A         mov edx,[ebp-4]      //[ebp-4]의 값(= int d)을 edx에 옮김
.text:0040102D         add edx,1            //edx의 값을 1 증가
.text:00401030         mov [ebp-4], edx     //edx의 값을 [ebp-4](= int d)에 옮김
.text:00401033         jmp short loc_40100F //40100F 주소로 점프한다. → 반복문 처음으로 이동
.text:00401035         mov eax, [ebp+8]     //[ebp+8]의 값(=int c)을 eax에 옮김
.text:00401038         add eax, [ebp-4]     //[ebp-4]의 값(=int d)과 eax를 더함

.text:0040103B         mov esp, ebp
.text:0040103D         pop ebp
.text:0040103E         retn// 함수 에필로그

 

구조체와 API Call

 

운영체제는 다양한 구조체들의 집합이라고 볼 수 있다.

 

void RunProcess()                //프로세스를 실행하는 함수
{
	STARTUPINFO si;              //프로세스를 실행할 때 기본적으로 필요한 두가지 구조체
	PROCESS_INFORMATION pi;      

	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));

	//Start the child process.
	if(!CreateProcess(Null,"MyChildProcess",NULL,NULL,FALSE,0,NULL,NULL,&si,&pi))
	{
		printf("CreateProcess failed.\\n");
		return;
	}

	// Wait until child process exits.
	WaitForSingleObject(pi.hProcess, INFINITE);
	
	//Close process and thread handles.
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hProcess);
}

⬇️

0x401000 	PUSH EBP
0x401001 	MOV EBP,ESP
0x401003 	SUB ESP, 54                           //함수 프롤로그

0x401006 	PUSH 44                               //0x401006~0x401013 : ZeroMemory()_STARTUPINFO 구조체 초기화
0x401008 	PUSH 0
0x40100A 	LEA EAX, DWORD PTR SS:[EBP-54]     
0x40100D 	PUSH EAX
0x40100E 	CALL calling.004011A0               
0x401013 	ADD ESP,0C
0x401016 	MOV DWORD PTR SS:[EBP-54], 44        //0x401016 : _STARTUPINFO 구조체의 첫 번째 멤버 변수에 0x44를 삽입
0x40101D 	PUSH 10                              //0x40101D~0x40102A : ZeroMemory()_PROCESS_INFORMATION 구조체 초기화
0x40101F 	PUSH 0
0x401021 	LEA ECX,DWORD PTR SS:[EBP-10]       
0x401024 	PUSH ECX
0x401025 	CALL calling.004011A0
0x40102A 	ADD ESP,0C                          //0x40102A~0x401048 : CreateProcess()를 호출. 인자를 역순으로 전달
0x40102D 	LEA EDX, DWORD PTR SS:[EBP-10]
0x401030 	PUSH EDX
0x401031 	LEA EAX, DWORD PTR SS:[EBP-54]
0x401034 	PUSH EAX
0x401035 	PUSH 0
0x401037 	PUSH 0
0x401039 	PUSH 0                          
0x40103B 	PUSH 0
0x40103D 	PUSH 0
0x40103F 	PUSH 0
0x401041	 PUSH calling.00407030
0x401046 	PUSH 0
0x401048 	CALL DWORD PTR DS:CreateProcessA
0x40104E 	TEST EAX,EAX                      //0x40104E~0x401050 : 리턴 값이 NULL인지 검사. NULL이 아닐 경우 점프
0x401050 	JNZ SHORT calling.00401061
0x401052 	PUSH calling.00407040             //0x401052~0x40105F : 리턴 값이 NULL인 경우 "Create Process failed.\a"를 출력
0x401057 	CALL calling.0040116F
0x40105C 	ADD ESP,4
0x40105F 	JMP SHORT calling.00401081
0x401061 	PUSH -1                           //0x401061~0x40107B : WaitingForSingleObject & CloseHandle을 진행
0x401063 	MOV ECX, DWORD PTR SS:[EBP-10]
0x401066 	PUSH ECX
0x401067 	CALL DWORD PTR DS:WaitForSingleObject
0x40106D 	MOV EDX, DWORD PTR SS:[EBP-10]
0x401070 	PUSH EDX
0x401071 	CALL DWORD PTR DSS:CloseHandle
0x401077 	MOV EAX, DWORD PTR SS:[EBP-C]
0x40107A 	PUSH EAX
0x40107B 	CALL DWORD PTR DS:CloseHandle

0x401081 	MOV ESP,EBP
0x401083 	POP EBP
0x401084 	RETN                              //함수 에필로그