导语:即使没有事先分析shellcode,我们也可以得到一些有用的变量,或者至少看起来它是一串可疑代码,需要进一步分析。

介绍

字符串/模式匹配算法是当前最流行和最简单的检测shellcode方法。原理很简单:所有代码都有其独特的特征,可以根据这种特征去在内存中验证。即使没有事先分析shellcode,我们也可以得到一些有用的变量,或者至少看起来它是一串可疑代码,需要进一步分析。

我提到的有用变量是指API字符串以及hash值,它们已经在检测恶意代码历史上使用了20多年。如果想要绕过这种检测方法,编写shelcode过程中我们要考虑使用像C或C++这样的HLL语言,进行非常规的编程,保证shellcode中的内容是可以变换的,不固定的。

这一过程中不止可执行代码可以变换,任何内容都可以变换:代码,数据,常量,字符串等等。今天我要展示的是使用名为Maru的hash函数生成可排列的API散列。Maru使用分组密码生成字符串的hash值,当然你大可不必使用分组密码做此事,但是这样可以防止使用SMT进行hash碰撞攻击。

想要获取更多有关信息,你可以阅读:使用SAT和SMT消除简单的散列算法

签名检测

在MS-DOS病毒在30年前刚出现时,基于签名的检测是很容易的。为了隐藏恶意代码,攻击者使用八比特的变量对代码进行异或运算,举个例子,在1991年出现的Skism Rythem Stack Virus-808病毒,使用了感染时间时的毫秒数字进行异或运算,所以对于100个版本来说,每一个都是不同的。

这是该代码的入口,以及其解密过程:

jmp    virus_start

encrypt_val db 00h

virus_start:

    call   encrypt         ; encrypt/decrypt file
    jmp    virus           ; go to start of code

encrypt:

    push   cx
    mov    bx, offset virus_code ; start encryption at data

xor_loop:
    mov    ch, [bx]        ; read current byte
    xor    ch, encrypt_val ; get encryption key
    mov    [bx], ch        ; switch bytes
    inc    bx              ; move bx up a byte
    cmp    bx,  offset virus_code + virus_size
    ; are we done with the encryption

    jle    xor_loop        ; no? keep going
    pop    cx
    ret

执行完这一串代码之后,病毒会使用当前时间值的毫秒数对'encrypt_val'进行更新:

;
    mov    ah, 2ch         ; get time
    int    21h

    mov    encrypt_val, dl ; save m_seconds to encrypt val so
                           ; theres 100 mutations possible

攻击者为做到隐蔽自己做了一些努力,但是解密器是可以被察觉到的,并且病毒很容易被杀掉。此外,杀毒软件可以模拟解密程序,通过验证签名,在病毒执行前阻止他。

在1997年Win32病毒出现时,已经不存在MS-DOS中断,相反,可以通过动态链接库dll对API函数进行访问,这使得检测恶意软件变得更加容易,当存在大量调用API函数的代码块时,系统就会发出警报。

攻击者最终使用了32位校验码,其中最受欢迎的就是CRC32。不久之后,LSD-PL首先使用了基于ADD-Rotate—Xor方法,在他们的WASM包中,代码如下:

i3: mov   esi,[4*eax+ebx]    ; ptr to symbol name
    add   esi,edx            ; rva2va

    push  ebx      ; hash: h=((h<<5)|(h>>27))+c
    push  eax
    xor   ebx,ebx
i4: xor   eax,eax  ; hash loop
    lodsb
    rol   ebx,5
    add   ebx,eax
    cmp   eax,0
    jnz   i4 
    ror   ebx,5 
    cmp   ebx,ecx  ; hash compare
    pop   eax
    pop   ebx
    je    i5       ; same: symbol found
    inc   eax
    jmp   i3       ; different: go to the next symbol

大部分合法代码是不需要调用api的,也不会手动导入地址表或者导出地址表的。如果碰到这种行为代码,那么通常它会对你的计算机造成危害。

API散列是很有趣的,它不会被字符串签名验证给杀掉,所以如果杀毒软件只是对是否包含调用API的代码进行检测,那么这种检测方法是无效的。尽管现在很多攻击软件(BeEF,meterpreter,Veil)都会使用无熵或者随机化的散列算法。但是他们是不会被改变的,所以检测起来还是很容易的。

当然,这些工具都提供了像RC4、AES这样的加密算法,不过这样即使通过了杀毒软件和IDS进行的简单扫描,但是通过仿真器去解密主要代码,观察它的行为,对它防御是完全没有问题的。

字符串问题

在理想情况下,攻击者希望在编译过程中对所有代码进行混淆或者加密,并且在解密过程中不会使用冗余的代码和工具。

constexpr是使用c++代码的理想选择,不过对于C来说,则没有这样的功能,除非编译器在编译过程中对所有字符串进行修改和加密。

一些汇编程序比如MASM,NASM是支持基于宏的编译,但是工作过程中是基于很复杂的函数的(并不是你不能)。Z0MBiE已经在2002年在”Meta病毒中的编码“以及”在HLL中解决字符串问题“中对这一问题进行了讨论,但是如果进一步查看这些解决方法,这里只是使用很少的熵值而已。

使用熵对API字符串进行加密的目的很显然是增加检测有效载荷的难度。不过这不是一个长久之计,迟早会被发现,但是这种方法肯定比现有的没有熵的hash算法要好很多。

散列函数构造

我们可以从分组密码中构造新的hash算法,于是对于提供的不同key值,就会生成不同的API散列。但是,很少的分组密码是适用于shellcode生成的。

这里有三个可以使用的分组密码机制(其实有很多的, 只是目前发现了三个)。

1.Davies–Meyer

dm_simple1.png

将每个块中的消息(mi)作为块密文的key,然后将先前的hash值(Hi-1)作为要被加密的明文。然后将输出的密文和之前的hash值(Hi-1)进行异或,生成下一个hash值(Hi)。在加密的第一轮中,之前并没有hash值产生,会使用我们预先设定的一个常量(H0)进行加密操作。

2.Matyas–Meyer–Oseas

mmo_simple.png

将每块中的消息(mi)作为要被加密的明文。输出的密文同样和相同的明文块进行异或,产生下一组hash值(Hi)。前一组hash值会作为这一组加密的密钥。在第一轮中,并没有之前产生的hash值,所以使用之前设定的初始化变量H0.

3. Miyaguchi–Preneel

mp_simple.png

每一块消息都会作为要加密的明文,输出之后的密文和同样的明文进行异或运算,然后再和前一组产生的hash进行异或运算,产生这一组hash值(Hi)。前一组产生的hash值作为了加密的密钥。

在第一轮中,并没有之前产生的hash值,所以使用之前设定的初始化变量H0。这些是我认为使用的3种简单设计。对于Maru哈希,我使用Davies-Meyer与一些非标准填充。

选择分组密码

我们知道在加密过程中需要一些静态值,与加密常数相比,有什么更好的静态值吗?

你可以对这些静态值进行改变,不过可能会影响加密函数的安全性,所以最好使用一个不包含任何静态变量的密码。在查看可能使用的分组密码时,我选择的前三个是基于ADD-Rotate-Xor(ARX)设计实现的。

1. Speck

由NSA设计,于2013年进行发布(https://eprint.iacr.org/2013/404)。对于不同的架构,产生不同的密钥以及分组大小。成为大多数架构可用的最小的基于软件的块密码。

32位机器的参数为64位的分组大小以及128位的密钥长度。64位模式下的参数为128位的分组大小,以及256为的密钥长度。这两种加密代码长度分别为64字节以及86字节。

如下是x86代码:

disasm.png

2. Chaskey-LTS

在2015年发布(https://eprint.iacr.org/2015/1182),基于Even-Mansour加密方式,使用SipHash中的置换函数进行产生。参数为128位的密钥,和128位的分组大小。在x86机器上实现大小为90字节:

disasm1.png

3.XTEA

拓展微加密算法。一种Feistel密码,在1997年发现微加密算法弱点之后发表(http://www.cix.co.uk/~klockstone/xtea.pdf).参数为128比特的密钥,分组大小为64比特。他有一个已知的常量,0x9E3779B9,这使他容易被基于签名检测出来,大小约80个字节:

disasm2.png

Speck是轻量级应用程序更好的设计,不能通过签名轻松识别。 (至少不是从任何常数)所以我选择它生成hash。

Maru Hash

那么在关于需要消除shellcode中常量的讨论之后,我在这里介绍hash函数的一部分。在转换为整数之前,64位常数是137素数的乘法逆元。它是加密过程中的初始化参数H0。

进一步来说:

1. 0.00729927007299270072992700729927是137的乘法逆元。
2. 舍弃小数部分中的前两个0,将剩下的转换为64位整数。
3. 结果为:0x654C37754C5E9939

H是通过用Maru常数和64位密钥进行异或初始化的。然后将API字符串作为密钥重复使用Speck进行加密,直到达到终止符为止。

下方代码是C原型:

uint64_t maru(const void *str, const void *key);

str参数指向我们的API字符串,长度不应超过128字节(即使函数接受任意长度的字符串)。key参数应该指向我们唯一的64位值,这应该是理想的随机选择。

uint64_t maru(const void *str, const void *key) 
{
    uint8_t *s=(uint8_t*)str;
    w64_t   *k=(w64_t*)key;
    w64_t    h;
    w128_t   m;
    uint32_t idx, end=0, i, t, k0, k1, k2, k3, x0, x1;

    // set the initial hash which is 64-bit key xor 1/137
    h.q = k->q ^ MARU_INIT_H; 

    // now update the hash using string as key
    for (idx=0; !end; ) {
      // if bytes remaining
      if (*s != 0) {
        // add byte to buffer
        m.b[idx++] = *s++;
      } else {
        // this is last block
        // pad with end bits
        while (idx < MARU_BLK_LEN) {
          m.b[idx] = (uint8_t)idx;
          idx++;
        }
        end++;
      } 
      // if m filled  
      if (idx == MARU_BLK_LEN) {
        // copy 128-bit key to local registers
        k0 = m.w[0]; k1 = m.w[1];
        k2 = m.w[2]; k3 = m.w[3];

        // copy H to local space
        x0 = h.w[0]; x1 = h.w[1];

        for (i=0; i<27; i++)
        {
          // encrypt block
          x0 = (ROTR32(x0, 8) + x1) ^ k0;
          x1 =  ROTL32(x1, 3) ^ x0;

          // create next subkey
          k1 = (ROTR32(k1, 8) + k0) ^ i;
          k0 =  ROTL32(k0, 3) ^ k1;

          XCHG(k3, k2);
          XCHG(k3, k1);    
        }
        // update previous hash
        h.w[0] ^= x0; h.w[1] ^= x1;
        // reset index
        idx = 0;
      }
    }
    // return 64-bit hash
    return h.q;
}

hash举例

以下是对API以及潜在密钥的测试案例,他们不必是字符串。

tests.png

从示例散列可以看出,一旦密钥被更改,生成的API哈希是完全不同的。通过选择不同的密钥,这使得我们的哈希值也会不同。

总结

设计自己的hash函数或者加密算法通常会很好的绕过杀毒软件。但是像Maru这样的hash算法并不适用于对防碰撞要求很高的场合。Maru hash真的是一个关于如何对shellcode中的字符串进行随机变换,进而绕过杀毒软件的字符串检测的方法。

源链接

Hacking more

...