Metasploit Framework载荷生成过程源码分析

本文深入解析了Metasploit中编码载荷的生成过程,包括从汇编代码编译到机器码、生成解码器存根、编码处理、NOP雪橇构造等步骤。重点介绍了Shikata_ga_nai编码器的工作机制,涉及到FPUSaveState结构体、循环解码逻辑和编码算法。此外,还概述了Metasploit框架内的方法调用流程,如EncodedPayload和Encoder模块的相关方法。
摘要由CSDN通过智能技术生成

摘要

  • 在Metasploit中,一段编码载荷的总体生成流程是:
    1. 将不同汇编代码片段组成载荷汇编代码,并编译成机器码。
    2. 生成解码器存根。
    3. 将原始载荷机器码按4字节对齐,并用编码器编码。
    4. 生成nop雪橇。
    5. 最终shellcode:nop雪橇 + 解码器存根(除去最后的0-4个字节, 这些拼到payload中来对齐) + 编码后的payload。
  • 编码器: 用于消除载荷中的坏字节(如’\x00’), 以及为载荷编码。而去除坏字节的方式即是进行编码,而非传统的将push 0替换成xor ecx, ecx和push ecx这样的方法。
  • 如果在栈溢出时是将shellcode放到栈上从栈顶开始往下的位置,则在使用shikata_ga_nai编码器时需要使用nop雪橇,因为该编码器将获取的FpuSaveState结构体数据放到了栈上,且会覆盖栈顶以下多个字节,所以需要通过nop雪橇提供足够的空间,避免覆盖了shellcode。

标记

  • a -> b: 表示方法a调用了方法b
  • a => b: 表示先调用方法a再调用方法b
  • a -> b => c: 表示在方法a的实现中, 依次调用了b和c
  • a => {…}: 花括号中是Ruby语句
  • M1::C1#f1: 表示模块M1下的C1类的实例方法f1
  • M1::C1.f1: 表示模块M1下的C1类的类方法f1

源码分析

  • 在MSF中,当执行run指令开始运行一个exploit模块时lib/msf/core/encoded_payload.rb文件中的Msf::EncodedPayload#generate会被调用,目的是根据用户的配置生成指定的载荷. 该方法的调用流程是: generate_raw => encode => generate_sled => {self.encoded = (self.nop_sled || '') + self.encoded}, 最终载荷的组成是nop雪橇 + 解码器存根(除去最后的0-4个字节, 这些拼到payload中来对齐) + 编码后的payload.
    • Msf::EncodedPayload#generate_raw -> generate_complete -> Msf::Payload::Windows::ReverseTcp.generate -> generate_reverse_tcp: 产生汇编代码, 并通过Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string进行汇编得到字节码.

    • Msf::EncodedPayload#encode: 如果没有指定encoder, 这个方法会依次尝试符合cpu架构和平台架构的各个编码器. 每个编码器都可能对载荷进行反复编码(用户可指定迭代次数). 如果编码成功了(载荷中没有坏字节, 载荷的大小(包含nop滑板)大于要求的最小字节数), 则停止编码.

      • Msf::Encoder#encode -> do_encode -> MetasploitModule#decoder_stub => MetasploitModule#encode_block. 后面这两个方法产生的字符串拼接起来得到编码后的payload. 各个编码器都会覆写这两个方法. 不同编码器会实现一个MetasploitModule类. 下面以x86/shikata_ga_nai编码器为例:
        • decoder_stub: 生成解码器存根. 各个编码器会独立实现该方法. 在该方法的上下文中, state.orig_buf为未编码payload, state.buf最后会保存编码后的payload.
          • generate_shikata_block:
            • 创建了大量Rex::Poly::LogicalBlock实例:
              • 每个这类实例中有@perms列表(实例初始化的时候, 二参及以后的参数形成的列表转为@perms), 列表中的每一项代表一条可选的指令.
              • @perms的元素既可以是代表机器码的字符串, 也可以是Proc实例(它们可通过调用Proc#call返回代表机器码的字符串).
              • 使用Rex::Poly::LogicalBlock#rand_perm方法可随机选@perms中的一条指令.
              • Rex::Poly::LogicalBlock#depends_on使这些实例关联起来. 其中一个实例为loop_inst, 而代码行loop_inst.generate(block_generator_register_blacklist, nil, state.badchars)则是用它生成"多态缓存"(polymorphic buffer), 在generate中该实例以及通过depends_on关联起来的实例都会被用到.
            • Rex::Poly::LogicalRegister实例: 用于代表特定cpu架构下的寄存器编码.
              • 初始化: count_reg = Rex::Poly::LogicalRegister::X86.new('count', 'ecx'), 其中二参'ecx'会传给Rex::Arch::X86.reg_number, 这个方法将'ecx'先转为大写, 然后传给Ruby的原生方法Object#const_get, 这个方法会查询Rex::Arch::X86模块中定义的常量, 最终找到Rex::Arch::X86::ECX常量, 其值即为ecx寄存器编码.
              • 在block实例中调用regnum_of(<Rex::Poly::LogicalRegister实例>), 可得到对应的寄存器编码.
            • loop_inst.generate: 反复执行Rex::Poly::Permutation#do_generate, 直到其返回值buf中没有坏字节.
              • generate_block_list(state, level): 采用递归的方法生成一个state.block_list列表.
                1. 对当前block实例的@depends列表中的每个block调用generate_block_list方法, 把得到的结果附加到state.block_list列表.
                2. [ self, perm ]附加到state.block_list列表. self是本block变量, perm是用rand_perm生成的.
                3. 同1, 不过@depends变为@next_blocks.
              • 迭代上一步得到block_list列表, 把每一项中的perm转成对应的指令机器码, 拼接到state.buffer, 得到解码器存根的机器代码.
          • 解码器存根的后几个字节会被切出, 放到state.buf开头. 其目的是使state.buf以4字节对齐.
        • 解码器存根中有"XORK"的标志, 把它替换成一个真实key, 即一个在encode函数中生成的, 不带坏字节的key. 这个key是在编码中加密用的.
          • 将生成的存根(替换上real_key之后)反汇编(可用Rex::Assembly::Nasm.disassemble方法,并用puts打印), 可看到如下汇编代码:
                mov esi,0xbf9f2758 ; 第二个操作数为real_key
                fcmovu st5 ; 目的是将FPUDataPointer填充到上述结构体. (执行任意fpu指令都可达到此目的)
                fnstenv [esp-0xc] ; 把FpuSaveState结构体保存到栈上的esp-0xC处, 则栈顶会保存FPUDataPointer, 即上面fcmovu指令的地址
                pop ebx ; 
                sub ecx,ecx ; ecx置零, 作为循环计数器
                mov cl,0x4b
            
                ; 偏移0x10, 循环体的开始处
                xor [ebx+0x12],esi ; 0x12即是上面fcmovu指令的地址到这段存根的下一个字节的地址的距离, 所以这条指令即是对编码部分的前4个字节开始解码
                add ebx,byte +0x4
                db 0x03
                ; 这段存根少了loop指令, 会在上面xor后还原出来, 如下:
                ; add esi, [ebx + 0x12] ; 原始数据和第一个key相加, 得到下一个key
                ; loop 0x10 ; 机器码是\xe2\xf5, \xf5应该是表示从loop指令的下一条指令的地址开始减去11, 得到的地址即为循环头部
            
            • 如下为28字节的FPU环境变量结构体(引用自: https://www.boozallen.com/insights/cyber/shellcode/shikata-ga-nai-encoder.html)
                ESP[0]: FPUControlWord;
                ESP[4]: FPUStatusWord;
                ESP[8]: FPUTagWord;
                ESP[0x0c]: FPUDataPointer; // 指向上一条FPU指令
                ESP[0x10]: FPUInstructionPointer;
                ESP[0x14]: FPULastInstructionOpcode;
            
        • 将原始payload按4个字节一个block, 使用encode_block进行编码(不够4字节时在末尾用0填充).
          • encode_block: 使用的是从Msf::Encoder::XorAdditiveFeedback中继承的encode_block编码方法. 算法如下图所示. orig是原始字节(4字节). oblock是输出的编码后的4字节. 将keyorig相加后截取低4个字节, 作为下一轮编码用的key.

            请添加图片描述

    • Msf::EncodedPayload#generate_sled: 生成nop雪橇, 会加在编码后的payload前面.

      • modules/nops/x86/single_byte.rb
        • 从一堆无用指令中取一定数量指令, 比如: nop; xchg eax,edi; cdq; dec ebp; inc edi; aaa; daa; das; cld; std; clc; stc; cmc; cwde; lahf; wait; salc
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值