DreamHack의 Beginners 로드맵을 기반으로 정리한 글입니다.
System Hacking
시스템 해킹(System Hacking)은 컴퓨터 프로그램의 행위를 조작하여 공격자가 원하는 행동을 실행하도록 하는 공격 전반을 일컫는다. '시스템 해킹'은 컴퓨터에서 구동되는 모든 프로그램에 대한 해킹에 적용 가능한 말이다. 흔히 웹 해킹이라고 부르는 인터넷 환경에서 발생하는 해킹도 넓게 보면 웹 브라우저라는 프로그램 상에서 발생하는 취약점이기 때문이다.
System Hacking 관련 취약한 프로그램들은 대부분 C 언어로 구현되었다. C 언어는 효율성과 유연함에 큰 강점이 있어 다양한 애플리케이션을 구현하는데 사용된다. 여기에는 운영체제, 게임 등 굉장히 복잡한 프로그램도 포함된다.
C 언어 기반 프로그램에서 발생할 수 있는 취약점에 대한 기본적인 지식과 이를 이해하기 위한 배경지식인 프로그램이 시스템 상에서 작동하는 방식, 프로그램을 분석하는데 필요한 도구, 취약점의 유형 및 이를 공략하는(익스플로잇하는) 방법론에 대해 알아야 한다.
시스템 해킹이 요구하는 배경 지식의 범주는 상당히 넓은데, 예를 들어 가장 기초적인 취약점인 스택 버퍼 오버플로우(Stack Buffer Overflow)를 이해하려면 assembly 레벨에서의 메모리 사용과 같은 시스템 프로그래밍 관련 지식을 요구한다.
시스템 해킹의 목표는 통상적으로 공격 대상 프로그램이 구동되는 서버의 임의 유저와 같은 권한을 획득하는 것이다. 원격 환경에 있는 공격자가 타겟 서버에서 운영체제의 명령어를 실행할 수 있는 공격을 원격 코드 실행(Remote Code Execution)이라고 부른다.
#include <stdio.h>
void fun (int x, int y)
{
int z;
char buf[100];
z = x;
read (0, buf, y);
printf ("x is %d\n", x);
//printf (“z is %d\n”, z);
}
int main (int argc, char* argv[])
{
int a, b;
a = 30;
b = 300;
fun (a,b);
}
- 위 코드가 실행 되었을 때, 프로그램이 어떻게 동작할지 예상할 수 있다
- calling convention이 무엇인지 설명할 수 있다.
- stack frame이 무엇인지 설명할 수 있다.
- register가 무엇인지 설명할 수 있으며, C 코드가 진행됨에 따라 레지스터 값들이 어떻게 변할지 유추할 수 있다.
- 시작 시점부터 종료 시점까지 main 함수의 stack의 변화를 설명할 수 있다
- 32bit 프로그램일 때와 64bit 프로그램일 때 stack 값들의 차이점을 설명할 수 있다.
- fun 함수의 취약한 부분이 무엇인지 찾을 수 있다.
- fun 함수의 취약점으로 인해 32bit와 64bit환경에서 x의 출력값이 어떻게 달라지는지 설명할 수 있다.
- x가 아닌 z를 출력하는 경우, 32bit와 64bit환경에서 z의 출력값이 어떻게 달라지는지 설명할 수 있다.
- Figure 1을 컴파일한 바이너리가 주어졌을 때, 이를 익스플로잇하여 셸(shell)을 획득하는 공격 코드를 작성할 수 있다.
위 문항 중 체크할 수 있는 답변이 없으면 리버싱 엔지니어링을 우선적으로 공부해야 한다.
Reverse Engineering
리버스 엔지니어링(Reverse Engineering)은 줄여서 리버싱으로도 부른다. 이름에서 알 수 있듯이 리버스 엔지니어링은 무언가를 설계하고 제작하는 '엔지니어링' 과정을 정반대로 수행하는 것이다. 컴퓨터 프로그래밍적 관점에서는 개발자가 소스 코드를 작성하고, 컴파일한 산출물에서부터 시작하는 것이다. 프로그램의 동작을 직접 실행 시켜보면서 소스 코드의 내용을 추측하는 것도 가능한 방법이고, 컴파일된 결과물의 데이터를 분석하여 소스 코드의 내용을 유추, 복구 해볼 수도 있다.
위에서 언급한 산출물을 흔히 '프로그램' 또는 '이진 파일'이라고 부른다. 컴파일되지 않은 코드 역시 '프로그램'이라고 부르는 경우가 있어 '이진 파일'이 조금 더 보편적으로 사용된다. 이진 파일은 사람이 눈으로 식별하기 어려운 데이터로 이루어져 있어, 단순히 이 데이터 값을 본다고 해서 어떤 동작을 하는지 알아내기가 어렵다.
이를 도와주는 것이 '디스어셈블러(disassembler)', '디컴파일러(decompiler)'와 같은 도구이다. 사람이 식별하기 어려운 데이터를, 보다 알아보기 쉬운 형태로 변형 시켜주는 역할을 한다.
이러한 리버싱을 간단하게 정리하면 '해킹'이면서 동시에 '분석'을 하는 분야이다.
리버스 엔지니어링에 대해 알기 위해선 해당 프로그램을 구성하는 프로그래밍 언어와 x86 assembly를 읽고 이해할 수 있어야 한다. 어셈블리어가 한 종류만 있는 것은 아니지만 기본적으로 복잡하게 구성된 x86 assembly를 이해할 수 있다면 다른 아키텍처의 어셈블리어는 보다 손쉽게 습득할 수 있을 것이다. 어셈블리어를 이해한다는 말은, 프로그램이 low-level에서 메모리를 어떤 방식으로 사용하는지 이해하고 있다는 말과 같다. 디컴파일러가 없는 환경에서 리버싱을 해야할 수도 있기 때문에 어셈블리어를 읽고 이해하는 능력은 리버싱을 위해 꼭 필요하다.
#include <stdio.h>
int enc[17] = {75, 104, 111, 111, 114, 35, 71, 117, 104, 100, 112, 107, 100, 102, 110, 36, 3};
int main(){
int key = 0;
int dec[17];
for(int i=0; i<17; i++){
printf("%c", enc[i]);
}
printf("\nYour input: ");
scanf("%d", &key);
for(int i=0; i<17; i++){
dec[i] = enc[i] - key;
printf("%c", dec[i]);
}
printf("\n");
return 0;
}
Dump of assembler code for function main:
0x00005555555551a9 <+0>: endbr64
0x00005555555551ad <+4>: push rbp
0x00005555555551ae <+5>: mov rbp,rsp
0x00005555555551b1 <+8>: sub rsp,0x60
0x00005555555551b5 <+12>: mov rax,QWORD PTR fs:0x28
0x00005555555551be <+21>: mov QWORD PTR [rbp-0x8],rax
0x00005555555551c2 <+25>: xor eax,eax
0x00005555555551c4 <+27>: mov DWORD PTR [rbp-0x5c],0x0
0x00005555555551cb <+34>: mov DWORD PTR [rbp-0x58],0x0
0x00005555555551d2 <+41>: jmp 0x5555555551f6 <main+77>
0x00005555555551d4 <+43>: mov eax,DWORD PTR [rbp-0x58]
0x00005555555551d7 <+46>: cdqe
0x00005555555551d9 <+48>: lea rdx,[rax*4+0x0]
0x00005555555551e1 <+56>: lea rax,[rip+0x2e38] # 0x555555558020 <enc>
0x00005555555551e8 <+63>: mov eax,DWORD PTR [rdx+rax*1]
0x00005555555551eb <+66>: mov edi,eax
0x00005555555551ed <+68>: call 0x555555555080 <putchar@plt>
0x00005555555551f2 <+73>: add DWORD PTR [rbp-0x58],0x1
0x00005555555551f6 <+77>: cmp DWORD PTR [rbp-0x58],0x10
0x00005555555551fa <+81>: jle 0x5555555551d4 <main+43>
0x00005555555551fc <+83>: lea rax,[rip+0xe01] # 0x555555556004
0x0000555555555203 <+90>: mov rdi,rax
0x0000555555555206 <+93>: mov eax,0x0
0x000055555555520b <+98>: call 0x5555555550a0 <printf@plt>
0x0000555555555210 <+103>: lea rax,[rbp-0x5c]
0x0000555555555214 <+107>: mov rsi,rax
0x0000555555555217 <+110>: lea rax,[rip+0xdf4] # 0x555555556012
0x000055555555521e <+117>: mov rdi,rax
0x0000555555555221 <+120>: mov eax,0x0
0x0000555555555226 <+125>: call 0x5555555550b0 <__isoc99_scanf@plt>
0x000055555555522b <+130>: mov DWORD PTR [rbp-0x54],0x0
0x0000555555555232 <+137>: jmp 0x55555555526f <main+198>
0x0000555555555234 <+139>: mov eax,DWORD PTR [rbp-0x54]
0x0000555555555237 <+142>: cdqe
0x0000555555555239 <+144>: lea rdx,[rax*4+0x0]
0x0000555555555241 <+152>: lea rax,[rip+0x2dd8] # 0x555555558020 <enc>
0x0000555555555248 <+159>: mov eax,DWORD PTR [rdx+rax*1]
0x000055555555524b <+162>: mov ecx,DWORD PTR [rbp-0x5c]
0x000055555555524e <+165>: sub eax,ecx
0x0000555555555250 <+167>: mov edx,eax
0x0000555555555252 <+169>: mov eax,DWORD PTR [rbp-0x54]
0x0000555555555255 <+172>: cdqe
0x0000555555555257 <+174>: mov DWORD PTR [rbp+rax*4-0x50],edx
0x000055555555525b <+178>: mov eax,DWORD PTR [rbp-0x54]
0x000055555555525e <+181>: cdqe
0x0000555555555260 <+183>: mov eax,DWORD PTR [rbp+rax*4-0x50]
0x0000555555555264 <+187>: mov edi,eax
0x0000555555555266 <+189>: call 0x555555555080 <putchar@plt>
0x000055555555526b <+194>: add DWORD PTR [rbp-0x54],0x1
0x000055555555526f <+198>: cmp DWORD PTR [rbp-0x54],0x10
0x0000555555555273 <+202>: jle 0x555555555234 <main+139>
0x0000555555555275 <+204>: mov edi,0xa
0x000055555555527a <+209>: call 0x555555555080 <putchar@plt>
0x000055555555527f <+214>: mov eax,0x0
0x0000555555555284 <+219>: mov rdx,QWORD PTR [rbp-0x8]
0x0000555555555288 <+223>: sub rdx,QWORD PTR fs:0x28
0x0000555555555291 <+232>: je 0x555555555298 <main+239>
0x0000555555555293 <+234>: call 0x555555555090 <__stack_chk_fail@plt>
0x0000555555555298 <+239>: leave
0x0000555555555299 <+240>: ret
End of assembler dump.
- 프로그램이 실행 되었을 때 프로그램이 어떠한 동작을 하는지 설명할 수 있다.
- c 코드의 enc[17] 와 key 가 메모리에 배치될 때 각각 어떠한 메모리 세그먼트에 위치할지 대략적으로 설명할 수 있다.
- 메모리와 레지스터의 차이를 설명할 수 있다.
- 명령어 집합 구조(Instruction Set Architecture, ISA)가 무엇인지 설명할 수 있다.
- c 코드가 어떠한 과정을 거쳐 컴파일 되는지, 그리고 컴파일 과정에서 코드가 어떻게 바뀌는지 설명할 수 있다.
- c, 어셈블리, 바이너리 코드 각각의 특징과 그 차이점에 대해 설명할 수 있다.
- 어셈블리 코드를 보고 main 함수의 stack frame 구조를 파악할 수 있다.
- 어셈블리 코드를 보고 메모리와 레지스터를 구분할 수 있다.
- 어셈블리를 보고 동일한 기능을 하는 C 코드를 작성할 수 있다
- c 코드에서 int형 배열이 문자로 출력될 수 있는 이유를 설명할 수 있다.
- “Hello dreamhack!” 을 출력하도록 하는 key 값을 구할 수 있다.
만약 4번까지 답을 할 수 없거나 질문이 이해가 안 된다면 먼저 C 언어를 공부한 후 리버싱에 대해 공부해야 한다.
'Security > System Hacking' 카테고리의 다른 글
| Beginners: DH101 - 3 (코딩) (5) | 2025.03.01 |
|---|---|
| Beginners: DH101 - 2 (리눅스 사용법) (0) | 2025.02.11 |
| Beginners: DH101 - 1 (컴퓨터 과학 기초) (3) | 2025.02.06 |
| System Hacking: Shellcode - 2 (1) | 2025.02.01 |
| System Hacking: Shellcode - 1 (3) | 2025.01.23 |