导语:我最近开始使用IDAPython,并惊讶于它对于自动化简单的逆向工程任务是多么的有用。我将使用这个链接中的Gootkit示例来进行分析:https://malshare.com/sample.php?action=detail&hash=ef4cf20e80a95791d76b3df8d9096f60。
我最近开始使用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 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不等。
这困扰了我一段时间,但最终,我注意到代码将始终执行解密例程。因为密钥的长度短于加密字符串的长度,所以解密算法需要通过将加密字符串的当前字节的模取为密钥长度,来计算要使用密钥的哪个字节。这由以下程序集指令表示:
因此,我们只需要读取移动到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中运行脚本…
啊!有用!我们看到恶意软件隐藏了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/