리버싱이란?
[reverse engineering] : 역공학이란 뜻으로 장치 또는 시스템의 기술적인 원리를 그 구조분석을 통해 발견하는 과정이다.
우리가 하는 악성코드 분석(소프트웨어 리버스 엔지니어링)에서는 악성코드를 분석해서 악성코드를 발견하는 과정이다.
https://ko.wikipedia.org/wiki/%EC%97%AD%EA%B3%B5%ED%95%99
기계어(0,1) <-> 어셈블러(PUSH, MOV..) <-> c, python..
어셈블리어란?
[assemly language] : 컴퓨터 프로그래밍에서 사용하는 저급 언어로 컴퓨터가 사용하는 언어다.
ex)push, pop, mov.. 같은 것이 있다.
https://ko.wikipedia.org/wiki/%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC%EC%96%B4
어셈블러란?
[assembler] : 어셈블리어를 기계어 형태의 오브젝트 코드로 해석해 주는 컴퓨터 언어 번역 프로그램이다.
컴퓨터에서 C언어로 된 코드를 짜서 컴파일하면 컴퓨터 내부에선 cpu에서 연산을 하는데
0과 1로 되어있는 기계어가 cpu가 이해할 수 있는 언어이기 때문에 cpu에서 컴퓨터에서 짠 C언어를 이해하려면 중간에서 번역을 해줘야 한다. 어셈블러는 기계어와 1:1로 대응되어 중간에서 번역 역할을 해줄 수 있다.
예시
1. 리버스 엔지니어링만을 위한 어셈블리
어셈블리의 명령 형식(어셈블리 x86 cpu의 IA-32 기준)
"명령어 + 인자"
명령어는 mov, push 같은 것이고 옵코드(opcode)라고 한다.
인자는 명령어 다음에 나와 어떤 장소에 값을 넣을 것인지 명령어에 해당하는 값 등이 들어가서 보통 주소가 들어간다.(=오퍼랜드라고 한다operand)
- 오퍼랜드 1개인 경우
ex) push 337
-> push(옵코드)는 스택에 값을 넣으라는 명령어로 스택에 337(오퍼랜드)을 넣어라
- 오퍼랜드 2개인 경우
ex) mov eax, 1
-> mov(옵코드), eax와 1(오퍼랜드) = eax에 1을 넣어라[앞의 오퍼랜드가 목적지 오퍼랜드 / 뒤의 오퍼랜드가 출발지 오퍼랜드 memcpy, strcpy와 같다.]
- 레지스터는 cpu가 사용하는 변수로 생각하고 c언어에서 사용하는 것처럼 변루소 더하고 빼는 연산을 할 수 있는 것으로 생각.
레지스터의 레지스터는 eax, ebx, ecx, edx, esi, edi, ebp, esp로 총 8개이다.
EAX(Accumulator)
- 산술 계산을 하며 리턴값을 전달한다.
변수의 덧셈, 뺄셈, 곱셈, 나눗셈 등에 사용되며 함수의 리턴값이나, return 100, return false 등의 코드를 사용할 때 100, false 값이 eax에 기록된다.
EDX(Data)
- eax와 역할은 같되, 리턴 값의 용도로 쓰이지 않는다.
각종 연산에 쓰이며 더하고 빼고 곱하는 용도로 사용된다.
ECX(Count)
- 루프문을 수행할 때 카운팅하는 역할을 한다. for 문에서 int i라고 선언할 때 i의 역할이라고 생각하면 쉽다.
다른 점은 미리 루프를 돌 값을 넣고 i--;로 감소하며 카운팅한다.
EBX
- 어떤 목적을 가지고 만들어진 레지스터가 아니라 하나쯤 더 필요하거나 공간이 필요할 때를 위해 적당한 용도를 알아서 사용한다. eax, edx, ecx 부족할때 사용
ESI, EDI
=ESI는 문자열이나 각종 반복 데이터를 처리 또는 메모리를 옮기는 데 사용한다.
=ESI는 시작지 인덱스(source Index), EDI는 목적지 인덱스(Destination Index)롤 사용된다.
즉, memcpy(voide *dest, void *src, size_t count)는 두번째 인자(source)에서 첫번째 인자(Destination)로 메모리를 복사한다.
이것처럼 ESI와 EDI 역시 ESI(source)에서 메모리를 읽어 EDI(destination)로 메모리를 복사한다.
경우에 따라 al, ah 레지스터도 있는데 16비트 레지스터로 ESI, EDI보다 크기가 반정도다.[l은 low, h은 high]
eax는 32비트 / ax는 16비트 / ah, al은 8비트
ex)
mov ah, byte ptr ds:[esi] // esi 주소에 담긴 값을 바이트 단위로(1바이트만 가져와서) ah에 넣어라
mov al, byte ptr ds:[esi] // 같은 값을 al에 넣어라
mov ax, byte ptr ds:[esi] // ax에 값을 넣어라
32비트 | 16비트 | 상위 8비트 | 하위 8비트 |
EAX | AX | AH | AL |
EDX | DX | DH | DL |
ECX | CX | CH | CL |
EBX | BX | BH | BL |
#리틀엔디언
바이트 저장 순서는 엔디언(endian)이라고 한다.
쉽게 설명하면 우리가 흔히 사용하는 순서의 숫자는 빅 엔디언이라고 하고 이것의 반대 방향은 리틀 엔디언이라고 한다.
0x12345678라는 DWORD 값이 있다.
12 34 56 78으로 읽으면 빅엔디언 78 56 34 12로 읽으면 리틀 엔디언
ex) 실제코드
int Plus(int a, int b)
{return a+b;}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
mov eax, dword ptr ss:[esp+8] =b
mov ecx, dword ptr ss:[esp+4] =a
add eax, ecx
retn
외울 필요 없는 어셈블리 명령어
PUSH
: 스택에 값을 넣는 것.
POP
: 스택에 있는 값을 가져오는 것.
MOV
: 값을 넣는 역할. (mov eax, 1은 1을 eax에 넣는 코드)
LEA
: 주소를 가져오라는 얘기로 가져올 src오퍼랜드가 주소라는 의미로 대부분 []로 둘러싼다.
ADD
: source에서 destination으로 더하는 명령
SUB
: 뺄셈
INT
: 인터럽트를 일으키는 명령어로 뒤 오퍼랜드로 어떤 숫자가 오냐에 따라 다른 처리가 일어난다. ex) INT 3 -> 옵코드가 0xCC인 DebugBreak()
CALL
: 함수를 호출하는 명령어. call뒤에 오퍼랜드로 번지가 붙는다. 해당 번지를 호출하고 작업이 끝나면 call 다음 번지로 되돌아온다.
call로 호출된 코드 안에서는 반드시 RET를 만나게 되어 다시 호출한 쪽으로 돌아오기 때문
INC, DEC
: inc는 i++;, dec는 i--;
AND, OR, XOR
: dest와 src를 연산한다.
NOP
: 아무것도 하지 말라는 명령어.
CMP, JMP
: 비교해서 점프하는 명령어
리버스 엔지니어링에 필요한 스택
1. 함수 호출 시 파라미터가 들어가는 방향
2. 리턴 주소
3. 지역 변수 사용
2. C 문법과 디스어셈블링
함수의 기본 구조
int sum(int a, int b)
{
int c = a + b;
return c;
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡ
push ebp // push를 통해 직므까지의 베이스 주소를 스택에 보관한다.
mov ebp, esp //현재의 스택 포인터인 esp를 ebp로 바꾼다. 지금까지의 기준이 될 스택 베이스 포인터(ebp)를 백업해 두고 새로운 포인터를 잡는 것. (즉, 함수를 시작할 때 새로운 스택을 사용한다. 그래서 기존의 포인터를 보관하고 현재의 스택 포인터를 베이스로 잡아서 시작한다.)
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
함수의 호출 규약
대표적으로 _cdec, _stdcall, _fastcall, _thiscall 4가지가 있다.
여기서 확인할 것은 디스어셈블된 코드를 보고 이것이 어떤 콜링 컨벤션(calling convention)에 해당하는지 파악하는 것이다.
이유는 리버스 엔지니어링을 할 때 call문을 보고 이 함수의 인자가 몇 개이고 어떤 용도로 쓰이는지 분석하기 위해서다.
_cdecl 방식
: 함수를 호출한 곳(함수 밖)에서 스택을 보정하는 방식
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 :
main:
push 2
push 1
call calling.00401000
add esp, 8
- call calling.00401000 밑에 add esp, 8로 봐서 함수를 호출한 곳에서 스택을 보정하는 방식이다.
- 파라미터 2개; add esp, 8 그리고 push 문이 2개라는 점을 봐서 4바이트 파라미터가 두 개라는 것.
- 리턴 값이 숫자; 맨 마지막 부분에 eax에 들어가는 값이 숫자라는 것을 봐서 리턴 값은 주소 같은 값이 아닌, 숫자임을 확인
_stdcall 방식
main:
push 2
push 1
call calling.00401000
-> 같은 코드인데 방식에 따라 차이가 있다.
_cdecl 방식과 달리 _stdcall 방식은 add esp, 8을 사용한 코드가 없다.
이건 main() 안에서 sum()을 사용한 뒤 어떤한 스택 처리도 없다는 뜻이다. 대신 sum() enldp return 8을 사용한 것으로 봐서 함수 안에서 스택을 처리했다.
대표적으로 Win32 API가 _stdcall 방식을 사용
_fastcall 방식
main:
push ebp
mov ebp, esp
...
call sub_401000
xor eax, eax
pop ebp
retn
파라미터가 2개 이하일 경우, 인자를 push로 넣지 않고 ecx와 edx 레지스터를 이용한다. 메모리를 이용하는 것보다 레지스터를 사용하는 것이 속도가 훨씬 빠르다.
_thiscall 방식
C++의 클래스에서 이용되는 방법으로 특징으로 현재 객체의 포인터를 ecx에 전달한다는 것이 있다.
객체에서 여러 클래스를 정의하다보면 모양은 동일해도 서로 다른 메모리 번지에 존재하게 된다. 이것을 구분해주기 위해 this 포인터를 사용하는데 바로 ecx로 전달되는 값이 this 포인터가 된다.
push edx
lea ecx, [ebp-4]
call 402000
if 문
cmp명령에서 비교
jnz, jn로 점프
반복문
구조체와 API Call
결론
3. C++클래스와 리버스 엔지니어링
C++ 분석의 난해함
클래스 뼈대
클래스의 수명과 전역변수
객체의 동적 할당과 해제
생성자와 소멸자
캡슐화 분석
다형성 구조 파악
4. DLL 분석
DLL 번지 계산법
재배치를 고려한 방법
push
점프문
번지고정
익스포트 함수
DLLAttach/DllDetach 찾기
패킹된 DLL의 DllMain() 찾기
DisableThreadLibraryCalls로 찾기
'리버싱 > 리버스 엔지니어링 바이블' 카테고리의 다른 글
03; 연산 루틴 리버싱 (0) | 2023.01.18 |
---|---|
02; 리버스 엔지니어링 중급 (0) | 2023.01.18 |
책 : 리버스 엔지니어링 바이블 시작 (0) | 2022.08.20 |