【IDAPython CTree】反编译代码操作练习

前言

IDA Pro可对二进制的汇编代码进行反编译,得到C伪代码,帮助安全人员进行分析,反编译的结果是一棵抽象语法树(Abstract Syntax Tree, AST)。同时,IDAPython 提供了相应的接口,让我们能够直接通过API,完成对反编译代码的分析操作。

由于网上相关的资料稀少,在观察比较了官方和一些现有博客后,本文主要通过一个案例,两种不同需求的例子来说明对这些接口的使用。

实验前置条件

这里我们采用一个简单的c语言代码[1],源代码如下:

#include<stdio.h>
#include<string.h>

int func3(int n, char *s)
{
  int b;
  if ( strcmp(s, "hello") == 0 )
  {
    b = 100;
  }
  else if ( strcmp(s, "hello1") == 0 )
  {
    b = 200;
  }
  else if ( strcmp(s, "hello2") == 0 )
  {
    b = 4;
  }
  else if ( strcmp(s, "hello3") == 0 )
  {
    b = 300;
  }
  else if ( strcmp("hello4", s) == 0 )
  {
    b = 400;
  }
  else if ( strcmp("hello5", s) == 0 )
  {
    b = 500;
  }
  else
  {
    b = 600;
  }
  return b + n;
}

int main(){
    int res = func3(10, "hello");
    printf("%d", res);
    return 0;
}

功能非常简单,经gcc 反编译得到二进制文件,并将该二进制文件拖入IDA ,结果如下:

至此,实验前置工作完成。

需求一:修改反编译代码中所有赋值语句

根据需求可以简要获得思路:

  1. 遍历表达式,找到所有的赋值语句:idapython 提供了一个 ctree_visitor_t 类,通过继承该类,并修改 visit_expr 可实现遍历所有表达式
  2. 获取赋值语句的变量和立即数
  3. 生成新的表达式替换原有表达式

下面是实现的脚本代码,我在必要的地方添加了注释,帮助理解。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-----------------File Info-----------------------
Name: ctree_test_script.py
Description:
Author: GentleCP
Email: me@gentlecp.com
Create Date: 7/21/2021 
-----------------End-----------------------------
"""
import idaapi
import idc

class MyVisitor(idaapi.ctree_visitor_t):
    def __init__(self, cfunc):
        idaapi.ctree_visitor_t.__init__(self, idaapi.CV_FAST)
        self.cfunc = cfunc

    def is_strcmp_expr(self, cexpr):
        '''
        检验是否为strcmp表达式, 注意此处是先取反,其操作数才是strcmp
        :param cexpr: cexpr_t
        :return:
        '''
        if cexpr.op != idaapi.cot_lnot:
            return False
        strcmp_expr = cexpr.x
        if strcmp_expr.a.size() != 2:
            return False
        if idaapi.get_func_name(strcmp_expr.x.obj_ea) != ".strcmp":
            return False
        return True

    def visit_expr(self, expr) -> "int":
        if expr.op != idaapi.cot_asg:
            # 不是赋值语句跳过
            return 0
        _x = expr.x  # x是第一个操作数
        _y = expr.y  # y是第二个操作数
        print("find expr:{},{}".format(_x.opname, _y.opname))
        # 判断是否满足变量和立即数的条件
        if _x.op == idaapi.cot_var and _y.op == idaapi.cot_num:
            # e.g. v3 = 100,
            num = _y.n.value(_y.type)  # 获取立即数的数值
            # 生成一个新表达式,idapython要想修改表达式,需要新建一个,并调用swap方法
            z = idaapi.cexpr_t()
            z.swap(_y)
            _y.op = idaapi.cot_str  # 修改立即数为string
            _y.string = 'hello world'  # 将所有立即数修改为hello world
        return 0

def main():
    func = idaapi.get_func(idc.here())  # 获取当前位置的函数
    cfunc = idaapi.decompile(func.start_ea)  # 反编译函数
    my_visitor = MyVisitor(cfunc)
    my_visitor.apply_to(cfunc.body, None)

if __name__ == '__main__':
    main()

再看对应的反编译结果,发现成功完成了替换

需求二:找到常数参数为hello的strcmp表达式

该需求主要是练习对语句和表达式结合的搜索方式

整理一下思路:

  1. 找到所有的if 语句:这次我们需要遍历语句,采取修改ctree_visitor_tvisit_insn 方法
  2. 判断其包含的条件表达式是否为 !strcmp() 类型
  3. 判断其参数中是否有 hello 参数,若有,给出提示

下面是实现的脚本代码,当找打我在必要的地方添加了注释,帮助理解:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-----------------File Info-----------------------
Name: ctree_test_script.py
Description:
Author: GentleCP
Email: me@gentlecp.com
Create Date: 7/21/2021 
-----------------End-----------------------------
"""
import idaapi
import idc
import ida_bytes


class MyVisitor(idaapi.ctree_visitor_t):
    def __init__(self, cfunc):
        idaapi.ctree_visitor_t.__init__(self, idaapi.CV_FAST)
        self.cfunc = cfunc

    def is_strcmp_expr(self, cexpr):
        '''
        检验是否为strcmp表达式, 注意此处是先取反,其操作数才是strcmp
        :param cexpr: cexpr_t
        :return:
        '''
        if cexpr.op != idaapi.cot_lnot:
            return False
        strcmp_expr = cexpr.x  #  !strcmp() -> strcmp()
        if strcmp_expr.a.size() != 2:
            return False
        if idaapi.get_func_name(strcmp_expr.x.obj_ea) != ".strcmp":
            return False
        return True

    def visit_insn(self, ins: idaapi.cinsn_t) -> "int":
        # 找到所有if语句,替换其中表达式
        if ins.op == idaapi.cit_if:
            # print("Find if statement at {}".format(ins.ea))
            cif = ins.cif  # cif_t
            # 判断if中表达式是否满足条件
            if self.is_strcmp_expr(cif.expr):
                print("Find !strcmp expr at {}".format(cif.expr.ea))
                strcmp_expr = cif.expr.x  # if -> ! -> strcmp
                for carg in strcmp_expr.a:
                    # 遍历strcmp函数的参数
                    if carg.op == idaapi.cot_obj:
                        # obj 对应string,根据地址获取字符串
                        if ida_bytes.get_strlit_contents(carg.obj_ea, -1, 0).decode == 'hello':
                            print("Found hello at {}".format(carg.obj_ea))

        return 0

def main():
    func = idaapi.get_func(idc.here())  # 获取当前位置的函数
    cfunc = idaapi.decompile(func.start_ea)  # 反编译函数
    my_visitor = MyVisitor(cfunc)
    my_visitor.apply_to(cfunc.body, None)

if __name__ == '__main__':
    main()

执行脚本得到如下结果:

在IDA中检查hello字符串地址,与查找结果相符

总结

本次主要是通过两个小例子熟悉一下idapython 中反编译相关的操作,因为网上这方面的资料过于稀缺,即便有,也都没有解释清楚,要真正理解其用法,还是得自己多实践,多搜索。

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!