Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班开始招生!
2021-04-15 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:167 收藏

使用unicorn来trace还原ollvm混淆的非标准算法

题目出自高研班3W计划9月的题目还原ollvm混淆的自定义算法,点击文末左下角阅读原文,前往帖子下载附件。
 
ollvm的自定义算法的还原一般就是意味着非常多的分支。我们可以F5打开后,逐步根据参数的传递,逐步的进行追踪,又或者是根据返回值,来逐层向来源追踪。
如果这个算法是一个比较纯粹的算法,那么我们可以用unicorn来模拟执行这个代码段,并且打印所有的汇编指令每一行的变化,最后还原出这个算法。
这个题目是一个比较纯粹的算法,这里我采用了unicorn来进行算法的恢复。
 
做这个题目,我定制了一个专门用来trace指令详细变化的回调,可以直观的看到每行汇编指令当前的寄存器的数值和指令执行后的数值。
 
unicorn相当于是一个cpu的模拟器,可以用来执行so中的代码段,一般不要直接使用apk中的so文件,直接使用是需要修复上下文的,这样会比较复杂。
最好是直接从内存中直接dump一个so出来,然后就可以直接执行so里面的代码段,然后看一个例子ollvm9.apk。
 
最终trace出来的成品demo:
https://github.com/dqzg12300/unicornDemo
 
首先用jadx打开这个apk,看看里面的按钮的功能:
然后看到这里主要用到了UUIDCheckSum这个函数来进行加密。
 
解压这个apk,用ida打开libnative-lib.so找到UUIDCheckSum函数。发现这个函数是ollvm混淆的,然后找入参的使用,发现了一个关键函数。
然后我们想要用unicorn来执行这个函数。首先找到函数的起始和终止位置,这个函数我找到是start:0xfcb4 end:0xff2c。
 
直接使用我们这个ida查看的so文件是不行了。最好是在真机执行时从内存中dump这个so出来。就是拥有完整上下文信息的。这里我使用了大佬的工具来dump:
https://github.com/lasting-yang/frida_dump
 
使用起来也是非常简单。
frida -U com.kanxue.ollvm_ndk_9 -l dump_so.jsdump_so("libnative-lib.so")
 
执行后生成了一个so文件libnative-lib.so_0x7eae047000_0x38000.so。将这个文件拷贝到py项目下。
 
接着梳理一下想要写一个使用unicorn来执行的流程。
1. 创建一个unicorn对象;
2. 使用unicorn创建一块内存,用来存放这个so代码。
3. 使用unicorn创建一块内存,用来存放栈空间。
4. 使用unicorn创建一块内存,用来存放要执行函数的参数。
5. 读取so,将so写入到上面预先创建的内存中。
6. 给参数的那块内存赋值。
7. 给寄存器赋值(x0,x1,sp),X0就是ida函数中看到的第一个参数,X1就是第二个参数。
8. unicorn启动执行指定片段的代码。
9. 读取执行结果。
10. 释放创建的内存。
然后下面看一下实现的代码:
if __name__ == '__main__':    initGlobalData()    #创建uc对象    uc=unicorn.Uc(unicorn.UC_ARCH_ARM64,unicorn.UC_MODE_ARM)    #从内存中dump下来so的基址    code_addr=0x7eae047000    #用来存放so代码的大小,尽量大一点。内存不值钱    code_size=8*0x1000*0x1000    #创建一块内存    uc.mem_map(code_addr,code_size)    #在上面那块内存后面继续划一片内存来当做栈空间    stack_addr=code_addr+code_size    stack_size=0x1000    #栈顶的位置,这里是64位的,所以偏移8个字节    stack_top=stack_addr+stack_size-0x8    #申请一块栈空间    uc.mem_map(stack_addr,stack_size)    #栈空间往后继续划一块空间用来存放参数    args_addr=stack_addr+stack_size    args_size=0x1000    uc.mem_map(args_addr, args_size)    #设置每句汇编执行都会调用hook_code    #uc.hook_add(unicorn.UC_HOOK_CODE,hook_code)    #读取so    with open("./libnative-lib.so_0x7eae047000_0x38000.so","rb") as f:        sodata=f.read()        #给前面创建的空间写入so的数据        uc.mem_write(code_addr,sodata)        #要执行的代码开始位置        start_addr=code_addr+0xFCB4        #要执行的代码结束位置        end_addr=code_addr+0xFF2C        #随机生成一个入参        input_str = ranstr(36)        print("input:%s input_addr:0x%x" % (input_str,args_addr))        input_byte=str.encode(input_str)        #将生成的入参写入前面创建的内存空间        uc.mem_write(args_addr,input_byte)        #ida中看到的函数有参数1、2,然后分别对应X0和X1,写入对应数据,栈寄存器给一个栈顶的地址        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0,args_addr)        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1,len(input_str))        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP,stack_top)        #开始执行代码段        uc.emu_start(start_addr,end_addr)        #ida中看到返回值是直接写在入参中,所以结果我们直接从入参的内存中读取        result=uc.mem_read(args_addr,args_size)        print("result:",result.decode(encoding="utf-8"))    #最后释放创建的内存    uc.mem_unmap(args_addr, args_size)    uc.mem_unmap(stack_addr,stack_size)    uc.mem_unmap(code_addr,code_size)
执行后,得到下面的结果:
input:nxRH3WwuTJgUfqcOS94CM5QEkoPeF0sZ8mGj input_addr:0x7eb6048000result: oySI2Vvt-KfTg-4NR8-BL4Pj-nQdG1r[9laf
执行这个代码段成功后,我们想的是能够将每行执行的汇编代码都打印一下。可以使用hook_add来添加一个指令执行时的回调函数,我们解开上面代码的这句注释。
uc.hook_add(unicorn.UC_HOOK_CODE,hook_code)
然后先设置一个最简单的打印看看。
def hook_code(uc: unicorn.Uc, address, size, user_data):    inst_code=uc.mem_read(address,size)    for inst in cs.disasm(inst_code,size):        print("0x%x:\t%s\t%s" % (address, inst.mnemonic, inst.op_str))
设置之后的打印效果是:
0x7eae056f08:    ldr    x25, [sp, #0x10]0x7eae056f0c:    sub    w9, w10, w90x7eae056f10:    strb    w8, [x0, #0x23]0x7eae056f14:    ldrb    w8, [x11, w9, uxtw]0x7eae056f18:    strb    w8, [x0, #0x22]0x7eae056f1c:    ldp    x20, x19, [sp, #0x40]0x7eae056f20:    ldp    x22, x21, [sp, #0x30]0x7eae056f24:    ldp    x24, x23, [sp, #0x20]0x7eae056f28:    add    sp, sp, #0x50
但是仅仅是这样的效果还是没办法拿来分析算法,必须将每个寄存器的结果在后面打印出来,所以要改造下,下面是我根据自己的需求定制的一个trace打印。
1. 在每行的汇编后面打印该行中所有使用到的寄存器数据
2. 在每行的最后再打印这个寄存器在计算后的结果值
3. 监控指定地址的内存变动。如果数据发生改变。则打印这块内存的数据
下面看具体实现。
 
首先创建一个全局文件globalData.py用来存放全局变量。
#上一次汇编指令global pre_codestr#上一次汇编的第一个寄存器名称global pre_regname#是否有记录上一次的数据global has_pre#监控的地址global watch_addrs
下面贴上完整例子:
import unicornimport randomimport stringimport capstoneimport reimport globalDataimport binascii def ranstr(num):    salt = ''.join(random.sample(string.ascii_letters + string.digits, num))    return salt cs = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)cs.detail = Trueall_regs = Nonereg_names = {    "X0": unicorn.arm64_const.UC_ARM64_REG_X0,    "X1": unicorn.arm64_const.UC_ARM64_REG_X1,    "X2": unicorn.arm64_const.UC_ARM64_REG_X2,    "X3": unicorn.arm64_const.UC_ARM64_REG_X3,    "X4": unicorn.arm64_const.UC_ARM64_REG_X4,    "X5": unicorn.arm64_const.UC_ARM64_REG_X5,    "X6": unicorn.arm64_const.UC_ARM64_REG_X6,    "X7": unicorn.arm64_const.UC_ARM64_REG_X7,    "X8": unicorn.arm64_const.UC_ARM64_REG_X8,    "X9": unicorn.arm64_const.UC_ARM64_REG_X9,    "X10": unicorn.arm64_const.UC_ARM64_REG_X10,    "X11": unicorn.arm64_const.UC_ARM64_REG_X11,    "X12": unicorn.arm64_const.UC_ARM64_REG_X12,    "X13": unicorn.arm64_const.UC_ARM64_REG_X13,    "X14": unicorn.arm64_const.UC_ARM64_REG_X14,    "X15": unicorn.arm64_const.UC_ARM64_REG_X15,    "X16": unicorn.arm64_const.UC_ARM64_REG_X16,    "X17": unicorn.arm64_const.UC_ARM64_REG_X17,    "X18": unicorn.arm64_const.UC_ARM64_REG_X18,    "X19": unicorn.arm64_const.UC_ARM64_REG_X19,    "X20": unicorn.arm64_const.UC_ARM64_REG_X20,    "X21": unicorn.arm64_const.UC_ARM64_REG_X21,    "X22": unicorn.arm64_const.UC_ARM64_REG_X22,    "X23": unicorn.arm64_const.UC_ARM64_REG_X23,    "X24": unicorn.arm64_const.UC_ARM64_REG_X24,    "X25": unicorn.arm64_const.UC_ARM64_REG_X25,    "X26": unicorn.arm64_const.UC_ARM64_REG_X26,    "X27": unicorn.arm64_const.UC_ARM64_REG_X27,    "X28": unicorn.arm64_const.UC_ARM64_REG_X28,    "W0": unicorn.arm64_const.UC_ARM64_REG_W0,    "W1": unicorn.arm64_const.UC_ARM64_REG_W1,    "W2": unicorn.arm64_const.UC_ARM64_REG_W2,    "W3": unicorn.arm64_const.UC_ARM64_REG_W3,    "W4": unicorn.arm64_const.UC_ARM64_REG_W4,    "W5": unicorn.arm64_const.UC_ARM64_REG_W5,    "W6": unicorn.arm64_const.UC_ARM64_REG_W6,    "W7": unicorn.arm64_const.UC_ARM64_REG_W7,    "W8": unicorn.arm64_const.UC_ARM64_REG_W8,    "W9": unicorn.arm64_const.UC_ARM64_REG_W9,    "W10": unicorn.arm64_const.UC_ARM64_REG_W10,    "W11": unicorn.arm64_const.UC_ARM64_REG_W11,    "W12": unicorn.arm64_const.UC_ARM64_REG_W12,    "W13": unicorn.arm64_const.UC_ARM64_REG_W13,    "W14": unicorn.arm64_const.UC_ARM64_REG_W14,    "W15": unicorn.arm64_const.UC_ARM64_REG_W15,    "W16": unicorn.arm64_const.UC_ARM64_REG_W16,    "W17": unicorn.arm64_const.UC_ARM64_REG_W17,    "W18": unicorn.arm64_const.UC_ARM64_REG_W18,    "W19": unicorn.arm64_const.UC_ARM64_REG_W19,    "W20": unicorn.arm64_const.UC_ARM64_REG_W20,    "W21": unicorn.arm64_const.UC_ARM64_REG_W21,    "W22": unicorn.arm64_const.UC_ARM64_REG_W22,    "W23": unicorn.arm64_const.UC_ARM64_REG_W23,    "W24": unicorn.arm64_const.UC_ARM64_REG_W24,    "W25": unicorn.arm64_const.UC_ARM64_REG_W25,    "W26": unicorn.arm64_const.UC_ARM64_REG_W26,    "W27": unicorn.arm64_const.UC_ARM64_REG_W27,    "W28": unicorn.arm64_const.UC_ARM64_REG_W28,    "SP": unicorn.arm64_const.UC_ARM64_REG_SP,} #初始化全局数据def initGlobalData():    globalData.has_pre=False    globalData.pre_codestr=""    globalData.pre_regname=""    #添加监视列表,trace时打印该内存的变动    globalData.watch_addrs= {0x7eae07e060:""}  def hook_code(uc: unicorn.Uc, address, size, user_data):    inst_code=uc.mem_read(address,size)    for inst in cs.disasm(inst_code,size):        #判断是否保存有上次的指令,有的话,则先打印上次的指令,并且查询上次的第一个寄存器的新数值        if globalData.has_pre and globalData.pre_regname:            regindex = reg_names[globalData.pre_regname.upper()]            regvalue = uc.reg_read(regindex)            globalData.pre_codestr+="\t//%s=0x%x" % (globalData.pre_regname,regvalue)            print(globalData.pre_codestr)            globalData.pre_codestr=""            globalData.has_pre=False         #监控我关心的内存空间,如果发生变动会再打印        if len(globalData.watch_addrs)>0:            for i,v in globalData.watch_addrs.items():                idata= uc.mem_read(i,0x10)                buf= binascii.b2a_hex(idata)                hexstr=buf.decode(encoding="utf-8")                if globalData.watch_addrs[i]==hexstr:                    continue                globalData.watch_addrs[i]=hexstr                print("0x%x\t%s" % (i, hexstr))         #拼接当前行的汇编指令        opstr="0x%x:\t%s\t%s" % (address, inst.mnemonic, inst.op_str)        #从当前行指令中匹配出所有的寄存器        res = re.findall(r'[^0]([wx][0-9]+)', " " + inst.op_str, re.I | re.M)        #如果有多个寄存器,取第一个为数值被改变的寄存器        if len(res)>0:            globalData.pre_regname = res[0]        res=list(set(res))        #如果有sp寄存器,则单独插入        if "sp" in inst.op_str:            res.append("sp")        #如果没有寄存器,则不需要记录为上次的,直接打印即可        if len(res)<=0:            has_pre=False            print(opstr)            continue        #记录数据为上次的指令        fenge = "\t\t------"        curreg=""        for regname in res:            regindex=reg_names[regname.upper()]            regvalue=uc.reg_read(regindex)            curreg+="%s=0x%x\t" % (regname,regvalue)        globalData.pre_codestr=opstr +fenge+ curreg        globalData.has_pre=True  # Press the green button in the gutter to run the script.if __name__ == '__main__':    initGlobalData()    #创建uc对象    uc=unicorn.Uc(unicorn.UC_ARCH_ARM64,unicorn.UC_MODE_ARM)    #从内存中dump下来so的基址    code_addr=0x7eae047000    #用来存放so代码的大小,尽量大一点。内存不值钱    code_size=8*0x1000*0x1000    #创建一块内存    uc.mem_map(code_addr,code_size)    #在上面那块内存后面继续划一片内存来当做栈空间    stack_addr=code_addr+code_size    stack_size=0x1000    #栈顶的位置,这里是64位的,所以偏移8个字节    stack_top=stack_addr+stack_size-0x8    #申请一块栈空间    uc.mem_map(stack_addr,stack_size)    #栈空间往后继续划一块空间用来存放参数    args_addr=stack_addr+stack_size    args_size=0x1000    uc.mem_map(args_addr, args_size)    #设置每句汇编执行都会调用hook_code    uc.hook_add(unicorn.UC_HOOK_CODE,hook_code)    #读取so    with open("./libnative-lib.so_0x7eae047000_0x38000.so","rb") as f:        sodata=f.read()        #给前面创建的空间写入so的数据        uc.mem_write(code_addr,sodata)        #要执行的代码开始位置        start_addr=code_addr+0xFCB4        #要执行的代码结束位置        end_addr=code_addr+0xFF2C        #随机生成一个入参        input_str = ranstr(36)        print("input:%s input_addr:0x%x" % (input_str,args_addr))        input_byte=str.encode(input_str)        #将生成的入参写入前面创建的内存空间        uc.mem_write(args_addr,input_byte)        #ida中看到的函数有参数1、2,然后分别对应X0和X1,写入对应数据,栈寄存器给一个栈顶的地址        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0,args_addr)        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1,len(input_str))        uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP,stack_top)        #开始执行代码段        uc.emu_start(start_addr,end_addr)        #ida中看到返回值是直接写在入参中,所以结果我们直接从入参的内存中读取        result=uc.mem_read(args_addr,args_size)        print("result:",result.decode(encoding="utf-8"))    #最后释放创建的内存    uc.mem_unmap(args_addr, args_size)    uc.mem_unmap(stack_addr,stack_size)    uc.mem_unmap(code_addr,code_size)
下面贴个打印的效果片段:
input:L8Cs5ntgNWiPMQRUKYXva0juFw9JqI4ApyZT input_addr:0x7eb60480000x7eae07e060    303132333435363738396162636465660x7eae056cb8:    str    x25, [sp, #0x10]        ------x25=0x0    sp=0x7eb6047fa8        //x25=0x00x7eae056cbc:    stp    x24, x23, [sp, #0x20]        ------x23=0x0    x24=0x0    sp=0x7eb6047fa8        //x24=0x00x7eae056cc0:    stp    x22, x21, [sp, #0x30]        ------x21=0x0    x22=0x0    sp=0x7eb6047fa8        //x22=0x00x7eae056cc4:    stp    x20, x19, [sp, #0x40]        ------x19=0x0    x20=0x0    sp=0x7eb6047fa8        //x20=0x00x7eae056cc8:    ldrb    w22, [x0, #0x18]        ------x0=0x7eb6048000    w22=0x0        //w22=0x460x7eae056ccc:    movz    w8, #0xf6c3        ------w8=0x0        //w8=0xf6c30x7eae056cd0:    movz    w9, #0xa29a        ------w9=0x0        //w9=0xa29a0x7eae056cd4:    movz    w13, #0x4941        ------w13=0x0        //w13=0x49410x7eae056cd8:    movz    w14, #0x7f29        ------w14=0x0        //w14=0x7f290x7eae056cdc:    movz    w15, #0x57d9        ------w15=0x0        //w15=0x57d90x7eae056ce0:    movz    w16, #0xcdcc        ------w16=0x0        //w16=0xcdcc0x7eae056ce4:    movz    w17, #0x425b        ------w17=0x0        //w17=0x425b0x7eae056ce8:    movz    w2, #0x30e6        ------w2=0x0        //w2=0x30e60x7eae056cec:    movz    w3, #0x7f2a        ------w3=0x0        //w3=0x7f2a
有了这份寄存器的变动,然后再根据我们的参数的指针在这份代码中查找。我搜索一下0x7eb6048000这个使用到的地方,然后就可以找到关于input的每个字节是怎么变换的规则了。
这个例子的难度在于最后两个字节的变动。我只说一下最后两个字节的计算是怎么来的。
0x7eae056ee8:    ldrb    w8, [sp, #0xc]        ------w8=0x6cdff6c3    sp=0x7eb6047fa8        //w8=0x900x7eae056eec:    adrp    x11, #0x28000        ------x11=0x9db        //x11=0x7eae07e0000x7eae056ef0:    ldr    w9, [sp, #0x1c]        ------w9=0x6594a29a    sp=0x7eb6047fa8        //w9=0x9db0x7eae056ef4:    add    x11, x11, #0x60        ------x11=0x7eae07e000        //x11=0x7eae07e0600x7eae056ef8:    and    x8, x8, #0xf        ------x8=0x90        //x8=0x00x7eae056efc:    ldr    w10, [sp, #0x1c]        ------w10=0x22    sp=0x7eb6047fa8        //w10=0x9db0x7eae056f00:    ldrb    w8, [x11, x8]        ------w8=0x0    x8=0x0    x11=0x7eae07e060        //w8=0x300x7eae056f04:    and    w9, w9, #0xfffffff0        ------w9=0x9db        //w9=0x9d00x7eae056f08:    ldr    x25, [sp, #0x10]        ------x25=0x0    sp=0x7eb6047fa8        //x25=0x00x7eae056f0c:    sub    w9, w10, w9        ------w9=0x9d0    w10=0x9db        //w9=0xb0x7eae056f10:    strb    w8, [x0, #0x23]        ------w8=0x30    x0=0x7eb6048000        //w8=0x300x7eae056f14:    ldrb    w8, [x11, w9, uxtw]        ------w8=0x30    w9=0xb    x11=0x7eae07e060        //w8=0x620x7eae056f18:    strb    w8, [x0, #0x22]        ------w8=0x62    x0=0x7eb6048000        //w8=0x620x7eae056f1c:    ldp    x20, x19, [sp, #0x40]        ------x20=0x9db    x19=0xa0504942    sp=0x7eb6047fa8        //x20=0x00x7eae056f20:    ldp    x22, x21, [sp, #0x30]        ------x21=0x90    x22=0xa0504942    sp=0x7eb6047fa8        //x22=0x00x7eae056f24:    ldp    x24, x23, [sp, #0x20]        ------x23=0x98c    x24=0x4e    sp=0x7eb6047fa8        //x24=0x0
这是最后的一段代码可以看到w8=0x62写入到了0x22的位置,w8=0x30写入到了0x23的位置。那么这两个数值是什么来历呢?
ldrb    w8, [x11, w9, uxtw]        ------w8=0x30    w9=0xb    x11=0x7eae07e060        //w8=0x62
这里看到是从另外一块内存0x7eae07e060的0xb的位置读取到了0x62。由于我前面已经使用了内存监控。所以这块地址的数据我们可以看到是下面的:
0x7eae07e060    30313233343536373839616263646566
这段字节转成字符串之后的结果就是:
0x7eae07e060    0123456789abcdef
而0xb的位置实际就是62。那么这里的意思就将0xb转成ascii,所以直接找0xb怎么来的就行了。
0x7eae056f0c:    sub    w9, w10, w9        ------w9=0x9d0    w10=0x9db        //w9=0xb
然后继续找0x9d0和0x9db怎么来的。
0x7eae056f04:    and    w9, w9, #0xfffffff0        ------w9=0x9db        //w9=0x9d0
最后只差0x9db是哪里来的,搜索一下0x9db。
0x7eae056e34:    add    w20, w23, w20        ------w20=0x4f    w23=0x98c        //w20=0x9db
然后这里我查过0x4f和0x98c了。然后发现上面有长的相同的流程。所以这种遍历的情况。我们直接搜索前面的地址0x7eae056e34就行了。
"0x7eae056e34:    add    w20, w23, w20        ------w20=0x69    w23=0x0        //w20=0x69""0x7eae056e34:    add    w20, w23, w20        ------w20=0x6b    w23=0x69        //w20=0xd4""0x7eae056e34:    add    w20, w23, w20        ------w20=0x6a    w23=0xd4        //w20=0x13e""0x7eae056e34:    add    w20, w23, w20        ------w20=0x75    w23=0x13e        //w20=0x1b3""0x7eae056e34:    add    w20, w23, w20        ------w20=0x50    w23=0x1b3        //w20=0x203""0x7eae056e34:    add    w20, w23, w20        ------w20=0x73    w23=0x203        //w20=0x276""0x7eae056e34:    add    w20, w23, w20        ------w20=0x37    w23=0x276        //w20=0x2ad""0x7eae056e34:    add    w20, w23, w20        ------w20=0x62    w23=0x2ad        //w20=0x30f""0x7eae056e34:    add    w20, w23, w20        ------w20=0x38    w23=0x30f        //w20=0x347""0x7eae056e34:    add    w20, w23, w20        ------w20=0x46    w23=0x347        //w20=0x38d""0x7eae056e34:    add    w20, w23, w20        ------w20=0x61    w23=0x38d        //w20=0x3ee""0x7eae056e34:    add    w20, w23, w20        ------w20=0x68    w23=0x3ee        //w20=0x456""0x7eae056e34:    add    w20, w23, w20        ------w20=0x4a    w23=0x456        //w20=0x4a0""0x7eae056e34:    add    w20, w23, w20        ------w20=0x70    w23=0x4a0        //w20=0x510""0x7eae056e34:    add    w20, w23, w20        ------w20=0x35    w23=0x510        //w20=0x545""0x7eae056e34:    add    w20, w23, w20        ------w20=0x6e    w23=0x545        //w20=0x5b3""0x7eae056e34:    add    w20, w23, w20        ------w20=0x48    w23=0x5b3        //w20=0x5fb""0x7eae056e34:    add    w20, w23, w20        ------w20=0x72    w23=0x5fb        //w20=0x66d""0x7eae056e34:    add    w20, w23, w20        ------w20=0x67    w23=0x66d        //w20=0x6d4""0x7eae056e34:    add    w20, w23, w20        ------w20=0x52    w23=0x6d4        //w20=0x726""0x7eae056e34:    add    w20, w23, w20        ------w20=0x4e    w23=0x726        //w20=0x774""0x7eae056e34:    add    w20, w23, w20        ------w20=0x4c    w23=0x774        //w20=0x7c0""0x7eae056e34:    add    w20, w23, w20        ------w20=0x30    w23=0x7c0        //w20=0x7f0""0x7eae056e34:    add    w20, w23, w20        ------w20=0x66    w23=0x7f0        //w20=0x856""0x7eae056e34:    add    w20, w23, w20        ------w20=0x57    w23=0x856        //w20=0x8ad""0x7eae056e34:    add    w20, w23, w20        ------w20=0x55    w23=0x8ad        //w20=0x902""0x7eae056e34:    add    w20, w23, w20        ------w20=0x47    w23=0x902        //w20=0x949""0x7eae056e34:    add    w20, w23, w20        ------w20=0x43    w23=0x949        //w20=0x98c""0x7eae056e34:    add    w20, w23, w20        ------w20=0x4f    w23=0x98c        //w20=0x9db"
到这里就知道了。这个0x9db是input的累加。不过并不是完整的累加。所以这里稍微留意下。就能得出结果了。
 
最后0x23位置的字节处理和0x22位置的基本雷同。我就不重复讲了。直接贴上解密后的代码。
#include <iostream># include <stdlib.h>#include <stdio.h> char getAscii(int num){    char tmp[3];    snprintf(tmp,3,"%x",num);    return tmp[0];} int main() {    char input[]="L8Cs5ntgNWiPMQRUKYXva0juFw9JqI4ApyZT";    char output[strlen(input)];    int sum=0;    int eorsum=0xff;    for(int i=0;i<strlen(input);i++){        output[i]=input[i]^0x1;        if(i==0x8||i==0xd||i==0x12||i==0x18){            output[i]=0x2d;            continue;        }else if(i==0xe){            output[i]=0x34;            continue;        }else if(i==0x23){            output[i]=input[0x22];            continue;        }else if(i==0x22){            output[i]=input[0x9];            continue;        }else if(i==0x17){            output[i]=input[0x18]^0x1;        }        if(i<0x22){            int addvalue=input[i];            if(i==0x17){                addvalue=input[0x18];            }            sum+=addvalue;            eorsum^=addvalue;        }    }    int data22=sum-(sum&0xfffffff0);    char tmp22=getAscii(data22);    int data23=eorsum&0xf;    char tmp23=getAscii(data23);    output[0x22]=tmp22;    output[0x23]=tmp23;    output[strlen(input)]=0x0;    printf("input:%s output:%s\n",input,output);    return 0;
最后输出结果:
input:L8Cs5ntgNWiPMQRUKYXva0juFw9JqI4ApyZT output:M9Br4ouf-VhQL-4TJX-w`1kG-v8KpH5@qx7c

优秀学员作品展示

优秀学员作品展示:

# 九月

macOS安装调试llvm入门

fart的理解和分析过程

使用ollvm自定义简单的字符串加密

使用ida trace来还原ollvm混淆的非标准算法

# 八月

ollvm算法还原案例分享

使用Frida打印Java类函数调用关系

# 七月

一个易上手的函数抽取样本还原

一个自定义classloader的函数抽取壳样本

利用Xposed对ollvm后的so中flag爆破

使用Frida分析动态注册jni函数绑定流程

frida跟踪应用中所有运行在解释模式的java函数

# 六月

举杯邀Frida,对影成三题

从三道题目入手入门frida

单纯使用Frida书写类抽取脱壳工具的一些心路历程和实践

某聊天app的音视频通话逆向

# 五月

记一次so文件动态解密

使用Frida简单实现函数粒度脱壳

初试IDA&FRIDA联合调试简单ollvm保护的加密函数源码

ollvm算法还原案例分享

# 四月

java函数转Native化的一些实践

某抽取壳的原理简析

frida辅助脱壳

一款最简单的关于动态注册的APP分析

# 三月

ollvm后的算法还原案例分享

ollvm CrackMe算法分析

ART下Hook系统函数修改内存中指定方法的运行指令逻辑案例分享

某类抽取加固APP的脱壳与修复

《安卓高级研修班(网课)》6月班开始招生!

课程内容
服务对象
一定基础的初、中级安卓逆向研究员,迫切希望提高自身能力、学习能力强,升职加薪意愿强烈、学习意愿强烈
服务内容
(1) 上述列出的两大计划、各八大专题及其包含的二十四个细目
(2) 专属班主任,敦促学习、鼓励士气;良好的抱团学习的氛围;
(3) 可以参加《安卓高级研修班》线下班,鼓励线下交流与大佬谈笑风生
(4) 注意2W班和3W班是完全独立噢,没有交集;
开班时间

开班时间:2021年6月开班

PS:以上为总体服务计划,具体课程时间(段)安排以最终合同约定的课程表为准。

培训价格

就业班注意事项: 

1. 就业班附带包就业服务(须达到合同规定的毕业标准),签合同保证就业及薪资,达不到退全款;
2. 就业班有入学考核,缴费成功后进入考核流程,考核不通过退全款;
3. 考核流程会包括简历筛选、班主任和老师(电话)面试等环节;

强化班注意事项:

1.强化班仅去除包就业服务,并且无入学考核,其余与就业班完全相同;
2.就业班与强化班一起授课,合计35人一个班,教学上不做任何区分。

金融风险注意事项:

1.《安卓高级研修班》全系列无任何金融计划,纯预付;无任何金融套路。
2. 网络课程为虚拟商品,购买之前可以观看下述试看内容,购买成功之后不接受退款。
报名地址
网课月薪三万计划:
https://www.kanxue.com/book-brief-84.htm
扫码立即报名!
网课月薪两万链接:
https://www.kanxue.com/book-brief-83.htm

扫码立即报名!

试看地址

3W:《ida trace分析非标准算法》
https://www.kanxue.com/book-53-900.htm

3W:《Fart&frida》

https://www.kanxue.com/book-53-997.htm

扫码免费试看

2W:《Fart中的脱壳点》
https://www.kanxue.com/book-54-901.htm

2W:《Dalvik下动态注册原理追踪 》

https://www.kanxue.com/book-54-1005.htm

扫码免费试看

联系我们
课程顾问微信:r0ysue(备注“安卓高研网课”)
 
渴望知识和力量的你还在等什么,赶紧报名加入我们吧!

免责条款

以上所有宣传稿件内容均不作为服务承诺,最终以实际签订培训合同为准。

课程大纲与细目会根据教学反馈不断优化、调整与更新,实际授课可能与宣传主题略有不同;

常见Q&A及预习指南

Q:有优惠么?!有优惠么?!有优惠么?!重要的事情说三遍!!

A:没有任何优惠噢。只送开学大礼包,把我们网课中需要准备的设备和环境直接送给大家。

3W班高研网课开学大礼包:

  • 一部pixel手机(sailfish)(安卓8脱壳镜像)

  • 安卓源码开发编译调试环境SSD移动硬盘500G
2W计划的话大礼包中的手机或移动硬盘二选一。

Q2:网课内容与线下班内容一样么?

A:月薪三万计划的内容与线下班的内容是一样的,我们在线下班沉淀大家的切实的需求和疑问,重新编排和制作内容作为网课与大家分享。月薪两万计划的内容由三万计划的讲师全新制作,充分体现工作场景一线的需求,更加贴近实战、实用,有用、好用。

Q3:非常关心ollvm和vmp,可以详细介绍下还原的方法和细节么?

A:目前针对ollvm和vmp,任何所谓的自动化,都是带很多前提和条件限制的;目前最快的还原ollvm或vmp的方法,还是手动分析,一般快则两三日、慢则一两周,基本上可以还原出来。

ollvm或vmp虽然非常复杂,但是并不代表没有取巧和判断的方法;依托于我们丰富的经验,我们会在课上将我们调试和分析的普适方法和一般性及特殊性技巧教给大家,同时带领大家开发属于自己的ollvm和vmp虚拟机,让学员既能够自己给自己的程序加密,又能够分析别人的经过ollvm或vmp保护的算法,这才是我们看雪高级研修班所传达的授人以渔的精神。

Q4:想报名网课需要什么样的基础?像我这样的初学者可以报名么?

A:
(1) 月薪两万计划推荐至少有实际安卓安全岗位工作经验一年以上为宜。初学者可以先看我们安卓版主非虫大佬的《Android软件安全权威指南》等安卓安全书籍进行入门,在看雪论坛看帖发帖提升自身水平,本套课程建议有工作经验的老手前来充电学习。
(2) 月薪三万计划视大家实际需求而定,一般看得懂目录及想要学习的人自己就懂,大家不用盲目跟风。如果看不懂目录及不理解目录的具体含义及意义,建议先从两万计划学起,多积累技术和经验。

Q5:学习三万计划之前,需要先掌握两万计划的基础吗?

A:不需要,互相独立的。月薪两万计划的定位更加偏向工作岗位一线逆向需求,月薪三万计划则更加偏向于高级调试技巧,二者互为补充,相辅相成。有非常多地大佬两个计划一起报名了,我们也会确保直播时间不会冲突。

Q6:想报三万的班,真的很想学高级技巧;但两万的班中也有很多是我想了解和学习的,大佬给些建议呢?

A:其实推荐两个班一起报,有好几位大佬就是两个班全报的。因为首先价格真心不贵,其实我们会将直播的时间错开,方便大家同时进修三万和两万计划,学习自己想要学习的、心仪的知识。

Q7:直播答疑如果错过了,是否会有直播内容的回放?

A:每一场直播都有回放,在看雪课程中可以观看。

Q8:就业班如何报名呢?流程是怎样的呢?

A:就业班是需要考核的。考核流程是先缴费报名,然后开始。会经过简历、(远程)一面和二面。通过之后补差价,不通过退全款。

Q9:我已经报名了,趁开班前还想再预习一下,可否给个预习指南,让我好好利用开班前这段时间再恶补一下。

(1) 在月薪三万计划中,我们学习的主要目标是,掌握调试、分析和还原ollvm、vmp的方法,定制art虚拟机进行自动化脱壳的方法,主要涉及的技术栈是C\C++还原、arm(64),C++开发。
因此首先推荐邓凡平先生的《深入理解Android:Java虚拟机art》,里面的第五章详细讲解了art虚拟机的实现语言C++11,是阅读art源代码必备的知识;其余部分也详细讲解了Class文件、dex文件和ELF文件的格式和内容,以及art虚拟机的编译、runtime、解释执行、内存、线程等art的技术细节;
推荐的第二本书是《C++反汇编与逆向分析技术揭秘》,按照书中的方法自己编写实验代码对C++使用ndk编译后arm汇编进行对照,掌握c++数据类型、控制流、函数和类在编译后arm汇编的表现形式;希望大家预先掌握这些知识,即使现在不开始看,开课后也会要求大家必须掌握。
(2) 在月薪两万计划中,我们更加注重的是实际工作中遇到的各种场景、实际工作能力的提升,及解决实际问题的能力。因此各种逆向环境的搭建、逆向的综合能力和利用代码的编写是最关键的,这里主要涉及的技术栈也是比较杂的:比如网络、Ubuntu/安卓系统知识、应用安全开发、Frida/JS/py等等、Java技巧,比较考验学员的计算机综合技术基础水平。
因此我们从工作实践中的需求出发,推荐大家首先强化安卓Java代码的开发、及各种网络和接口的知识,这两大技能被大量应用到应用安全、漏洞检测、渗透测试、黑灰攻防等方向,我们并不推荐具体的书目,只要涉及Java、安卓和网络的图书,都可以。有句话叫做开发的高度,决定了你逆向的高度,希望大家利用好开班前的时间,强化一下Java和网络开发的能力。
立即报名
网课月薪三万计划:
https://www.kanxue.com/book-brief-84.htm
扫码立即报名!
网课月薪两万链接:
https://www.kanxue.com/book-brief-83.htm

扫码立即报名!


- End -
公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458383765&idx=1&sn=05677124c6ac59df20834be473c96900&chksm=b180c51f86f74c09b5edc82384110b1258add52dfcc736b30e9fdbb5cef085d92f9946af2dbb#rd
如有侵权请联系:admin#unsafe.sh