리버싱
리버싱이란?
리버스 엔지니어링(Reverse Engineering)
Reverse - 뒤집다
Engineering - 공학
- 역공학이라고 해석 가능
- 완성된 프로그램을 해체하고 분석하여 구조와 기능, 디자인을 파악하는 기술을 의미
- 리버싱은 각종 악성코드나 불법 프로그램에 대응을 위해 사용
- 구조, 기능, 동작 등을 역으로 추적하여 분석하고 원리를 이해하며 부족한 부분을 보완하며 새로운 기능 등을 추가하는 작업
리버싱 방법
- 정적 분석
파일의 겉모습을 관찰하여 분석하는 방법
파일을 열지 않고 파일 종류, 헤더, 디스어셈블리어, 디컴파일러로 분석
디스어셈블러를 이용해서 내부코드와 구조를 확인하는 방법
- 동적 분석
파일을 실행하며 코드 흐름과 메모리 상태 등으로 분석하는 방법
레지스트리, 네트워크 등을 관찰하면서 프로그램의 행위를 분석
디버거를 이용하여 프로그램 내부 구조와 동작 원리를 분석리버싱을 배우기 위해 필요한 지식
- 컴퓨터 구조
- ISA
- Byte Ordering
- Encoding/Decoding
- 운영 체제
- 메모리 구조, 컴파일, 인터프리터
어셈블리어 (Assembler)
구조
section.data
- 데이터 영역
- 초기값이 있는 데이터를 저장 (문자열, 상수 등)
section.bss
- 비어 있는 데이터 영역
- 초기값 없이 공간만 필요한 변수를 저장 (입력 버퍼 등)
section.text
- 코드 영역
- 명령어가 들어가는 부분 (기계어로 번역될 명령어/코드)
section.text
global_start;
- 링커에게 시작할 위치를 알려줌
_start:
- 실행할 명령어 작성
- 프로그램이 실행될 때 가장 먼저 실행되는 지점
- C언어의 main() 함수와 비슷하지만, 운영체제가 직접 호출하는 주소
아키텍처 (Architecture)
아키텍처란?
- CPU가 명령어를 처리하는 방식을 나타냄
- 하드웨어 시스템의 전반적인 구조와 동작을 나타냄
주요 아키텍처 비교
x86 x86-64 ARM ARM64 주소 크기 32bit 64bit 32bit 64bit 레지스터 EAX,EBX RAX,RBX R0~R15 X0~x30 엔디안 Little Little Little / Big Little (일반적으로)x86-64는 x86 아키텍처와 호환되는 64bit 아키텍처
- 32 / 64bit 아키텍처 -> 32 / 64bit는 CPU가 한 번에 처리 할 수 있는 데이터의 크기
WORD
- 하나의 기계어 명령어, 연산을 통해 저장된 장치에서 컴퓨터 프로세서로 옮겨 놓을 수 있는 데이터 단위
- WORD의 길이는 컴퓨터의 데이터 버스 크기와 같음
- 한번의 작업으로 저장장치에서 프로세서 레지스터로 데이터를 이동시킴
-> CPU가 한 번에 처리할 수 있는 데이터의 크기
x64 Register
레지스터의 종류
범용 레지스터
데이터 연산을 위해 사용되는 레지스터
- EAX 산술 연산 및 논리 연산 수행 + 함수의 반환값 저장
- EBX 메모리 주소 저장
- ECX 반복문 사용 시 카운터로 사용
- EDX EAX와 같이 사용 + 큰 수의 곱셈과 나눗셈 연산
- EDI 복사할 때 목적지 주소 저장
- ESI 데이터를 조작하거나 복사할 때 데이터의 주소 저장
- ESP 메모리 스택의 끝 지점 주소 포인터
- EBP 메모리 스택의 첫 지점 주소 포인터
- EIP 다음에 실행해야 할 명령어의 주소 포인터
세그먼트 레지스터
아키텍처 메모리를 세그먼트 단위로 접근할 때 사용되는 특수한 레지스터
- CS 기계 명령 포함 코드 세그먼트의 시작 주소를 가리킴
- DS 프로그램에 정의된 데이터 영역의 시작 주소를 가리킴
- SS 연산 결과 등을 임시로 저장 또는 삭제할 때 사용하는 스택 영역의 시작부분을 가리킴
- ES 추가로 사용된 데이터 세그먼트의 주소를 가리킴
- FS 여분 레지스터
- GS 여분 레지스터
플래그 레지스터
CPU가 연산을 수행한 후 결과의 상태를 저장하는 특수한 레지스터
-> 조건문 등에 사용되어짐
ZF 연산결과가 0일 경우 참
CF 부호 없는 숫자의 연산 결과가 비트 범위를 넘으면 참
AF 연산 결과 하위 4 bit에서 비트 범위를 넘으면 참
OF 부호 있는 숫자의 연산 결과가 비트 범위를 넘으면 참
SF 연산 결과가 음수면 참
PF 연산 결과에서 1로 된 비트의 수가 짝수면 참
DF 문자열 조작에서 참이면 레지스터 값 감소, 거짓이면 증가
TF 디버깅에 사용명령어 포인터 레지스터
- 프로그램이 기계어로 이루어져 있을 때, CPU가 실행시킬 코드를 가리킴
- 명령어 레지스터는 rip이며, 8byte의 크기를 지님
함수 호출 규약 (Calling Convention)
- 함수 호출 시 호출자와 피호출자 간에 어떻게 데이터를 주고받을지에 대한 규칙
- 함수에 인자를 전달 방법 / 반환값 저장 위치 / 함수 호출 전후에 레지스터나 스택을 다룰 방법에 대해 정해 놓은 약속
x86 호출 규약
- Cdecl
인자 전달은 오른쪽에서 왼쪽으로 전달하고 호출자
기본 C언어 규약, 여러 인자를 지원하며 인자 함수를 지원- Stdcall
인자 전달은 오른쪽에서 왼쪽으로 전달하고 피호출자
WinAPI에서 사용하고 함수가 끝나면 스택을 정리- Fastcall
앞의 2~3개 인자는 레지스터에 전달하고 나머지는 스택에 전달하고 피호출자이며 ECX, EDX를 사용하고 성능 향상이 목적이다.- Thiscall
this는 ECX에 전달하고 나머지는 스택에 전달하고 호출자
C++ 클래스 멤버 함수용으로 사용x86-64 프롤로그와 에필로그
- 함수를 호출하면 컴파일러는 정확한 규칙을 따라 어셈블리 코드를 생성
- 프롤로그(Prologue)와 에필로그(Epilogue)
프롤로그(Prologue)
- push rbp --- 이전 함수의 프레임 포인터 저장
- mov rbp, rsp --- 현재 스택 포인터를 기준 프레임 포인터로 설정
- sub rsp, N --- 지역 변수나 정렬 공간 확보 (N은 16의 배수)
에필로그(Epilogue)
- mov rsp, rbp --- 스택 포인터 복원
- pop rbp --- 이전 프레임 포인터 복원
- ret --- 호출자 주소로 복귀
함수가 끝나면 호출한 쪽(Caller)으로 돌아가야 함
과제
Hello Layer7 12번 출력
section .data
msg db 'Hello Layer7', 10 ; "Hello Layer7" 문자열 저장
len equ $ - msg
section .text
global _start
_start:
; 1
mov eax, 4 ; 시스템 호출 번호 4 : sys_write <- write 함수
mov ebx, 1 ; 파일 디스크립터 1 : 출력 (stdout)
mov ecx, msg ; 출력할 문자열의 주소를 ECX에 이동
mov edx, len ; 출력할 길이를 EDX에 이동
int 0x80 ; 커널 인터럽트를 호출해서 write 실행
; 2
int 0x80
; 3
int 0x80
; 4
int 0x80
; 5
int 0x80
; 6
int 0x80
; 7
int 0x80
; 8
int 0x80
; 9
int 0x80
; 10
int 0x80
; 11
int 0x80
; 12
int 0x80
; exit
mov eax, 1 ; 시스템 호출 번호 1 : sys_exit
xor ebx, ebx ; ebx = 0 (exit 코드 0)
int 0x80 ; 커널 인터럽트 호출 -> 종료
section .data
- msg : 출력할 문자열 "Hello Layer7"에 줄바꿈 문자(ASCII 10, 즉 \n)
- len : 문자열의 길이를 계산.
$는 현재 주소, msg는 시작 주소 → $ - msg는 문자열 전체 길이section .text
- 프로그램의 시작을 _start 라벨을 외부(운영체제)에서 사용할 수 있도록 지정
- Linux 커널이 실행을 시작할 때 _start 레이블을 기준으로 실행
_start:
- 문자열 한 줄을 출력
- int 0x80만 반복되어 총 12번 호출되며, 같은 메시지를 반복 출력
반복 구조
- eax, ebx, ecx, edx 값이 바뀌지 않음
따라서 int 0x80 반복 호출시 같은 메시지를 반복 출력- "Hello Layer7" 12번 출력