导语:我最近开始使用IDAPython,并惊讶于它对于自动化简单的逆向工程任务是多么的有用。我将使用这个链接中的Gootkit示例来进行分析:https://malshare.com/sample.php?action=detail&hash=ef4cf20e80a95791d76b3df8d9096f60。

我最近开始使用IDAPython,并惊讶于它对于自动化简单的逆向工程任务是多么的有用。我将使用这个链接中的Gootkit示例来进行分析:https://malshare.com/sample.php?action=detail&hash=ef4cf20e80a95791d76b3df8d9096f60。

在分析Gootkit示例时,我遇到了许多代码片段,如下所示:

strack_strs.png

这是一种常见的恶意技术,在恶意软件中,重要的字符串(如WindowsAPI调用的名称和硬编码值)常常被加密,以便在静态分析期间尽可能少地留下线索。我们可以在0x0041A94B看到恶意软件正在将十六进制值加载到我标记为ENCR_str_1的局部变量中,一旦加载完所有十六进制值,恶意软件就会使用字符串的长度和局部变量(Buf 1)的地址调用subb_402150,然后subb_402150将使用Heapalloc分配一个缓冲区。

解密例程可以在下面找到:

xor_decryption.png

这是一个简单的XOR解密循环,我们可以看到在这个块的末尾,ECX中的XOR‘d字节通过调用load_xor_char_to_buf函数被存储到在上一步中分配的buf 1中。解密例程中使用的密钥实际上是加密字符串的最后5个字节。

这种类似的模式已经在整个恶意软件中被识别出来,所以我决定编写一个简单的IDAPython脚本来自动解密所有字符串。请注意,该脚本可以在IDA中静态运行,无需使用调试器。

def findStackStrings(func_addr):
  func = get_func(func_addr)
  start = func.startEA
  end = func.endEA
  instr = start
  stack_str = {} #{ address: #instr }
  
  str_addr = instr
  seen = 0
  #find the stack strings
  while instr <= end:
    op1 = print_operand(instr, 0)
    if print_insn_mnem(instr) == "mov" and getRegs(op1) == "ebp":
      if seen == 0:
        str_addr = instr
      seen += 1
    else:
      if seen > 7: #the end of stack_str
        stack_str[str_addr] = (seen, instr)
      seen = 0
  
    instr = next_head(instr, end)
  
  #look ahead for the length of the key 
  #stack strings always followed by a cmp with the counter, and a jmp to the negative branch (xor loop)
  #look ahead for the instruction mov  esi, key_length
  for addr, (length, str_end) in stack_str.iteritems():
    instr =  str_end #start at end of stack_str
    encr_str = []
    key = []
    instr = addr
    key_length = 0
    while instr <= end:
      if print_insn_mnem(instr) == "mov" and print_operand(instr, 0) == "esi":
        key_length = int(print_operand(instr, 1))
        print "Start addr @ 0x%x, key_length: %d, 0x%x" % (addr, key_length, instr)
        break
      instr = next_head(instr, end)
    
    #iterate the stack address and get the immediates
    #we need to look ahead for the 
    instr = addr
    for i in range(length):
      if i >= length - key_length:
        key.append(print_operand(instr, 1).strip('h'))
      else:
        encr_str.append(print_operand(instr, 1).strip('h'))
      instr = next_head(instr, end)  
    
    decoded = xor([int(x,16) for x in encr_str], 
      [int(x,16) for x in key])
      
    #write comment at the start address
    print decoded
MakeComm(addr, decoded)

首先,我在IDA中查找函数,并获得它的开始和结束地址。

func = get_func(func_addr)
start = func.startEA
end = func.endEA
instr = start

使用开始和结束地址,我将遍历函数中的每一条指令,以标识堆栈字符串的开头。我用来识别堆栈字符串的启发式是一个7(任意设置)的连续指令,它将mov执行到ebp相对地址中。

while instr <= end:
  op1 = print_operand(instr, 0)
  if print_insn_mnem(instr) == "mov" and getRegs(op1) == "ebp":
    if seen == 0:
      str_addr = instr
    seen += 1
  else:
    if seen > 7: #the end of stack_str
      stack_str[str_addr] = (seen, instr)
    seen = 0
  instr = next_head(instr, end)

一旦堆栈字符串被标识出来,我就将长度和结束地址存储到由其起始地址索引的字典中。

stack_str[str_addr] = (seen, instr)

在标识了所有堆栈字符串之后,我将遍历stack_str以确定解密过程中使用的密钥。最初,我假设一个键作为所有堆栈字符串的最后5个字节,但事实证明,所使用的键的长度从5到6不等。

这困扰了我一段时间,但最终,我注意到代码将始终执行解密例程。因为密钥的长度短于加密字符串的长度,所以解密算法需要通过将加密字符串的当前字节的模取为密钥长度,来计算要使用密钥的哪个字节。这由以下程序集指令表示:

modulo.png

因此,我们只需要读取移动到ESI中的值。与直接搜索ESI中的mov不同,我首先寻找“cdq”助记符,因为它是一条罕见的指令,除了解密例程之外,很可能不适用于其他任何地方。

while instr <= end:
  if print_insn_mnem(instr) == "cdq":
    instr = next_head(instr, end)
    if print_insn_mnem(instr) == "mov" and print_operand(instr, 0) == "esi":
      key_length = int(print_operand(instr, 1))
      print "Start addr @ 0x%x, key_length: %d, 0x%x" % (addr, key_length, instr)
      break
  instr = next_head(instr, end)

一旦确定了密钥长度,现在剩下的就是编写一个XOR解密函数,并将堆栈字符串和密钥提供给它。

def xor(encr_str, key):
  ret = ""
  for i in range(len(encr_str)):
    ret += chr(encr_str[i] ^ key[i % len(key)])
  return ret

最后,我们将解密后的字符串作为注释写入加密字符串的起始地址旁边。

instr = addr
for i in range(length):
  if i >= length - key_length:
    key.append(print_operand(instr, 1).strip('h'))
  else:
    encr_str.append(print_operand(instr, 1).strip('h'))
  instr = next_head(instr, end)  
decoded = xor([int(x,16) for x in encr_str], 
  [int(x,16) for x in key])
  
#write comment at the start address
print decoded
MakeComm(addr, decoded)

在IDA中运行脚本…

decrypted.png

啊!有用!我们看到恶意软件隐藏了ntdll.dll,可能是后来将其作为参数传递给LoabLibrary调用。

指向脚本的链接:https://gist.github.com/JohnPeng47/3fc58c8a8c9060eafc2cf17fd55bbc59。

注意:脚本实际上将无法识别二进制文件中的所有字符串。我认为这是由于某些字符串使用Unicode编码,而我假设是ASCII,但还没有证实这一点。

更新:回去再看了一遍我的脚本,想知道为什么它不能解码所有的字符串。事实证明,我最初的怀疑是正确的,某些字符串是用16位Unicode编码的。为了处理Unicode的情况,我对脚本做了一点修改。

decoded = xor([int(x,16) for x in encr_str], 
      [int(x,16) for x in key])
      
    #super hacky way of handling strings
    #we are assuming that the malware author is most likely using the ascii subset of unicode
    #which uses the first 8 bits of a unicode char
    #so essentially the 16 bit unicode strings comes out as a bunch of 8 bit ascii chars separated by 0's 
    decoded = "".join([chr(x) for x in decoded if x != 0])
    print decoded
    MakeComm(addr, decoded)
    
def xor(encr_str, key):
  ret = []
  for i in range(len(encr_str)):
    xor_chr = encr_str[i] ^ key[i % len(key)]
    ret.append(xor_chr)
  return ret

我意识到大多数Unicode字符串只是简单地使用ASCII子集,因此,对于16位Unicode字符串,只使用前8位,其余8位保留为零。由于ASCII和Unicode都对前128个字符使用相同的编码值,我们可以将这些字符视为由零分隔的ASCII。运行新更新的脚本,我们可以看到它成功地解密了Unicode字符串。

widestring.png

http://johnpeng47.com/2018/08/14/decrypting-strings-in-the-gootkit-with-idapython/

源链接

Hacking more

...