当前位置:   article > 正文

抖音so反混淆_抖音 jni_onload

抖音 jni_onload

arm架构下ollvm平坦化的反混淆

app样本:douyin9.9.0
so样本:libcms.so
逆向工具:ida,jadx

观察函数控制流程图(CFG)

所要获取的签名算法位于leviathan方法中,该方法是个native方法,在libcms.so中没有静态关联函数,所以我们需要找到其动态注册的位置。
在这里插入图片描述

首先,分析Jni_OnLoad函数,Jni函数的动态注册一般都在这里进行:

在这里插入图片描述

关于ollvm的基本原理,大家可以在 https://github.com/obfuscator-llvm/obfuscator 上进行详细了解,在这里不做过多说明。

可以看到 JNI_OnLoad已经被彻底地混淆,通过流程图结构,我们可以初步推断是ollvm的指令平坦,进一步观察发现大部分流程块都会跳转至0x46FE,从而可以得出0x46FE是主分发器所在块,r0寄存器作为保存索引值的寄存器(在这里我将混淆里用于索引到相关块的常量值称为索引值)。

在这里插入图片描述

去除一般混淆

仔细观察函数内容,发现其中还包含另外一种混淆结构,如下图所示:
在这里插入图片描述
从0x478c的位置PUSH {R0-R3}开始是另一个混淆结构的开始,功能比较简单,分析之后其作用是直接跳转到另一个地址,跳转的位置是到POP {R0-R3}指令的下一个地址,即0x47E4的位置。
去除这中混淆的方式也比较简单,直接把0x478c处修改成到0x47E4的跳转即可,中间的混淆代码可以用nop填充掉,鉴于整个函数有多处这样的混淆,可以写个ida脚本批量处理,通过搜索特征字节码来定位混淆代码的起始与结束位置,例如混淆开始处的opcode为0F B4 78 46 79 46(为了避免错误识别可以多判断几个字节码),结束处的opcode为0F BC。

脚本内容如下:

from idc import *
from idautils import *
from idaapi import *
from keystone import *

ks=Ks(KS_ARCH_ARM,KS_MODE_THUMB)

def ks_disasm(dis_str):
    global ks
    encoding,count=ks.asm(dis_str)
    return encoding

func_start=0x44c8
func_end=0x498c
patch_start = None
patch_end = None
for i in range(func_start, func_end):
    #PUSH{R0-R3} or PUSH{R0-R3,R7}
    #MOV R0,PC
    #MOV R1,PC
    if get_bytes(i,6,0) == b'x0fxb4x78x46x79x46' or get_bytes(i,6,0) == b'x8fxb4x78x46x79x46':
        patch_start = i
    #POP{R0-R3}
    if get_bytes(i,2,0) == b'x0fxbc':
        if patch_start != None:
            patch_end = i + 2
    #POP{R7},POP{R0-R3}
    if get_bytes(i,4,0) == b'x07xbcx88xbc':
        if patch_start != None:
            patch_end = i + 4
    if nop_start != None and nop_end != None:
        for i in range(0, patch_end - patch_start, 2):
            patch_byte(nop_start+i,0x00)
            patch_byte(nop_start+i+1,0xbf)
        dis_str = 'b #{}-{}'.format(patch_end, patch_start)
        jmp_addr.append(patch_start)
        encoding = ks_disasm(dis_str)
        for item in encoding:
            print('{}'.format(hex(item)))
        for j in range(len(encoding)):
            patch_byte(patch_start+j,encoding[j])
        patch_start = None
        patch_end = None
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

寻找相关块

准备工作已完成,正式进入我们的主题,还原ollvm的混淆代码——关于如何还原流程平坦化,有三个问题需要解决:

一、找出流程里所有的相关块
二、找出各个相关块之间执行的先后顺序
三、使用跳转指令将各个相关块连接起来

第一个问题,通过以下规律找到差不多所有的相关块:
1、后继是分发器块的,一般是相关块(注意是一般,因为有的次分发器也会跳转至主分发器
2、相关块的前继块中,有且只有当前相关块一个后继块的,也是相关块

Github上有许多二进制分析框架(angr,barg,miasm等等),可以对函数生成CFG(控制流程图),由于barg和miasm对arm-v7a指令的支持不是很完全,有些代码无法反汇编,最终我选用了angr。(github地址:https://github.com/angr/angr)

这里我参考了github上的一个x86 ollvm反混淆脚本(https://github.com/SnowGirls/deflat):

filename = 'your sopath/libcms.so'
#start of JNI_Onload
start_addr=0x44c8
end_addr=0x498c
project = angr.Project(filename, load_options={'auto_load_libs': False})
cfg = project.analyses.CFGFast(regions=[(startaddr,endaddr)],normalize='True')
#函数为Thumb-2指令,所以寻找functions时的函数地址应该加上1
target_function = cfg.functions.get(start_addr+1)
#将angr的cfg转化为转化为类似ida的cfg
supergraph = am_graph.to_supergraph(target_function.transition_graph)
#手动寻找主分发器,返回块,序言块
main_dispatcher_node=get_node(supergraph,0x46ff)
retn_node=get_node(supergraph,0x4967)
prologue_node=get_node(supergraph,0x44c9)
#手动填入保存索引值的寄存器
regStr='r1'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

project.analyses.CFGFast方法可以快速生成一个二进制代码的控制流程图,但这种流程图和ida的流程有一些不同(比如会将BL函数调用前后的代码分割成两个基本块),而am_graph.to_supergraph方法,则可将其进一步转化为类似ida流程图的结构。

生成cfg后我们通过之前提到的规律来寻找所有的相关块,先找出所有跳转至主分发器的基本块,再筛选其中的相关块。

在这里插入图片描述
还有一些如上图红框所示1其实是一个次分发器,但最后跳转到了主分发器,对于这种情况,我们需要通过匹配特征反汇编指令来过滤掉这些分发器。而红框所示2包含了栈指针操作,但可以发现其并未使栈平衡,观察整个函数也并未发现其他平衡栈的操作,所以可以初步判定此类型的块也可能是混淆块,也应该过滤掉。

在这里我把直接跳转到主分发器的相关块称作一级相关块,对于没有跳转到主分发器而是跳转到另一个相关块的相关块称为次级相关块,则可以通过递归的方块由一级相关快逐层向上寻找,直到找出所有相关块。

由于编译器的优化导致某些相关块在改变了后并未跳转至主分发器,而是跳到了另一个共用代码段。如下图所示,0x14f44处是主分发器,因为编译优化的原因上面三处相关块都会引用同一个代码块,这样在后面的符号执行时,需要将0x14ac8处的相关块看做是上面三个相关块的一部分,符号执行时应该跳过这个共用的相关块。最后在patch指令时,将共用的相关块添加至每个引用它的相关块的末尾,然后再进行跳转。

在这里插入图片描述
寻找所有相关块的代码:

def get_relevant_nodes(supergraph):
    global pre_dispatcher_node, prologue_node, retn_node,special_relevant_nodes,regstr
    relevants = {}

    #寻找那些没有直接跳转到主分发器的相关块
    def find_other_releventnodes(node,isSecondLevel):
        prenodes = list(supergraph.predecessors(node))
        for prenode in prenodes:
            if len(list(supergraph.successors(prenode)))==1:
                relevants[prenode.addr] = prenode
                if isSecondLevel and not(is_has_disasmes_in_node(node, [['mov', regstr]]) or is_has_disasmes_in_node(node, [['ldr', regstr]])):
                    #由于编译器的优化导致某些相关块在改变了索引值后并未跳转至主分发器,而是跳到了另一个共用代码段。
                    special_relevant_nodes[prenode.addr]=node.addr
                find_other_releventnodes(prenode,False)

    nodes = list(supergraph.predecessors(main_dispatcher_node))

    for node in nodes:
        #获取基本块的反汇编代码
        insns = project.factory.block(node.addr).capstone.insns
        if node in relevant_nodes:
            continue
        #过滤跳转到主分发器的子分发器
        elif len(insns)==4 and insns[0].insn.mnemonic.startswith('mov') and 
                insns[1].insn.mnemonic.startswith('mov') and 
                insns[2].insn.mnemonic.startswith('cmp') and 
                is_jmp_code(insns[3].insn.mnemonic):
            continue
        elif len(insns)==1 and is_jmp_code(insns[0].insn.mnemonic):
            continue
        elif len(insns)==2 and insns[0].insn.mnemonic.startswith('cmp') and 
            is_jmp_code(insns[1].insn.mnemonic):
            continue
        elif len(insns)==5 and (is_has_disasmes_in_node(node,[['mov',''],['mov',''],    ['cmp',''],['ldr',regstr]]) or 
            is_has_disasmes_in_node(node,[['mov',''],['mov',''],['cmp',''],                ['mov',regstr]]) )and 
            is_jmp_code(insns[4].insn.mnemonic):
            continue

        #过滤有add sp操作但没有sub sp操作的块
        elif is_has_disasmes_in_node(node,[['add','sp']]) and 
                is_has_disasmes_in_node(node,[['nop','']]):
            continue

        #将认定为相关块的基本块保存
        relevants[node.addr]=node
        #寻找其他没有跳转到主分发器的相关块
        find_other_releventnodes(node,True)
    return relevants

def is_startwith(str1,str2):
    if str2=='':
        return True
    return str1.startswith(str2)

#是否是跳转指令
def is_jmp_code(str):
    if not str.startswith('b'):
        return False
    if str.startswith('bl'):
        if str.startswith('ble') and not str.startswith('bleq'):
            return True
        else: return False
    return True

#是否是函数调用指令
def is_call_code(str):
    if not str.startswith('bl'):
        return False
    if str.startswith('ble') and not str.startswith('bleq'):
        return False
    return True

def is_has_disasmes_in_insns(insns,disinfolist):
    size = len(disinfolist)
    for i in range(len(insns) - (size-1)):
        is_has = True
        for j in range(size):
            insn=insns[i+j].insn
            disinfo=disinfolist[j]
            if not (is_startwith(insn.mnemonic,disinfo[0]) and is_startwith(insn.op_str,disinfo[1])):
                is_has=False
                break
        if is_has: return True
    return False

def is_has_disasmes_in_node(node,disinfolist):
    insns=project.factory.block(node.addr,node.size).capstone.insns
    return is_has_disasmes_in_insns(insns,disinfolist)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

符号执行

找到所有相关块后,下一步要做的就是找出各个相关块的执行先后关系。使用angr的符号执行框架是一个不错的选择,它能够模拟CPU指令执行。当我们对一个相关块进行符号执行时,它能够正确找到下一个相关块。

符号执行先从函数序言处开始,我们将所有符号执行到达的相关块保存至一个队列里,将已经进行过符号执行的相关块从栈中弹出,然后在队列中取出新的相关块进行符号执行;同时我们在符号执行到一个新的相关块时需要保存当前CPU执行环境(内存状态,寄存器状态),以确保下次符号执行新的相关块时,所有的CPU执行环境都正确。

对于有分支结构的相关块,我们采用特征反汇编来识别:
在这里插入图片描述
可以看到,在有分支的相关块中,是通过IT(if then)指令来实现不同条件分支的。IT指令中T的个数代表了分支指令数量(比如 ITTT EQ成立则会执行执行该指令后三条指令,否则会跳过这三条指令),在这里,寄存器R1作为状态值,在相关块中进行更新,然后返回主分发器,通过更新后的R1的值再找到下一个相关块。
为了实现两个分支流程,我们需要自行改变执行流程,而angr的hook功能正好为我们实现了这一点,我们对IT指令的位置进行hook,通过设置跳过的地址长度来实现分支流程。

符号执行代码如下:

    #队列用于保存相关块的符号执行环境
    flow = defaultdict(list)
    queue = queue.Queue()
    addrinqueue=[]
    #从函数开始处符号执行
    queue.put((startaddr+1,None,0))

    while not queue.empty():
        env=queue.get()
        pc=env.addr
        #到达函数返回块或是相关块已经执行过,则跳过该次执行
        if pc in addrinqueue or pc==retaddr:
            continue
        state=env
           block=project.factory.block(pc,relevants[pc].size)
        has_branches=False
        bl_addr=[]
        it_addr=[]
        insns=block.capstone.insns

        for i in range(len(insns)):
            insn=insns[i].insn
            #判断相关块中是否有bl指令,有就将bl指令地址保存,后面符号执行时直接hook跳过
            if insn.mnemonic.startswith('bl'):
                bl_addr.append((insn.address,insn.size))
            if i==len(insns)-1:
                continue
            #判断相关块中是否存在分支结构,有就将IT指令地址保存,符号执行时通过hook到达不同分支
            if insn.mnemonic.startswith('it') and insns[i+1].insn.op_str.startswith(regstr):
                if pc in patch_info:
                    continue
                has_branches = True
                patch_addr_info=[]
                patch_addr_info.append(insn.address)
                j=insn.mnemonic.count('t')
                it_addr.append((insn.address,insns[i+j+1].insn.address-insn.address))
                it_addr.append((insn.address,insns[i+1].insn.address-insn.address))
                movinsns=None
                if insns[-1].insn.mnemonic.startswith('b'):
                    movinsns=insns[i+j+1:-1]
                else:
                    movinsns=insns[i+j+1:]
                movcodes=bytearray()
                #如果IT指令之后有改变ZF状态操作,则将改变ZF状态的功能去除,ZF状态改变会影响分支的执行
                for insnx in movinsns:
                    if insnx.insn.mnemonic.startswith('movs'):
                        encoding=ks_disasm('{} {}'.format('mov',insnx.insn.op_str))
                        movcodes.extend(encoding)
                    else: movcodes.extend(insnx.insn.bytes)
                patch_info[pc]=(patch_addr_info,insn.op_str,movcodes)

        if has_branches:
              #有分支结构,对两个分支都进行符号执行
            symbolic_execution(pc,state, bl_addr, it_addr[0])
            symbolic_execution(pc,state, bl_addr,it_addr[1])
        else:
            symbolic_execution(pc,state,bl_addr)
def symbolic_execution(start_addr, state, bl_addrs=None, branch_addr=None):
    global real_to_real_nodes
    global regs_init_info,queue,flow,addrinqueue
    def handle_bl(state):
        pass

    def handle_branch(state):
        pass

    def init_regs(state,regs_info):
        if len(regs_info)==0:
            return
        for regstr,regvalue in regs_info.items():
            if regstr=='r0': state.regs.r0=claripy.BVV(regvalue,32)
            elif regstr=='r1': state.regs.r1=claripy.BVV(regvalue,32)
            elif regstr=='r2': state.regs.r2=claripy.BVV(regvalue,32)
            elif regstr=='r3': state.regs.r3=claripy.BVV(regvalue,32)
            elif regstr=='r4': state.regs.r4=claripy.BVV(regvalue,32)
            elif regstr=='r5': state.regs.r5=claripy.BVV(regvalue,32)
            elif regstr=='r6': state.regs.r6=claripy.BVV(regvalue,32)
            elif regstr=='r7': state.regs.r7=claripy.BVV(regvalue,32)
            elif regstr=='r8': state.regs.r8=claripy.BVV(regvalue,32)
            elif regstr=='r9': state.regs.r9=claripy.BVV(regvalue,32)
            elif regstr=='r10': state.regs.r10=claripy.BVV(regvalue,32)
            elif regstr=='r11': state.regs.r11=claripy.BVV(regvalue,32)
            elif regstr=='r12': state.regs.r12=claripy.BVV(regvalue,32)
            elif regstr=='sp': state.regs.sp=claripy.BVV(regvalue,32)
            elif regstr=='lr': state.regs.lr=claripy.BVV(regvalue,32)
            elif regstr=='pc': state.regs.pc=claripy.BVV(regvalue,32)

    global project, relevant_block_addrs, modify_value
    if bl_addrs!=None:
        for addr in bl_addrs:
            #hook bl指令 跳过函数调用
            project.hook(addr[0], handle_bl, addr[1])
    if branch_addr!=None:
          #hook it指令 实现不同分支执行
        project.hook(branch_addr[0],handle_branch,branch_addr[1],replace=True)
    if state==None:
        #初始化,用于第一次符号执行时
        state = project.factory.blank_state(addr=start_addr, remove_options={angr.sim_options.LAZY_SOLVES},
                                        add_option={angr.options.SYMBOLIC_WRITE_ADDRESSES})
        #初始化寄存器
        init_regs(state,regs_init_info)
    sm = project.factory.simulation_manager(state)
    loopTime=0
    maxLoopTime=1
    skip_addr=None
    #如果是编译优化导致进行符号执行的相关块改变索引值后未跳转至主分发器,则跳过该相关块跳转到的下一个相关块
    if start_addr in special_relevant_nodes:
        skip_addr=special_relevant_nodes[start_addr]
        maxLoopTime+=1

    sm.step()
    while len(sm.active) > 0:
        for active_state in sm.active:
            #多次循环至主分发器而不经过任何相关块,可认定该路径为死循环
            if active_state.addr==main_dispatcher_node.addr:
                if loopTime<maxLoopTime:
                    loopTime+=1
                else:
                    return None
            if active_state.addr==start_addr:
                return None
            if active_state.addr==retaddr:
                return (active_state.addr, active_state, start_addr)
            #判断是否是相关块
            if active_state.addr in relevant_block_addrs and active_state.addr != skip_addr:
                #如果是相关块,将该块的地址符号执行状态保存放入队列里面
                queue.put((active_state))
                #将符号执行的相关块和其后续相关块对应保存在字典里
                flow[start_addr].append(active_state.addr)
                #保存已经符号执行过的相关块,以免重复执行浪费时间
                addrinqueue.append(start_addr)
        sm.step()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132

其中project.hook用于直接跳过一些指令:

project.hook(self, addr, hook=None, length=0, kwargs=None, replace=False)
addr参数为hook的地址,length参数用于设置跳过的地址长度,hook参数可以设置一个在执行到这条指令时的回调方法。

sm.step会执行到下一个基本块的位置,这时判断如果该块是相关块的话,就停止符号执行,将该基本块的地址和当前的符号执行环境保存至之前所说的符号执行队列里,用于下一次对该块的符号执行。

这样等队列里所有块都符号执行完毕后,我们就理清了相关块之间的关系,下面一步,就是需要通过修改指令来建立相关块之间的跳转。

patch指令建立相关块间的跳转

通过B系列指令来构建跳转,因为大部分相关块最后一条指令都是跳转回主分发器,对于还原混淆来说是无用的,所以我们选择在这里进行patch,将该指令替换成到下一个相关块的指令。如果是有分支结构的相关块,则需要patch两条跳转指令,这时哪里有空间存放另一条跳转指令呢?有两种方案可以解决:

1、相关块里最后用于改变索引值的指令都是无用的,所以我们可以将IT指令及其后面的分支指令去除,再将后面的指令移上去,这样就可以腾出空间放入另一条跳转指令。注意分支跳转指令的条件要和原先IT指令的条件保持一致。

patch前:
在这里插入图片描述
patch后:
在这里插入图片描述
2、如果去除IT相关指令后空间依然不够,第二种方法则是构建一个跳板,先跳转到一块无用代码区域(足够放下我们的跳转指令),再从这块区域跳转到后面的相关块。无用代码区域从哪里找呢?可以从我们之前寻找相关块时过滤掉的代码块中获取,在过滤的时候将这些无用块的地址和大小保存起来,当需要构建跳板时再从中找到符合条件的代码区域。
在这里插入图片描述
patch过程中其他需要注意的地方因为该函数是Thumb-2代码,长度可以为2字节或4字节,如果原本到主分发器的跳转是2字节,而新的跳转范围如果过大则可能是4字节,所以在patch前都要先判断下预留空间是否足够,如果不够的话,再通过上述一二两种方法进行处理。

最终效果查看

我们对JNI_OnLoad函数进行完上述处理,可以看到平坦化结构已经被消除
ida CFG:

在这里插入图片描述

ida f5的效果:
在这里插入图片描述
在这里插入图片描述

算法还原

刚对Jni_Onload的最外层进行了反混淆,f5之后可以看到,主要调用了sub_10710和sub_23B0两个函数,跟进sub_10710,并没有发现对vm的引用,而在sub_23B0中引用了vm,所以先分析sub_23B0。

在这里插入图片描述

跟进后该函里面是一段简单的混淆数,最终其实是跳转到了sub_23C6处的函数。

sub_23C6中存在这上一篇遇到的常规混淆,利用脚本将其清除之后,可以分析出该函数的作用是跳转到调用sub_23B0的第一个参数的地址,同时将javaVM指针的地址作为第一个参数传入到sub_23C6,通过ida动态调试我们得出sub_23B0的第一个参数为0x2520,所以我们继续,跟进到sub_0x2520。
在这里插入图片描述

观察该函数cfg和内部特征,可以得出该函数又是经过ollvm平坦化处理,利用之前的去混淆脚本配置好之后进行处理:

在这里插入图片描述

部分f5之后的c代码:
在这里插入图片描述

并没有直接找到后面引用到javaVM指针的代码,但是上图2处看结构很可能是调用vm->getEnv,为了进一步优化f5代码和变量之间关系,我们需要进一步处理。可以注意到其中一些分支其实是不会执行的,如上图中1处分支的条件,从数学角度分析该条件不会成立,但ida并不能识别出来,所以我们在符号执行的时候需要特殊处理,去掉这些不会执行的相关块。

第二次优化处理后:
在这里插入图片描述

在这里插入图片描述

这次可以看到JavaVM指针被直接引用到,函数执行的流程也基本上一览无遗,该函数里主要对一些全局变量进行解密,解密成用于注册jni方法的字符串或是其他用途的重要数据。

在这里插入图片描述

我们找到env->RegisterNatives处,然后通过参数获取到leviathan方法的对应的native函数地址位于0x576dc处。

0x576dc同样是和之前一样,存在常规跳转混淆和平坦化混淆,反混淆脚本伺候之:
在这里插入图片描述
在这里插入图片描述
最后又是调用了sub_23B0这个函数,在前面已经分析过该函数的作用,可以看出,并没有直接将env等一些jni参数直接传入该函数,而是构建了一个可能是数组的结构,将这些参数加上一定的偏移放入该结构,再将该数组地址作为第二个参数传入。
第一个参数表达式其实是一个定值,其值是sub_551c0的地址,不会受到表达式中变量a4影响(大家有兴趣可以验证下)。

进入到sub_551c0,又是ollvm+跳转混淆,老样子,脚本跑一跑。
f5之后:

在这里插入图片描述

在这里插入图片描述

可以看到在上图处取出了jni方法的参数,紧接着,对java层传入的数组和参数i2进行了一些简单的字节变换操作,将变换后的的数组,数组大小以及i1的值作为一个新的结构传入到sub_215BC。从后面调用的jni函数来看,sub_215BC的返回值极可能是最终加密之后的byte数组,所以我们继续跟进去:

在这里插入图片描述

这里我对参数结构进行了一些转换,以便于更好的理解c代码,可以发现参数结构体作为一个数组的成员继续传入到下一层,继续跟进到sub_229C8:

该函数就是加密算法的核心位置了,但乍一看混淆方式貌似和前面的方式不太一样,该函数是纯ARM指令,也就用不了之前的脚本,而且混淆方式也不太像是ollvm,仔细观察这个函数可以发现里面有大量的switch-case跳转表结构,但ida没有识别出来,我们可以自己设置添加跳转表结构:

在这里插入图片描述

如上图r6是索引值,r1是跳转表基址,r3是跳转表索引到的值,最后跳转的位置即r1+r3。在ida依次选择Edit->Other->Specify switch idiom,为此处配置一个跳转表结构:
在这里插入图片描述

找到所有的跳转表后,便可以f5查看c代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

基本上无法解读,但大体上可以知道其结构,while循环内的switch-case结构,但多数case内又嵌套着另一层switch-case结构,形成多层嵌套,所以代码看起来比较复杂。而最外层switch的case索引,是通过读取从dword_85B60起始的一串数据表,再通过转换运算得到。该数据表内的数据相当于一条命令,不同的命令可以执行不同的case组合,完成不同的功能。

用C代码实现流程

还原该算法难度较大,主要是while内switch case循环次数很多,经验证,总共有一万多次循环,如果对每次循环都进行分析的话,工作量可想而知。所以这里可以另辟蹊径,可以试着新建一个c工程,将f5的伪代码复制出来,同时再构建一个与当前程序执行环境相同的内存环境,直接脱机运行:

    FILE* fp = fopen("libcms-dump", "rb");
    if (!fp)return-1;
    fseek(fp, 0L, SEEK_END);
    int size = ftell(fp);
    //将dump下来的so放入到申请的内存里
    pBuf = new char[size] {0};
    fseek(fp, 0L, SEEK_SET);
    fread(pBuf, size, 1, fp);
    fclose(fp);
    //对so进行数据重定位修复,ori_addr是dump so时,so的加载基址
    relocation(pBuf, ori_addr);
    const char* data="b6a274acedea791afce92a344ccdd80d00000000000000000000000000000000063745505e61c692b79747ec710f8a3100000000000000000000000000000000";
    int ts = 1583457688;
    unsigned char* pBuf3 = (unsigned char*)malloc(64);
    HexStrTobytes((char*)data, pBuf3);
    int swapts = _byteswap_ulong(ts);
    char* pBuf2 = (char*)malloc(20);
    sub_112D8((uint8*)pBuf2, pBuf3, 4);
    sub_112D8((uint8*)pBuf2+4, pBuf3+16, 4);
    sub_112D8((uint8*)pBuf2+8, pBuf3+32, 4);
    sub_112D8((uint8*)pBuf2+12, pBuf3+48, 4);
    sub_112D8((uint8*)pBuf2+16, (unsigned char*)&swapts, 4);
    sub_112D8((uint8*)pBuf2 + 12, (unsigned char*)pBuf+0x8f09c, 4);
    MYINPUT input2 = { pBuf2,20,-1 };
    unsigned int a2[354] = { 0 };
    a2[0] = (unsigned int)&input2;
    a2[2] = (unsigned int)&pBuf[0x21ef5];
    a2[3] = (unsigned int)&a2[350];
    a2[352] = pBuf[0x90690];
    MYINPUT* pInput = &input;
    sub_229C8((unsigned int*)&pBuf[0x85b60], a2, (unsigned int*)&pBuf[0x8eb70], (unsigned int*)&pBuf[0x8eba0], &a2[2]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

因为该代码内用到了很多的全局变量,很多变量都是在app运行后才开始解密,我们也没有对那些代码进行分析,所以需要将app运行到sub_229c8时的整个libcms.so的内存dump出来,可以通过xposed+cydia或是frida hook sub_229c8来实现dump,同时记录下其加载基址,因为android平台pic(位置无关代码)编译的原因,所有全局变量的引用都是通过got(全局偏移表)完成的,加载器会根据加载基址来修正,并向got填入正确的全局变量的地址。当我们自己实现该函数功能,申请一段内存pBuf来存放so数据,把got内全局变量的地址修正到pBuf的位置,如某重定位数据a=S,app运行时的基址是A,pBuf的地址是B,则重定位a的值为S-A+B,这样便相当于从pBuf处加载so。

通过readelf -D 获取数据重定位信息:
在这里插入图片描述
对数据进行重定位:

void relocation(char* bytes,uint32 ori_addr)
{
    uint32 new_addr = (uint32)bytes;
    unsigned int reldyn_start = 0xbac + new_addr;
    size_t reldyn_size = 5496;
    Elf32_Rel* pRel = (Elf32_Rel*)reldyn_start;

    //relocation for .rel.dyn
    for (int i = 0; i < reldyn_size / sizeof(Elf32_Rel); ++i) 
    {
        uint8 relType = (pRel->r_info)&0xff;
        if (relType == R_ARM_RELATIVE || relType == R_ARM_GLOB_DAT)
        {
            *(uint32*)(bytes + pRel->r_offset) = *(uint32*)(bytes + pRel->r_offset) -ori_addr + new_addr;
        }
        pRel += 1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在sub_229c8内有多处函数调用,同样需要把这样函数复制出来实现,需要注意的时这个地方:
在这里插入图片描述

1147行是一个函数调用,v102的值分析后得出是sub_21ef4地址,其功能也很简单:

在这里插入图片描述
考虑到a1可能有多个不同值,所以通过hook sub_21ef4,来获取所有app运行用到的a1的值,之后找到所有a1指向的函数并复制出来实现:

在这里插入图片描述
这样我们直接运行代码:在这里插入图片描述
成功获取返回值,为了验证正确性,将我们程序得到的结果放入到请求参数中,可以正常返回数据!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/133280
推荐阅读
相关标签
  

闽ICP备14008679号