ptrace 함수는 'process trace'의 약자로, 리눅스와 같은 유닉스 계열 시스템에서 디버깅이나 프로세스를 제어를 위해 사용되는 시스템 콜이다. 한 프로세스가 다른 프로세스를 감시하거나 제어할 수 있게 해주고, 이를 통해, 자식 프로세스의 메모리나 레지스터를 읽거나 쓸 수 있고, 시그널을 가로채거나 강제로 보낼 수 있으며, 자식 프로세스의 시스템 호출을 추적할 수도 있다.
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ptrace 함수의 원형은 위와 같다. ptrace 함수는 리눅스 시스템 콜이기 때문에 리턴값은 long형이고, 사용하려면 'sys/ptrace.h' 헤더 파일을 포함해야 한다. 함수 원형에 나온 매개 변수를 먼저 알아보자.
가장 중요한 매개 변수로, 어떤 행동을 할 것인지 지정하는 명령 코드이다.
예시
PTRACE_TRACEME : 자식 프로세스가 디버깅 허용을 선언함
PTRACE_ATTACH : 외부 프로세스를 attach해서 디버깅 시작
PTRACE_PEEKDATA : 대상 프로세스 메모리에서 데이터를 읽음
PTRACE_POKEDATA : 대상 프로세스 메모리에 데이터를 씀
PTRACE_CONT 중단된 : 프로세스를 계속 실행시킴
PTRACE_SYSCALL : 다음 시스템 콜 전에 중단되게 함
PTRACE_GETREGS : 대상 프로세스의 레지스터 상태를 읽음
제어하려는 대상 프로세스의 ID이다. 단, PTRACE_TRACEME 요청일 때는 자기 자신을 의미하기 때문에 항상 '0'이 된다.
request에 따라 의미가 달라지는 주소값 또는 위치이다.
예시
PTRACE_PEEKDATA, POKEDATA - 메모리 접근을 원하는 주소
PTRACE_GETREGS, SETREGS - 무시되거나 구조체에 따라 동작
PTRACE_CONT - 다음 중단 시 받을 시그널 번호 (보통 0이다.)
이것도 request에 따라 의미가 달라지는 값이다.
예시
PTRACE_PEEKDATA - 무시됨 (리턴값이 읽은 데이터이다.)
PTRACE_POKEDATA - 사용할 값
PTRACE_GETREGS - 레지스터 값을 저장할 구조체 포인터
PTRACE_SETREGS - 레지스터 값을 담고 있는 구조체 포인터
PTRACE_ATTACH - 무시됨 (NULL로 줘도 된다.)
아래는 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)' 값이 된다.
#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;
}