本文介绍了如何通过IDA Python脚本来实现对栈溢出漏洞的检测,并以ascii_easy一道PWN基础题为例来实战。
原文
IDAPython是一个用于复杂逆向工程任务的强大的自动化工具。尽管有很多文章介绍了用IDAPython来简化基本的逆向任务,但很少有提及使用IDAPython来审计二进制漏洞的方法。
因为这不是一个新的方法(Halvar Flake在2001年做过关于IDA脚本自动分析漏洞的研究),但令人惊讶的是,这个话题没有被更多的说明。这可能是因为在现代操作系统上想要利用漏洞日渐复杂困难。然而,这对于自动化部分的漏洞研究还是有价值的。
在这篇文章中,我们将介绍使用基本的IDAPython来检测程序中出现的能导致栈溢出问题的地方。在这篇文章中,我会用自动化探测方法实战pwnable.kr中的ascii_easy二进制题目。尽管这个二进制文件小到我们可以手动整个去分析它,它仍然是一个很好的学习案例以便我们使用相同的IDAPython技术取分析更大更复杂的二进制文件。
在我们写任何IDAPython脚本前,我们先要决定我们想让我们的脚本做什么。
这里,我们选择了简单漏洞中的一个栈溢出漏洞,其可由strcpy
函数导致将用户可以控制的字符串拷贝到栈缓存区中。既然我们知道了我们要寻找什么,我们可以开始考虑如何取自动化寻找这种漏洞。
我们分为两步:
strcpy
函数)为了寻找任何调用strcpy
的地方,我们需要首先定位strcpy
这个函数本身。
使用IDAPython API很容易做到这一点。使用如下的代码片段来打印二进制文件中所有的函数名:
for functionAddr in Functions(): print(GetFunctionName(functionAddr))
(注:可以在ida底部的python命令控制窗口中输入命令)
我们可以看到,所有的函数名都被输出了。
然后,我们要添加过滤取寻找我们感兴趣的strcpy
函数。简单的字符串比较我们就能达到效果。但因为我们常常需要处理一些函数名相似但仍有区别的情况(例如_strcpy
,这取决于导入函数的命名),我们最好检查子字符串。
在前面的基础上,我们有下面的代码
for functionAddr in Functions(): if “strcpy” in GetFunctionName(functionAddr): print hex(functionAddr)
既然我们获得了我们感兴趣的函数,我们需要获取所有调用它的地方。这需要很多步骤。
首先我们要获取strcpy
交叉引用的地方,然后我们要检查这其中的每个地方是否真正调用了strcpy
函数。总结一下就有下面的代码:
for functionAddr in Functions(): # Check each function to look for strcpy if "strcpy" in GetFunctionName(functionAddr): xrefs = CodeRefsTo(functionAddr, False) # Iterate over each cross-reference for xref in xrefs: # Check to see if this cross-reference is a function call if GetMnem(xref).lower() == "call": print hex(xref)
对ascii_easy二进制文件运行此脚本后我们得到如下结果:
即我们找到了0x451b2c目标地址。
现在,通过上面的代码,我们知道了如何获取所有程序中调用strcpy
的地方。而ascii_easy恰好就只有这一个调用strcpy
的地方(也恰好可利用),很多程序有很多调用strcpy
的地方(很多都不可被利用),因此我们需要一些方法取分析对strcpy
的调用来根据可利用的可能性进行排序。
一个缓存区溢出漏洞的常见特点是它们往往涉及栈上的缓冲区。尽管在堆上或者别的地方的缓存区溢出也是有可能的,栈溢出是一个更简单的利用方式。
这涉及一些对strcpy
函数的目的地参数(destination)的分析,我们知道目的地参数是strcpy
函数的第一个参数,而且我们可以通过浏览函数的反汇编得到这个参数。这个调用strcpy
函数的反汇编如下:
分析上面的代码,有两种可以找到_strcpy
函数的目的地参数。
第一种方法是依赖IDA自动分析后对已知函数的注释。上图中,IDA已经自动检测到了_strcpy
函数的dest
参数并标记。
另一个方法是从函数调用前开始寻找push
指令。每当我们找到一个这样的指令,我们可以自增一个计数器直到我们定位到了参数的索引。这里,既然我们要找的dest
参数是第一个参数,这个方法将会在先前一个push指令停下。
在这些情况中,当我们遍历这些代码时,我们要注意某些可以打破函数执行流的指令。例如ret
或jmp
这样改变执行流的指令会使精确分析参数变得困难。另外,我们需要确保我们不遍历那些当前所处函数之前的地方。现在,我们将只是在搜索参数时识别非顺序执行代码流的地方。如果找到任何非顺序代码流实例,则停止搜索。
我们将使用第二种查找参数的方法(查找被推送到堆栈的参数)。为了帮助我们以这种方式查找参数,我们创建一个helper函数。此函数将从函数调用的地址向向前查找,跟踪push到堆栈的参数并返回我们指定参数对应的操作数。
对于上面的例子,helper函数将返回eax
寄存器的值,因为eax
寄存器保存了strcpy
的目标参数dest。结合一些基本的python与IDAPython API,我们可以构建一个如下函数:
def find_arg(addr, arg_num): # Get the start address of the function that we are in function_head = GetFunctionAttr(addr, idc.FUNCATTR_START) steps = 0 arg_count = 0 # It is unlikely the arguments are 100 instructions away, include this as a safety check while steps < 100: steps = steps + 1 # Get the previous instruction addr = idc.PrevHead(addr) # Get the name of the previous instruction op = GetMnem(addr).lower() # Check to ensure that we haven’t reached anything that breaks sequential code flow if op in ("ret", "retn", "jmp", "b") or addr < function_head: return if op == "push": arg_count = arg_count + 1 if arg_count == arg_num: # Return the operand that was pushed to the stack return GetOpnd(addr, 0)
为了判断eax是否指向在栈中的缓存区buffer,当它被push入栈时,我们应该继续跟踪eax从哪来。因此,我们使用的和之前类似的搜索循环:
# Assume _addr is the address of the call to _strcpy # Assume opnd is “eax” # Find the start address of the function that we are searching in function_head = GetFunctionAttr(_addr, idc.FUNCATTR_START) addr = _addr while True: _addr = idc.PrevHead(_addr) _op = GetMnem(_addr).lower() if _op in ("ret", "retn", "jmp", "b") or _addr < function_head: break elif _op == "lea" and GetOpnd(_addr, 0) == opnd: # We found the destination buffer, check to see if it is in the stack if is_stack_buffer(_addr, 1): print "STACK BUFFER STRCOPY FOUND at 0x%X" % addr break # If we detect that the register that we are trying to locate comes from some other register # then we update our loop to begin looking for the source of the data in that other register elif _op == "mov" and GetOpnd(_addr, 0) == opnd: op_type = GetOpType(_addr, 1) if op_type == o_reg: opnd = GetOpnd(_addr, 1) addr = _addr else: break
上面的代码展示了我们搜索汇编指令以找到保存dest buffer的过程。它也展示了很多检查,例如确保我们没有没有搜索到函数开头之前的地址或任何可能改变执行流的指令。它也尝试追踪其他寄存器。例如,此代码尝试解释下面演示的情况。
... lea ebx [ebp-0x24] ... mov eax, ebx ... push eax ...
另外,在上面的代码中,我们使用了函数is_stack_buffer
,这个函数是这个脚本的最后一部分,有些东西没有在IDA API中定义。这个函数的目的很简单,给定指令的地址和操作数索引,检查这个变量是否是一个栈上的buffer。虽然IDA API没有给我们直接提供这样的函数,但我们可以通过其他方法。通过是同get_stkvar
函数并检查返回值是None还是一个object,我们可以有效检查操作数是否是一个栈上变量。函数实现如下:
def is_stack_buffer(addr, idx): inst = DecodeInstruction(addr) return get_stkvar(inst[idx], inst[idx].addr) != None
注意这个函数和IDA7 API并不兼容。在下篇文章中我们会提到检测栈上buffer的新的方法并保持和最近IDA API的兼容。
我们现在可以把他们组合成一个脚本来寻找strcpy导致的栈溢出漏洞了。通过上述技巧我们可以拓展到不止支持strcpy
,还可以是strcat
,sprintf
等函数(可以参考Microsoft Banned Functions List)
完整的脚本
def is_stack_buffer(addr, idx): inst = DecodeInstruction(addr) return get_stkvar(inst[idx], inst[idx].addr) != None def find_arg(addr, arg_num): # Get the start address of the function that we are in function_head = GetFunctionAttr(addr, idc.FUNCATTR_START) steps = 0 arg_count = 0 # It is unlikely the arguments are 100 instructions away, include this as a safety check while steps < 100: steps = steps + 1 # Get the previous instruction addr = idc.PrevHead(addr) # Get the name of the previous instruction op = GetMnem(addr).lower() # Check to ensure that we havent reached anything that breaks sequential code flow if op in ("ret", "retn", "jmp", "b") or addr < function_head: return if op == "push": arg_count = arg_count + 1 if arg_count == arg_num: #Return the operand that was pushed to the stack return GetOpnd(addr, 0) for functionAddr in Functions(): # Check each function to look for strcpy if "strcpy" in GetFunctionName(functionAddr): xrefs = CodeRefsTo(functionAddr, False) # Iterate over each cross-reference for xref in xrefs: # Check to see if this cross-reference is a function call if GetMnem(xref).lower() == "call": # Since the dest is the first argument of strcpy opnd = find_arg(xref, 1) function_head = GetFunctionAttr(xref, idc.FUNCATTR_START) addr = xref _addr = xref while True: _addr = idc.PrevHead(_addr) _op = GetMnem(_addr).lower() if _op in ("ret", "retn", "jmp", "b") or _addr < function_head: break elif _op == "lea" and GetOpnd(_addr, 0) == opnd: # We found the destination buffer, check to see if it is in the stack if is_stack_buffer(_addr, 1): print "STACK BUFFER STRCOPY FOUND at 0x%X" % addr break # If we detect that the register that we are trying to locate comes from some other register # then we update our loop to begin looking for the source of the data in that other register elif _op == "mov" and GetOpnd(_addr, 0) == opnd: op_type = GetOpType(_addr, 1) if op_type == o_reg: opnd = GetOpnd(_addr, 1) addr = _addr else: break
可以见 https://github.com/Somerset-Recon/blog/blob/master/into_vr_script.py
运行结果
这样,我们就找到了存在问题的函数地址0x8048528