最近打算做安全sdk,由于以前只想着攻没想过防,所以也没积累下什么资料,于是只好从其他app借鉴一下。
在分析某手的时候,其so中大量使用了跳转表,刚开始的时候没在意,以为只是部分函数使用了,因此图省事直接手动修复。
结果越搞越不对劲,发现到处都是这玩意儿,于是准备分析下逻辑,使用脚本一次性全部修复下。
分析
1、每次跳转的起始代码都是一样的(标记为1的块),保存寄存器,获取本次跳转在跳转表的索引,然后跳到跳转表的起始位置。
2、每个函数都有一个跳转表,最开始为一条blx指令(标记为2),均跳转到0x7584(标记为4的块)。
3、每个跳转表的blx指令之后是一个数组(标记为3的块),每个元素为跳转的偏移。
4、所有跳转均在0x7584实现(标记为4的块)。
4.1、因为步骤2使用的blx,所以此时lr寄存器为偏移数组的起始地址(标记为3的块);
4.2、步骤1获取到的跳转表索引,加上步骤4.1获取的跳转表数组起始地址,能够获取到跳转的实际地址;
4.3、恢复寄存器,实现跳转。
脚本
1、查找ldr指令,获取本次跳转在跳转表的索引。
2、获取跳转表的位置。
3、检查跳转表起始内容是否为blx指令,是否跳到0x7584。
4、计算跳转位置并修复。
5、因为修复后可能导致之前被识别为数据的内容转换成指令,所以还需要循环多次修复,直到遍历所有指令后都不需要修复才结束。
import keystone
import idc
arch = keystone.KS_ARCH_ARM
mode = keystone.KS_MODE_THUMB
ks = keystone.Ks(arch, mode)
def is_ldr(ea):
ldr_addr = ea
ldr_flags = idc.get_full_flags(ldr_addr)
if not idc.is_code(ldr_flags):
return False, None
ldr_op = idc.print_insn_mnem(ldr_addr)
if ldr_op != 'LDR':
return False, None
ldr_opt0 = idc.get_operand_type(ldr_addr, 0)
ldr_op0 = idc.print_operand(ldr_addr, 0)
if ldr_opt0 != idc.o_reg or ldr_op0 != 'R0':
return False, None
ldr_opt1 = idc.get_operand_type(ldr_addr, 1)
if ldr_opt1 != idc.o_mem:
return False, None
ldr_opv1 = idc.get_operand_value(ldr_addr, 1)
return True, ldr_opv1
def is_bl(ea):
bl_addr = ea
bl_flags = idc.get_full_flags(bl_addr)
if not idc.is_code(bl_flags):
return False, None
bl_op = idc.print_insn_mnem(bl_addr)
if bl_op != 'BL':
return False, None
bl_opt0 = idc.get_operand_type(bl_addr, 0)
if bl_opt0 != idc.o_near:
return False, None
blx_addr = idc.get_operand_value(bl_addr, 0)
return True, blx_addr
def is_jump_table(ea):
blx_addr = ea
blx_flags = idc.get_full_flags(blx_addr)
if not idc.is_code(blx_flags):
return False, None
blx_op = idc.print_insn_mnem(blx_addr)
if blx_op != 'BLX':
return False, None
blx_opt0 = idc.get_operand_type(blx_addr, 0)
if blx_opt0 != idc.o_near:
return False, None
route_addr = idc.get_operand_value(blx_addr, 0)
return True, route_addr
def check_code_items(ea, t_flag):
addr = ea
flags = idc.get_full_flags(ea)
if not idc.is_head(flags):
idc.del_items(idc.get_item_head(ea))
addr = idc.next_head(ea)
while addr != idc.BADADDR:
flags = idc.get_full_flags(addr)
if not idc.is_code(flags) or idc.get_sreg(addr, 't') != t_flag:
idc.del_items(addr)
addr = idc.next_head(addr)
else:
break
if idc.get_sreg(ea, 't') != t_flag:
idc.split_sreg_range(ea, 't', t_flag)
def patch_jump_addr(ldr_addr, tb_addr, ldr_src_val_addr):
push_addr = idc.prev_head(ldr_addr)
bl_addr = idc.next_head(ldr_addr)
patch_addr = push_addr
pc = patch_addr + 4
if pc & 0b11:
pc &= 0xfffffffc
ldr_pc_offset = ldr_src_val_addr - pc
idc.del_items(push_addr)
idc.del_items(ldr_addr)
idc.del_items(bl_addr)
ldr_src_flags = idc.get_full_flags(ldr_src_val_addr)
if not idc.is_dword(ldr_src_flags):
idc.del_items(ldr_src_val_addr, 0, 4)
idc.create_dword(ldr_src_val_addr)
tb_index = idc.get_wide_dword(ldr_src_val_addr)
index_addr = tb_addr + tb_index * 4
index_flags = idc.get_full_flags(index_addr)
if not idc.is_dword(index_flags):
idc.del_items(index_addr, 0, 4)
idc.create_dword(index_addr)
offset = idc.get_wide_dword(index_addr)
jmp_addr = tb_addr + offset
code = 'ldr pc,[pc,' + hex(ldr_pc_offset) + ']'
encoding, count = ks.asm(code, patch_addr)
if not encoding:
print 'asm error', hex(patch_addr)
return
for item in enumerate(encoding):
idc.patch_byte(patch_addr + item[0], item[1])
idc.patch_dword(ldr_src_val_addr, jmp_addr)
check_code_items(jmp_addr, idc.get_sreg(patch_addr, 't'))
idc.create_insn(patch_addr)
idc.auto_wait()
def func_patch():
time = 0
count = -1
while count != 0:
time += 1
count = 0
print 'time:', time,
idc.auto_wait()
ins_addr = idc.next_head(0)
while ins_addr != idc.BADADDR:
succ, ldr_src_val_addr = is_ldr(ins_addr)
if not succ:
ins_addr = idc.next_head(ins_addr)
continue
ldr_addr = ins_addr
ins_addr = idc.next_head(ldr_addr)
succ, blx_addr = is_bl(ins_addr)
if not succ:
continue
succ, route_addr = is_jump_table(blx_addr)
if not succ or route_addr != 0x7584:
continue
tb_addr = blx_addr + 4
patch_jump_addr(ldr_addr, tb_addr, ldr_src_val_addr)
count += 1
print 'count:', count
print 'start...'
func_patch()
print 'end...'
结果
脚本执行后可以看到,总共遍历了16次。
JNI_OnLoad修复后的内容如下。
未修复处理
因为修复脚本是遍历ida当前分析出的所有指令,所以未被识别成指令的部分不会修复。
如果分析过程中,手动将数据转换成指令后,存在未修复的跳转,只需要再执行一次脚本即可。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2022-2-22 01:02
被卧勒个槽编辑
,原因: 修复脚本bug