idaPython脚本编写实例
1. 背景
在使用IDA
对darkside样本进行分析的时候,发现样本是动态加载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. 构建
从函数的功能和IDA
转C
代码的功能阅读,加上动态调试,可以构建出如下代码:
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. 总结
这不是一次从0
到1
的创造,而是一次从1
到1
的练习,非常感谢前辈的无私奉献,附录是我对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) | 停止执行脚本,关闭数据库并返还给脚本调用者 |