Security/System Hacking

Beginners: DH101 - 4 (101)

arsenic-dev 2025. 3. 1. 01:27

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);
}

 

  1. 위 코드가 실행 되었을 때, 프로그램이 어떻게 동작할지 예상할 수 있다
  2. calling convention이 무엇인지 설명할 수 있다.
  3. stack frame이 무엇인지 설명할 수 있다.
  4. register가 무엇인지 설명할 수 있으며, C 코드가 진행됨에 따라 레지스터 값들이 어떻게 변할지 유추할 수 있다.
  5. 시작 시점부터 종료 시점까지 main 함수의 stack의 변화를 설명할 수 있다
  6. 32bit 프로그램일 때와 64bit 프로그램일 때 stack 값들의 차이점을 설명할 수 있다.
  7. fun 함수의 취약한 부분이 무엇인지 찾을 수 있다.
  8. fun 함수의 취약점으로 인해 32bit와 64bit환경에서 x의 출력값이 어떻게 달라지는지 설명할 수 있다.
  9. x가 아닌 z를 출력하는 경우, 32bit와 64bit환경에서 z의 출력값이 어떻게 달라지는지 설명할 수 있다.
  10. 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.
  1. 프로그램이 실행 되었을 때 프로그램이 어떠한 동작을 하는지 설명할 수 있다.
  2. c 코드의 enc[17] 와 key 가 메모리에 배치될 때 각각 어떠한 메모리 세그먼트에 위치할지 대략적으로 설명할 수 있다.
  3. 메모리와 레지스터의 차이를 설명할 수 있다.
  4. 명령어 집합 구조(Instruction Set Architecture, ISA)가 무엇인지 설명할 수 있다.
  5. c 코드가 어떠한 과정을 거쳐 컴파일 되는지, 그리고 컴파일 과정에서 코드가 어떻게 바뀌는지 설명할 수 있다.
  6. c, 어셈블리, 바이너리 코드 각각의 특징과 그 차이점에 대해 설명할 수 있다.
  7. 어셈블리 코드를 보고 main 함수의 stack frame 구조를 파악할 수 있다.
  8. 어셈블리 코드를 보고 메모리와 레지스터를 구분할 수 있다.
  9. 어셈블리를 보고 동일한 기능을 하는 C 코드를 작성할 수 있다
  10. c 코드에서 int형 배열이 문자로 출력될 수 있는 이유를 설명할 수 있다.
  11. “Hello dreamhack!” 을 출력하도록 하는 key 값을 구할 수 있다.

만약 4번까지 답을 할 수 없거나 질문이 이해가 안 된다면 먼저 C 언어를 공부한 후 리버싱에 대해 공부해야 한다.