pwnable.tw 의 CAOV 문제이다. 처음 풀어보는 c++문제이다.
일단 보호기법은 PIE 빼고 다 걸려있다. 오랜만에 엄청난 삽질을 한 문제다.
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: NO PIE (0x400000)
일단 기본적으로 키(string)과 value(long)을 저장하고 출력하는 프로그램이다.
소스코드를 줘서 소스코드만 본다고 문제가 풀리지 않는다. 왜인지는 모르겠지만, IDA로 까보면 edit 함수에 이상한 녀석이 있다.
unsigned __int64 Edit()
{
__int64 v0; // rax
__int64 v1; // rax
__int64 old[6]; // [rsp+0h] [rbp-80h]
char v4; // [rsp+30h] [rbp-50h]
unsigned __int64 v5; // [rsp+68h] [rbp-18h]
v5 = __readfsqword(0x28u);
default(old);
copy((__int64)&v4, (__int64)old, (const char **)data); //v4 is what???
destroy((__int64)&v4);
sub_401B1A(data);
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "\nYour data info before editing:");
std::ostream::operator<<(v0, (__int64)&std::endl<char,std::char_traits<char>>);
sub_401D70(old);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "\nYour data info after editing:");
std::ostream::operator<<(v1, (__int64)&std::endl<char,std::char_traits<char>>);
sub_401D70((__int64 *)data);
destroy((__int64)old);
return __readfsqword(0x28u) ^ v5;
}
대체 v4라는 녀석이 왜 복사생성자에 들어가있는지도 모르겠지만, 어쨌든 소멸자가 호출될 때 *v4의 값을 컨트롤 가능하면 원하는 곳에서 free가 가능하다. 왜냐면 소멸자(함수)가 아래와 같기 때문이다. delete
도 결국은 free
함수로 귀결된다. (이유는 뒤에 나온다.)
_DWORD *__fastcall destroy(__int64 a1)
{
if ( *(_QWORD *)a1 )
operator delete[](*(void **)a1);
*(_QWORD *)a1 = 0LL;
*(_QWORD *)(a1 + 8) = 0LL;
*(_QWORD *)(a1 + 16) = 0LL;
return init_time((_DWORD *)a1);
}
그리고 이 값을 어떻게 컨트롤 할지는 당연히 이전에 edit_name
을 하는 부분에서 컨트롤이 가능할 것이다. 비슷한 스택프레임에 존재를 하기 때문에 취약점을 생각하면 당연하다.
위 그림들을 보면 알겠지만 edit_name
의 src 배열이 Data.edit
의 v4보다 0x60바이트 만큼 앞서있다. 따라서 우리는 이를 활용해서 fake로 free
를 할 수 있다.
이렇게 보면 왜 뜬금없이 name을 받으라고 했는지도 알 수 있다. fake 청크를 만들라는 것이다.
그냥은 leak할 수 없다. 모든 입력뒤에는 null문자가 들어가기 때문이다. 여기서 너무 뻘짓을 많이 했다. 먼저 name 부분에 0x70사이즈의 fake chunk를 만들어서 free 해준다.
우리는 name의 fd를 변조할 수 있기 때문에 전역 포인터 변수 Data의 값을 덮을 수 있는 곳을 할당받아야 하고, 이 위치는 다음과 같다.
변조 후 heap 상황은 아래 사진과 같게 된다.
우리는 Data 객체의 멤버를 마음대로 할 수 있으니까, key의 위치에 got등을 넣어서 leak을 하면 된다.
(참고로 해당 libc에서 printf
의 오프셋이 0x55800
이라서 leak이 정상적으로 안되니 다른 got를 사용하자)
이후에는 double free를 이용해서 __malloc_hook
에 원가젯을 넣어주면 된다.
왜 이 취약점이 발생했는지 좀 궁금해서 분석을 해보았는데, 결론은 = 연산자 오버로딩 자체에서 일어난 취약점인것 같다. 연산자 오버로딩에서는 이 소스코드처럼 직접 대입을 하는 것이 아니라 임시객체를 반환하는 형식으로 코드를 작성해야한다. 즉 복사생성자가 할일을 연산자 오버로딩에 사용하면 안된다. 그리고 이는 개발자가 어떻게 코드를 작성하는지에 따라 다른데, = 연산자 오버로딩은 별로 좋은 습관은 아닌 것 같다. 편할지는 모르겠지만, 명시적으로 ToString(), ToInt() 같은 메서드를 사용하는게 좋은 습관인 듯 하다.