checksec
└─# checksec cpp_string
[*] '/root/dream/cpp_string/cpp_string'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
문제풀이
//g++ -o cpp_string cpp_string.cpp
#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) {
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);
}
}
}
- "flag" 파일을 읽고
flag[64]
에 저장 →read_flag()
- "test" 파일을 읽고
readbuffer[64]
에 저장 →read_file()
- "test" 파일 쓰기 →
write_file()
readbuffer[64]
에 저장된 데이터 출력 →show_contents()
cpp의 std::ifstream
객체에서 사용하는 read()
에 대한 취약점을 다룬 문제다.
read()
의 경우 기존 C언어의 read()
와 동일한 작업을 수행한다. 하지만 버퍼에 입력을 받을 때 NULL 바이트를 추가하지 않기 때문에 메모리릭이 발생할 수 있다.
예를 들어 readbuffer[64]
와 flag[64]
가 메모리상에서 연결됐다고 가정하자.
std::ifstream
객체의 read()
를 통해 readbuffer[64]
를 "A"로 가득 채우면 어떻게 될까?
"A"*64+"DH{this_is_flag?}"
메모리에서는 위와 같이 저장될 것이다. 그렇다면 readbuffer[]
를 출력하면 어떻게 될까?
NULL 바이트가 추가되지 않았기 때문에, readbuffer[64]
뿐만 아니라 뒤에 연결된 flag
변수의 데이터까지 출력될 것이다.
정리하면
std::ifstream
객체의read()
를 사용하여readbuffer[64]
를 입력한다면, 메모리 릭 발생 가능이 존재readbuffer[64]
와flag[64]
가 메모리에서 연결되어 할당된 상황
위 2가지 조건을 만족한다면 메모리릭 취약점을 이용해서 flag를 읽을 수 있다.
○ read_file()
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);
}
}
read_file()
은 "test" 파일에 값을 readbuffer[64]
에 저장하는 함수다. 코드를 보면 is.read()
로 readbuffer[64]
에 데이터를 저장하는 것을 확인할 수 있다.
- 즉, 위에서 언급한 것처럼 버퍼를 입력 받은 후 널 바이트를 별도로 추가하지 않기 때문에 메모리 릭이 발생할 수 있다.
- 1번 조건 만족!
pwndbg> info var
All defined variables:
Non-debugging symbols:
/* other var */
0x0000000000602380 readbuffer
0x00000000006023c0 flag
0x0000000000602400 writebuffer[abi:cxx11]
0x0000000000602420 std::__ioinit
0x0000000000602428 _end
다음으로 flag[64]
와 readbuffer[64]
의 위치를 확인해보자.
전역변수 주소는 info var
명령어로 구할 수 있다. flag[64]
와 readbuffer[64]
의 주소를 연산하면 64바이트 차이임을 알 수 있다.
- 2번 조건 만족!
그렇다면 공격시나리오는 다음과 같이 작성할 수 있다.
- "test" 파일에
"a"*64
를 저장 read_file()
,read_flag()
실행show_contents()
실행 후,readbuffer[]
와flag[]
확인
공격 시나리오대로 진행해보자.
reference
[dreamhack] Memory Corruption - C++