ptrace 함수

shrew·2025년 3월 30일

ptrace

Concept

ptrace 함수는 'process trace'의 약자로, 리눅스와 같은 유닉스 계열 시스템에서 디버깅이나 프로세스를 제어를 위해 사용되는 시스템 콜이다. 한 프로세스가 다른 프로세스를 감시하거나 제어할 수 있게 해주고, 이를 통해, 자식 프로세스의 메모리나 레지스터를 읽거나 쓸 수 있고, 시그널을 가로채거나 강제로 보낼 수 있으며, 자식 프로세스의 시스템 호출을 추적할 수도 있다.

How to use

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

ptrace 함수의 원형은 위와 같다. ptrace 함수는 리눅스 시스템 콜이기 때문에 리턴값은 long형이고, 사용하려면 'sys/ptrace.h' 헤더 파일을 포함해야 한다. 함수 원형에 나온 매개 변수를 먼저 알아보자.

request

가장 중요한 매개 변수로, 어떤 행동을 할 것인지 지정하는 명령 코드이다.

예시
PTRACE_TRACEME : 자식 프로세스가 디버깅 허용을 선언함
PTRACE_ATTACH : 외부 프로세스를 attach해서 디버깅 시작
PTRACE_PEEKDATA : 대상 프로세스 메모리에서 데이터를 읽음
PTRACE_POKEDATA : 대상 프로세스 메모리에 데이터를 씀
PTRACE_CONT 중단된 : 프로세스를 계속 실행시킴
PTRACE_SYSCALL : 다음 시스템 콜 전에 중단되게 함
PTRACE_GETREGS : 대상 프로세스의 레지스터 상태를 읽음

pid

제어하려는 대상 프로세스의 ID이다. 단, PTRACE_TRACEME 요청일 때는 자기 자신을 의미하기 때문에 항상 '0'이 된다.

addr

request에 따라 의미가 달라지는 주소값 또는 위치이다.

예시
PTRACE_PEEKDATA, POKEDATA - 메모리 접근을 원하는 주소
PTRACE_GETREGS, SETREGS - 무시되거나 구조체에 따라 동작
PTRACE_CONT - 다음 중단 시 받을 시그널 번호 (보통 0이다.)

data

이것도 request에 따라 의미가 달라지는 값이다.

예시
PTRACE_PEEKDATA - 무시됨 (리턴값이 읽은 데이터이다.)
PTRACE_POKEDATA - 사용할 값
PTRACE_GETREGS - 레지스터 값을 저장할 구조체 포인터
PTRACE_SETREGS - 레지스터 값을 담고 있는 구조체 포인터
PTRACE_ATTACH - 무시됨 (NULL로 줘도 된다.)

실습

Example code

아래는 Dreamhack - ptrace_block 문제의 주요 함수들이다. 동작 과정을 살펴보자.

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int fd; // [rsp+Ch] [rbp-514h]
  _QWORD v5[32]; // [rsp+10h] [rbp-510h] BYREF
  _BYTE buf[1032]; // [rsp+110h] [rbp-410h] BYREF
  unsigned __int64 v7; // [rsp+518h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  memset(v5, 0, sizeof(v5));
  puts("generate your flag!");
  printf("> ");
  __isoc99_scanf("%255s", v5);
  sub_13F1(v5, buf, 256LL);
  fd = open("./out.txt", 1);
  write(fd, buf, 0x100uLL);
  close(fd);
  return 0LL;
}

main 함수에서는 'v5' 변수에 문자열을 입력 받고, sub_13F1 함수로 값을 넘기고 있다.

__int64 __fastcall sub_13F1(__int64 a1, __int64 a2, int a3)
{
  _BYTE v5[256]; // [rsp+20h] [rbp-120h] BYREF
  _QWORD v6[4]; // [rsp+120h] [rbp-20h] BYREF

  v6[3] = __readfsqword(0x28u);
  v6[0] = 0LL;
  v6[1] = 0LL;
  AES_set_encrypt_key(&unk_4010, 128LL, v5);
  AES_cbc_encrypt(a1, a2, a3, v5, v6, 1LL);
  return 0LL;
}
.data:0000000000004010 ; _BYTE byte_4010[16]
.data:0000000000004010 byte_4010       db 41h, 28h, 19h, 4Eh, 0A5h, 7Ch, 0A1h, 41h, 13h, 0CFh
.data:0000000000004010                                         ; DATA XREF: sub_12C9+83↑o
.data:0000000000004010                                         ; sub_12C9+93↑o ...
.data:000000000000401A                 db 88h, 0ACh, 2Ah, 0F0h, 0B7h, 0DAh
.data:000000000000401A _data           ends

main 함수에서 값을 넘겨 받은 sub_13F1 함수에서는 AES CBC(128bit) 암호화를 진행한다. 여기서 사용되는 키는 'byte_4010'이다.

void sub_12C9()
{
  unsigned int v0; // eax
  int v1; // ebx
  int v2; // [rsp+4h] [rbp-1Ch]
  int i; // [rsp+8h] [rbp-18h]
  int j; // [rsp+Ch] [rbp-14h]

  v0 = time(0LL);
  srand(v0);
  v2 = 1;
  for ( i = 0; i <= 4095; ++i )
  {
    v1 = ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
    v2 *= v1 * rand();
  }
  for ( j = 0; j <= 14; ++j )
    byte_4010[j + 1] += byte_4010[j] + v2;
}

해당 데이터는 sub_12C9 함수에서도 나오는데 해당 함수의 주소를 확인해 보면 '.init_array'에 존재하는데 이는 main 함수가 시작되기 전 수행된다. 함수를 살펴보면 디버깅 중이라면, 'byte_4010' 값을 변조한다.

int sub_1392()
{
  unsigned int v0; // eax
  int result; // eax
  int i; // [rsp+8h] [rbp-8h]
  char v3; // [rsp+Ch] [rbp-4h]

  v0 = rand();
  srand(v0);
  result = rand();
  v3 = result;
  for ( i = 0; i <= 15; ++i )
  {
    result = i;
    byte_4010[i] ^= v3;
  }
  return result;
}

sub_1392 함수도 마찬가지로 main 함수 전에 수행되는 함수로 rand 함수 반환 값과 'byte_4010'을 XOR 연산하고 있다. 결과적으로, 최종 키는 '(byte_4010) XOR (v3)' 값이 된다.

Solve code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/aes.h>

//원본 키(byte_4010)
unsigned char original_key[16] = {
    0x00, 0x28, 0xC3, 0x91,
    0x34, 0xB0, 0xD3, 0x92,
    0xA7, 0xF4, 0x7C, 0xA8,
    0x52, 0x42, 0xFB, 0xD5
};

//파일의 내용을 읽어서 할당한 버퍼에 저장하고, 해당 버퍼 포인터를 반환
char* read_file(const char *filename) {
    FILE *fp = fopen(filename, "rb");

    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    rewind(fp);

    char *buffer = malloc(fsize + 1);
    if (!buffer) {
        fprintf(stderr, "Error: memory allocation failed\n");
        fclose(fp);
        exit(EXIT_FAILURE);
    }

    fread(buffer, 1, fsize, fp);
    buffer[fsize] = '\0';

    fclose(fp);
    return buffer;
}

int main(void) {
    // 암호문 파일을 읽어오기
    char* cipher_text = read_file("out_flag.txt");

    //0x00 ~ 0xFF까지 모든 후보를 원본 키의 각 바이트와 XOR해서 최종 키(candidate_key)를 생성
    for (int candidate = 0; candidate < 256; candidate++) {
        unsigned char candidate_key[16];
        for (int i = 0; i < 16; i++) {
            candidate_key[i] = original_key[i] ^ (unsigned char)candidate;
        }

        // AES 복호화를 위한 키 설정 (AES-128)
        AES_KEY aes_key;
        if (AES_set_decrypt_key(candidate_key, 128, &aes_key) != 0) {
            fprintf(stderr, "Error: setting decryption key with candidate 0x%02x\n", candidate);
            continue;
        }

        // CBC 모드의 초기화 벡터(IV)는 모두 0으로 설정
        unsigned char iv[16] = {0};

        // 복호화 결과를 담을 버퍼
        unsigned char plaintext[256];
        AES_cbc_encrypt((unsigned char*)cipher_text, plaintext, 256, &aes_key, iv, AES_DECRYPT);

        // 복호화된 평문이 'DH{'로 시작하면 플래그라고 판단
        if (plaintext[0] == 'D' && plaintext[1] == 'H' && plaintext[2] == '{') {
            printf("Found flag with candidate 0x%02x: %s\n", candidate, plaintext);
            break;
        }
    }

    free(cipher_text);
    return 0;
}
profile
보안 공부 로그

0개의 댓글