Python社区  »  Python

用IDAPython解密Gootkit中的字符串

嘶吼专业版 • 11 月前 • 113 次点击  

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

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

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

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

这是一个简单的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 decodedMakeComm(addr, decoded)

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

func = get_func(func_addr)start = func.startEAend = func.endEAinstr = 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不等。

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

因此,我们只需要读取移动到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 = addrfor 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 addressprint decodedMakeComm(addr, decoded)

在IDA中运行脚本…

啊!有用!我们看到恶意软件隐藏了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字符串。

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


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/DESXoytt7R
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/23410
 
113 次点击  
分享到微博