社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

用IDAPython解密Gootkit中的字符串

嘶吼专业版 • 8 年前 • 999 次点击  

我最近开始使用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