IDAPython

idaPython脚本编写实例

1. 背景

在使用IDAdarkside样本进行分析的时候,发现样本是动态加载dll,这使得IDA分析起来极不方便,所以就需要结合idaPython编写一个脚本用来加载dll

2. 入口

IDA中找到加载函数,如下所示:

其中sub_40171C的源代码如下:

转成C代码如下,一段解密,并将内容输出到byte_410ABE

sub_40182A的关键代码如下:

转成C代码如下,可见其功能是调用解密函数解密后,再调用LoadLibraryA加载dll

sub_40182A中存在解码函数sub_4016D5

转成C代码如下,主要是分类输出,主体解密不在此函数中:

sub_4016D5嵌套了一个函数sub_4017AA

转成C代码如下,解密流程函数:

3. 构建

从函数的功能和IDAC代码的功能阅读,加上动态调试,可以构建出如下代码:

import idaapi
import idc
import idautils


# 密钥生成函数
def Resolve_Buffer(arg1, arg2, length):
    global KEY_BUFFER
    v3 = 240
    v4 = arg1[0]
    v5 = arg1[1]
    v6 = arg1[2]
    v7 = arg1[3]

    def _copy_bytes(integer, BUFFER, offset):
        for i in range(0, 4):
            BUFFER[offset + i] = integer & 0xff
            integer >>= 8
        return BUFFER

    while v3 >= 0:
        KEY_BUFFER = _copy_bytes(v4, KEY_BUFFER, v3 + 12)
        KEY_BUFFER = _copy_bytes(v7, KEY_BUFFER, v3 + 8)
        KEY_BUFFER = _copy_bytes(v5, KEY_BUFFER, v3 + 4)
        KEY_BUFFER = _copy_bytes(v6, KEY_BUFFER, v3)
        v4 -= 0x10101010
        v7 -= 0x10101010
        v5 -= 0x10101010
        v6 -= 0x10101010
        v3 -= 0x10

    lo_v8 = 0
    v9 = 0
    v10 = 0

    while True:
        while True:
            lo_result = KEY_BUFFER[v9] & 0xff
            lo_v8 = (lo_result + ((arg2[v10] + lo_v8) & 0xff)) & 0xff
            hi_result = KEY_BUFFER[lo_v8]

            v10 += 1
            KEY_BUFFER[lo_v8] = lo_result
            KEY_BUFFER[v9] = hi_result
            if v10 >= length:
                break
            v9 += 1
            v9 &= 0xff
            if v9 == 0:
                return

        v10 = 0
        v9 += 1
        v9 &= 0xff
        if v9 == 0:
            break

# 密钥加载函数
def Get_KEY_BUFFER():
    global KEY_BUFFER
    # 由RVA计算出函数的实际内存地址
    for s in idautils.Segments():
        if idc.get_segm_name(s) == ".text":
            resolve_func_ea = 0x71C + s

    # 获取函数交叉引用地址,这里只有一处交叉引用,可以不用判断
    for ref in idautils.CodeRefsTo(resolve_func_ea, 1):
        ref_ea = ref

    # 获取三个参数压入函数的参数
    arg_list = [0, 0, 0]
    for i in range(0, 3):
        prev_instruction_ea = idc.prev_head(ref_ea)  # 获取交叉引用处的上一个指令地址
        if idc.print_insn_mnem(prev_instruction_ea) == 'push':  # 获取该处的助记符,判断是否是push
            arg_list[i] = idc.get_operand_value(prev_instruction_ea, 0)  # 获取第一个操作数
            ref_ea = prev_instruction_ea

    # 获取三个参数
    arg1 = []
    arg2 = []
    arg1_ea = arg_list[0]
    arg2_ea = arg_list[1]
    length = arg_list[2]
    while arg1_ea < arg_list[0] + length:
        arg1.append(int.from_bytes(idaapi.get_bytes(arg1_ea, 4), 'little'))  # 以dword形式拷贝,并以小端模式转为int型
        arg1_ea += 4
    while arg2_ea < arg_list[1] + length:
        arg2.append(int.from_bytes(idaapi.get_bytes(arg2_ea, 1), 'little'))  # 以byte形式拷贝,并以小端模式转为int型
        arg2_ea += 1

    Resolve_Buffer(arg1, arg2, length)

# 获取待解密内容
def Get_Encrypted_Lib_Table():
    LoadLibraryA_ea = idc.get_name_ea_simple("LoadLibraryA")
    LoadLibraryA_ref = None
    if LoadLibraryA_ea != idaapi.BADADDR:
        for ref in idautils.CodeRefsTo(LoadLibraryA_ea, 1):
            LoadLibraryA_ref = ref  # 获取LoadLibraryA交叉引用地址
            break

    ENCRYPTED_LIB_TABLE_ea = idc.get_operand_value((LoadLibraryA_ref - 22), 1)
    ENCRYPTED_LIB_TABLE_ea -= 4

    global API_TABLE_start_ea
    global ENCRYPTED_LIB_TABLE
    API_TABLE_start_ea = idc.get_operand_value((LoadLibraryA_ref - 16), 1)
    ENCRYPTED_LIB_TABLE = idaapi.get_bytes(ENCRYPTED_LIB_TABLE_ea, 0xE70) #0xE70是密文的总长度,可通过动态调试得出大小

def Decrypt_String(encrypted_string, length):
    BUFFER1 = []
    for i in KEY_BUFFER:
        BUFFER1.append(i)

    v2 = 0
    v4 = 0
    curr_index = 0
    v6 = 0

    while length != 0:
        v4 = (BUFFER1[1 + v2] + v4) & 0xFF
        v6 = BUFFER1[1 + v2] & 0xFF
        v7 = BUFFER1[v4] & 0xFF
        BUFFER1[v4] = v6
        BUFFER1[1 + v2] = v7
        v6 = (v7 + v6) & 0xFF
        v2 += 1
        encrypted_string[curr_index] ^= BUFFER1[v6]
        curr_index += 1
        length -= 1
    return encrypted_string

def Resolve_String(encrypted_string, length):
    v3 = int(length / 0xFF)
    v4 = length % 0xFF
    if v3 != 0:
        v5 = int(length / 0xFF)

        while v5 != 0:
            encrypted_string = Decrypt_String(encrypted_string, 255)
            v5 -= 1
    if v4 != 0:
        encrypted_string = Decrypt_String(encrypted_string, v4)

    return ''.join([chr(x) for x in encrypted_string])[:-1]

# 密钥存储数组
KEY_BUFFER = []
# 密文存储起点
API_TABLE_start_ea = 0
# 密文存储数据
ENCRYPTED_LIB_TABLE = None
Darkside_DLL_LIST = set(['ntdll', 'kernel32', 'advapi32', 'shell32', 'ole32', 'oleaut32', 'mpr',
						 'iphlpapi', 'shlwapi', 'gdi32', 'user32', 'netapi32', 'wsock32', 'wininet', 'wtsapi32'])

def Main():
    global KEY_BUFFER, API_TABLE_start_ea, ENCRYPTED_LIB_TABLE
    for i in range(0, 256):
        KEY_BUFFER.append(0)

    Get_KEY_BUFFER()
    Get_Encrypted_Lib_Table()

    ENCRYPTED_LIB_TABLE_counter = 0
    length = int.from_bytes(ENCRYPTED_LIB_TABLE[ENCRYPTED_LIB_TABLE_counter:ENCRYPTED_LIB_TABLE_counter+4], 'little')
    ENCRYPTED_LIB_TABLE_counter += 4

    curr_API_ea = API_TABLE_start_ea

    while length!= 0:
        encrypted_string = [x for x in ENCRYPTED_LIB_TABLE[ENCRYPTED_LIB_TABLE_counter:ENCRYPTED_LIB_TABLE_counter+length]]
        resolved_API = Resolve_String(encrypted_string, length)
        print(resolved_API)
        if resolved_API not in Darkside_DLL_LIST:
            idc.set_name(curr_API_ea, 'mw_' + resolved_API)
            curr_API_ea += 4
        ENCRYPTED_LIB_TABLE_counter += length
        length = int.from_bytes(ENCRYPTED_LIB_TABLE[ENCRYPTED_LIB_TABLE_counter:ENCRYPTED_LIB_TABLE_counter + 4], 'little')
        ENCRYPTED_LIB_TABLE_counter += 4

if __name__ == "__main__":
    Main()

4. 对比

核心函数原始代码:

动态加载dll后代码:

5. 总结

这不是一次从01的创造,而是一次从11的练习,非常感谢前辈的无私奉献,附录是我对IDAPython-Book的整理,希望也能对正在学习idaPython的你有所帮助。

6. IDAPython附录

api名称 功能说明
idc.get_screen_ea() 返回当前光标所在处的地址,等效于"here()"
idc.get_inf_attr(INF_MIN_EA) 返回IDB中的最小地址
idc.get_inf_attr(INF_MAX_EA) 返回IDB中的最大地址
idc.get_segm_name(ea) 返回ea地址所在的段名称
idc.generate_disasm_line(ea,0) 返回ea地址处的反汇编代码
idc.next_head(ea) 返回ea地址处下一个汇编指令的地址
idc.prev_head(ea) 返回ea地址处上一个汇编指令的地址
idc.print_insn_mnem(ea) 返回ea地址处的助记符
idc.print_operand(ea,n) 返回ea地址处第n+1个操作数
idc.get_operand_type(ea,n) 返回ea地址处第n+1个操作数类型
idc.get_operand_value(ea,n) 获取ea地址处第n+1个操作数值
idc.op_plain_offset(ea,n,base) 将ea地址出的第n+1个操作数转换为偏移量(base是基地址)
idaapi.BADADDR 返回有效地址
idautils.Segments() 返回每个段地址的起始地址
idc.get_segm_end(ea) 返回ea地址所在段的结束地址
idc.get_next_seg(ea) 返回ea地址所在段的下一个段的起始地址
idc.selector_by_name(str_SectionName) 返回str_SectionName段的序号
idc.get_segm_by_sel(int) 返回int序号段的起始地址
idautils.Functions() 返回已知函数的列表,包含每个函数的起始地址,可使用参数以限定返回范围
idc.get_func_name(ea) 返回ea地址所在函数区域的函数名称
idc.get_next_func(ea) 返回ea地址所在函数的下一个函数的起始地址
idc.get_prev_func(ea) 返回ea地址所在函数的上一个函数的起始地址
idaapi.get_func(ea).start_ea 返回ea地址所在函数的起始地址
idaapi.get_func(ea).end_ea 返回ea地址所在函数的结束地址
idc.get_func_attr(ea,FUNCATTR_START) 返回ea地址所在函数的起始地址
idc.get_func_attr(ea,FUNCATTR_END) 返回ea地址所在函数的结束地址
idc.create_insn(ea) 从ea地址开始尝试将数据转换为代码
idc.add_func(start,end) 从start地址开始识别为函数,直至end地址结束(许多情况下不需要end,ida会自动判断结束位置)
idaapi.get_arg_addrs(ea) 获取被调用函数的参数地址(并不是特别有效,但对api较为准确)
idautils.FuncItems(ea) 返回ea地址所在函数的所有指令地址
idc.next_addr(ea) 返回ea地址的下一个地址
idaapi.decode_insn(insn_t,ea) 解码。insn_t是ida_ua.insn_t()创建的,ea是需要解码的地址
idaapi.FlowChart(idaapi.get_func(ea),idaapi.FC_PREDS) 针对ea地址所在函数生成图,idaapi.FC_PREDS表示计算前驱节点
idc.add_default_til(name) 加载name库(TIL),并返回加载状态,1(成功),0(失败)
idc.import_type(idx,type_name) 导入单个定义,idx表示类型索引,type_name表示具体类型
idc.get_struc_id(string_name) 获取结构体类型的id
idc.op_stroff(ea,n,strid,delta) 将结构名称添加到偏移量
idc.del_struc(sid) 删除id为sid的结构体
idc.add_struc(idx,name,is_union) 构建新的结构体
idc.add_enum(idx,name,flag) idx是新枚举的序号,name是名称,flag是idaapi.hexflag()的标志
idc.get_name_ea_simple(str) 返回api的地址,str为api名称
idautils.CodeRefsTo(ea,flow) 返回ea地址的被何处交叉引用,flow指定否循环正常的代码流程(0-是,1-否)
idautils.CodeRefsFrom(ea,flow) 返回ea地址引用的是何处的代码,flow指定否循环正常的代码流程(0-是,1-否)
idautils.Names() 返回一个迭代器对象,可循环遍历该对象,打印出所有重命名函数和api,格式(ea,str_name)
idc.set_name(ea,name,SN_CHECK) 将ea地址重命名为name
idautils.DataRefsTo(e) 返回ea地址的数据被何处交叉引用
idautils.DataRefsFrom(ea) 返回ea地址引用的是何处的数据
idautils.XrefsTo(ea,flags=0) 返回ea地址的所有被交叉引用的地址
idautils.XrefsFrom(ea,flags=0) 返回ea地址交叉引用的所有地址
idautils.XrefTypeName(typeid) 返回typeid类型的名称,搭配XrefsTo/XrefsFrom使用
ida_search.find_binary(start,end,searchstr,radiux,sflag) 搜索特定字节,start、end指定范围;searchstr指定搜索内容;radiux编写搜索模块时使用;sflag搜索方向或条件
ida_search.find_text(ea,y,x,searchstr,sflag) 搜索特定字节,ea是起始地址,y是搜索行数,x是行中坐标,通常xy为0,剩余参数同上
ida_search.find_code(ea,sflag) 查找下一个标记为代码的地址,ea为当前地址,sflag同上
ida_search.find_data(ea,flag) 查找下一个标记为数据的地址,ea为当前地址,sflag同上
ida_search.find_unknown(ea,flag) 查找下一个标记为未知的地址,ea为当前地址,sflag同上
ida_search.find_defined(ea,flag) 查找下一个标记为代码或数据的地址,ea为当前地址,sflag同上
ida_search.find_imm(ea,sflag,value) 查找value的地址,ea为起始地址,sflag同上
idc.get_full_flags(ea) 返回ea地址的内部标志,用于idc.is*函数集做判断
idc.is_code(f) 如果 IDA 已将地址标记为代码,则返回 True
idc.is_data(f) 如果 IDA 已将地址标记为数据,则返回 True
idc.is_tail(f) 如果 IDA 已将地址标记为尾部,则返回 True
idc.is_unknown(f) 如果 IDA 已将地址标记为未知,则返回 True。当 IDA 尚未识别地址是代码还是数据时使用此类型。
idc.is_head(f) 如果 IDA 已将地址标记为头部,则返回 True
idc.read_selection_start() 返回在ida中选取的代码起始地址
idc.read_selection_end() 返回在ida中选取的代码结束地址的下一个地址
idc.set_cmt(ea,comment,0) 给ea地址添加comment注释,0表示注释不可重复,1表示注释可重复
idc.get_cmt(ea,repeatable) 获取ea地址处的注释,repeatable是bool型,True表示重复,False表示不可重复
idc.set_func_cmt(ea,cmt,repeatable) 给ea地址所在的函数添加cmt注释,repeatable同上
idc.get_func_cmt(ea,repeatable) 获取ea地址所在函数的注释,repeatable同上
ida_bytes.get_flags(ea) 获取ea地址的标志,将用于hasUserName做判断
idc.hasUserName(flags) 根据get_flags获取的标志,判断地址是否被重命名
idc.set_color(ea,what,color) 为ea地址着色,what指定着色范围:指令着色(CIC_ITEM),功能块着色(CIC_FUNC),段着色(CIC_SEGM),color为十六进制颜色代码
idc.get_wide_byte(ea) 获取ea地址处一个字节大小的数据
idc.get_wide_word(ea) 获取ea地址处一个字大小的数据
idc.get_wide_dword(ea) 获取ea地址处一个双字大小的数据
idc.get_qword(ea) 获取ea地址处一个四字大小的数据
idc.get_bytes(ea,size,use_dbg=False) 获取ea地址后size大小的原始数据块,use_dbg可选,在调试内存时需要
idaapi.get_bytes(ea,size) 获取ea地址后size大小的原始数据块
idc.patch_byte(ea,value) 将ea地址处一个字节的值修改为value,大小与函数指定的原始大小一致
idc.patch_word(ea,value) 将ea地址处一个字的值修改为value,大小与函数指定的原始大小一致
idc.patch_dword(ea,value) 将ea地址处一个双字的值修改为value,大小与函数指定的原始大小一致
idc.read_selection_start() 选择突出显示的数据地址的开始
idc.read_selection_end() 选择突出显示的数据地址的结束
ida_kernwin.ask_file(forsave,mask,prompt) 文件导入导出;forsave为0表示导入,1表示导出;mask表示导出类型;prompt是提示内容
idaapi.jumpto(ea) 将反汇编视图重定向到ea地址处
idaapi.auto_wait() 等待ida完成分析后再将控制权返回给脚本
idc.qexit(0) 停止执行脚本,关闭数据库并返还给脚本调用者