인프런 - 윈도우 악성코드(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 함수의 필요 없는 파라미터를 처리하는 방법 )
- 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 //함수 에필로그
'4-3. 2023-2 심화 스터디 > 윈도우 악성코드 분석' 카테고리의 다른 글
[2023.11.11] 인프런 윈도우 악성코드(malware) 분석 입문 과정 강의 수강 - 섹션 4 (1) | 2023.11.17 |
---|---|
[2023.11.04] 인프런 윈도우 악성코드(malware) 분석 입문 과정 강의 수강 - 섹션 3 (0) | 2023.11.10 |
[2023.10.14] 인프런 윈도우 악성코드(malware) 분석 입문 과정 강의 수강 - 섹션 2 (1) | 2023.10.14 |
[2023.10.07] 인프런 윈도우 악성코드(malware) 분석 입문 과정 강의 수강 - 섹션 0, 섹션 1 (1) | 2023.10.09 |