본문 바로가기
게임해킹/knockon 부트캠프

[1주차 TIL] KnockOn Bootcamp - 헤더파일

by HHack 2025. 4. 7.
반응형

1. 개요

✅ 표준 헤더 파일이란?

<stdio.h>, <stdlib.h>, <string.h> 같은 헤더 파일은 C 표준 라이브러리의 일부이다.
이 파일들에는 함수 원형(prototypes), 매크로 정의, 구조체 선언, 상수 등이 정의되어 있다.
실제 구현은 .c나 .o 파일에 들어 있고, 컴파일 할 때 연결됩니다.

// stdio.h 내부 일부 예시
int printf(const char *, ...);
int scanf(const char *, ...);
FILE *fopen(const char *filename, const char *mode);

🛠 사용자 정의 헤더 파일 만드는 법

일반적으로는 아래와 같이 정의합니다.

  • .h 파일에는 선언만
  • .c 파일에는 구현

예시(main.c, hacker.h)

//main.c
#include "hacker.h"

int main(){
    hacker* qwertyou = new_hacker();
    printf("%s's age is %d\n",qwertyou->name, qwertyou->age);
    printf("%s's level is %d\n",qwertyou->name, qwertyou->level);


    printf("\n\n-----qwertyou's skill list-----\n");
    for(int i=0;i<qwertyou->skill_num;i++){
        printf("%s's skill%d : %s\n",qwertyou->name, i, qwertyou->skill_list[i]);
    }
}
//hacker.h
#pragma once
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

typedef struct hacker{
    char name[50];
    unsigned int age;
    char** skill_list;
    int skill_num;
    int level;
}hacker;

int input_name(hacker*);
int input_age(hacker*);
int input_new_skill(hacker*);
void increase_size(char***);

hacker* new_hacker() {
    hacker* new_ptr = (hacker*)malloc(sizeof(hacker));

    memset(new_ptr->name, '\0', sizeof(new_ptr->name));
    new_ptr->skill_num = 0;
    new_ptr->level = 1;

    new_ptr->skill_list = (char**)malloc(sizeof(char*));

    input_name(new_ptr);
    input_age(new_ptr);
    input_new_skill(new_ptr);

    return new_ptr;
}


int input_name(hacker* ptr){
    write(1,"Input the name : ",17);
    return scanf("%s",ptr->name);
}

int input_age(hacker* ptr){
    printf("Input the age : ");
    return scanf("%u",&ptr->age);
}

int input_new_skill(hacker* ptr){
    write(1, "Input new Skill : ", 18);
    
    char buf[50] = {0,};
    scanf("%s",buf);

    char* tmp = (char*)malloc(strlen(buf)+1);
    strcpy(tmp,buf);

    ptr->skill_num++;
    increase_size(&(ptr->skill_list), ptr->skill_num - 1);

    ptr->skill_list[ptr->skill_num-1] = tmp;
    return ptr->skill_num;
}

void increase_size(char*** skill_list, int current_size) {
    char** new_ptr = (char**)malloc(sizeof(char*) * (current_size + 1));

    for (int i = 0; i < current_size; i++) {
        new_ptr[i] = (*skill_list)[i];
    }

    free(*skill_list);
    *skill_list = new_ptr;
}

1. 리버싱과의 연관성

(1) 헤더 파일은 '정의된 세계'의 지도다

리버싱 입장에서 실행 파일을 분석할 때 가장 먼저 하는 일 중 하나는:

  • “이 함수가 어떤 구조체를 쓰고 있는지”
  • “이 포인터가 가리키는 게 어떤 데이터인지”
  • “이 함수가 실제 어떤 기능을 수행하는지”

이걸 빠르게 이해하려면?
👉 결국 원래 소스 코드의 정의부(header)추측하거나 유추해야 한다.

(2) 구조체(struct)는 메모리의 청사진이다.

typedef struct hacker {
    char name[50];          // +0
    unsigned int age;       // +50
    char** skill_list;      // +54
    int skill_num;          // +58
    int level;              // +62
} hacker;

이건 컴파일된 후 메모리에 연속적인 구조로 저장됩니다.
Cheat Engine이나 x64dbg로 이걸 역추적할 때 다음과 같은 상황이 생길 수 있습니다.

  • 어떤 주소에서 "qwertyou"라는 문자열이 발견.
  • 바로 옆 주소에서 int: 30, 포인터: 0x123456, 또 다른 int: 1이 나옴.
  • 👉 이걸 보면, 리버서 입장에서는 이건 구조체처럼 생겼다고 추정하게 됨.

즉, 구조체의 정의(=헤더 파일 내용)를 잘 알고 있으면

  • 디스어셈블된 코드나 메모리 덤프에서 데이터 해석이 훨씬 쉬워짐.
  • 그 반대로, 헤더 구조를 모르면 메모리 덤프를 보고도 뭔지 도무지 모름.

(3) 함수 선언은 시그니처 분석에 도움 된다

int input_name(hacker*);

이 함수는 구조체 포인터를 받아 name을 채웁니다. 하지만 바이너리에서는 아래와 같이 작동합니다.

mov rdi, rax        ; 구조체 주소를 rdi에 넣음
call input_name

이런 식으로 보이기 때문에, 함수의 인자와 리턴 타입이 헤더에 정의된 시그니처가 없으면 어셈블리만 보고 추론하는 데 훨씬 더 많은 시간이 걸립니다.

(4) 실제 리버싱 예시 - 게임 해킹

예를 들어 너가 어떤 RPG 게임을 리버싱한다고 해보겠습니다.

  • .text 영역에서 플레이어 관련 함수들을 발견.
  • PlayerStruct 비슷한 주소를 찾았는데 뭔가 name, HP, level이 있는 것 같다고 추정.

그럼 내부적으로 이렇게 추정하게 됩니다.

typedef struct Player {
    char name[32];
    int hp;
    int level;
    float x, y, z;
    ...
} Player;

이건 마치 hacker.h 구조체를 거꾸로 유추하는 과정입니다. 즉, 리버서가 항상 하는 일이 바로 ‘헤더를 상상’하는 작업입니다.

(5) 결론: 헤더 파일을 공부해야 하는 이유 (리버싱 관점)

이유 설명
📍 메모리 구조 이해 구조체 정의는 메모리 해킹/치트 엔진 분석의 핵심
🔍 함수 분석 함수 원형은 디스어셈블된 코드에서 인자/리턴 타입 추정에 필요
🧠 추론력 향상 리버스 시 '원래 코드의 형태'를 상상하는 데 훈련이 됨
💉 후킹/패칭 어떤 함수에 인자를 어떻게 넘기고 후킹할지 구조가 있어야 판단 가능
⚙️ 리버서의 습관 실제로 고급 리버서들은 바이너리를 보면 .h부터 상상해
반응형