美文网首页
Nu1L第一次RE公开赛WP

Nu1L第一次RE公开赛WP

作者: SamiraG | 来源:发表于2020-04-05 14:25 被阅读0次

    因为第二天有xctf所以这道题从晚上九点出来到12点我没有做出来就睡了,看了官方的wp记录一下。

    引用

    Nu1L官方wp
    某大佬的博客

    花指令

    关于花指令
    https://blog.csdn.net/whklhhhh/article/details/88677670
    https://blog.csdn.net/whklhhhh/article/details/88730934
    在没有去除花指令之前,打开ida的关键代码如下所示:

    void sub_123413B0()
    {
      unsigned int v0; // ST40_4
      unsigned int v1; // ST44_4
      int v2; // ST78_4
    
      nullsub_11();
      sub_12341340(0);
      v0 = strlen("npointer{");
      v1 = strlen("}");
      v2 = sub_123416AD(v0 + v1 + 33);
      nullsub_12();
      sub_12341020("Input the correct keys: ");
      nullsub_24();
      sub_12341050("%s", v2, v0 + v1 + 33);
      JUMPOUT((char *)&loc_1234148F + 1);
    }
    

    JUMPOUT后面的代码没有反编译出来,查看汇编代码

    .text:12341487 E8 04 00 00 00                    call    near ptr loc_1234148F+1
    .text:1234148C 77 EB                             ja      short loc_12341479
    .text:1234148E 07                                pop     es
    .text:1234148F
    .text:1234148F                         loc_1234148F:                     ; CODE XREF: sub_123413B0+D7↑p
    .text:1234148F 88 36                             mov     [esi], dh
    .text:12341491 83 04 24 01                       add     [esp], 1
    .text:12341495 C3                                retn
    

    call指令等于这样两条指令,一是把自身所在位置的下一条指令的地址压入堆栈,二是jmp到call的地址处,而ret指令可以理解为jmp到call指令压入堆栈的地址,因此,可以用call指令这样来写花指令:
    1.call一个地址,在call下面随便写一点花指令,但是要注意一点与jmp版花指令不同的,我们要记得自己写的花指令占了多少个字节,比如,占了2字节
    2.在call里面,也就是函数里面,首先pop出压入的地址,然后把这个地址减去花指令占用的字节数,这里是2字节,再重新push进堆栈,然后就ret,这样,call结束以后执行的下一条指令就是我们想要去的位置了,也就是花指令下面的正常的指令了
    对于这样一个花指令,call一个函数内的地址然后再retn返回,IDA会认为被call的地址是一个新的函数,当前函数就被截断了,影响到了IDA的分析。
    图中call指令之后在call函数里将栈顶的值加了1,所以call指令后面的77可以忽略直接到EB 07这条指令,这个是jmp 7的意思,可以直接跳到retn指令后面。
    官方wp中说主程序中穿插的花指令形式如下:

    #define JUNK2(idx) __asm{          \
      __asm  call next1_junk2_##idx          \
      __asm  __emit 0x77          \
      __asm  jmp next_junk2_##idx        \
      __asm  __emit 0x88          \
      __asm  next1_junk2_##idx:            \
      __asm  add dword ptr ss:[esp], 1  \
      __asm  ret              \
      __asm  next_junk2_##idx:              \
    }
    
    
    #define JUNK1(idx) __asm{\
    __asm jmp jlabel##idx \
    __asm __emit 0x88 \
    __asm jlabel_##idx : \
    __asm ret \
    __asm __emit 0xba \
    __asm jlabel##idx : \
    __asm call jlabel_##idx  \
    }
    

    这两个花指令的二进制形式如下:

    '''
    EB0388C3BAE8F9FFFFFF
    E80400000077EB07883683042401C3
    '''
    stripd = ""
    with open('gatesXgame_un.exe', "rb") as f:
        raw = f.read()
        i = 0
        while i < len(raw):
            if raw[i:i+10] == "\xEB\x03\x88\xC3\xBA\xE8\xF9\xFF\xFF\xFF":
                stripd += "\x90"*10
                i = i + 10
            elif raw[i:i+15] == "\xE8\x04\x00\x00\x00\x77\xEB\x07\x88\x36\x83\x04\x24\x01\xC3":
                stripd += "\x90"*15
                i = i + 15
            else:
                stripd += raw[i]
                i += 1
    with open('gate_str.exe', 'wb') as f:
        f.write(stripd)
    

    去除花指令后的代码

    int sub_123413B0()
    {
      unsigned int v0; // ST40_4
      unsigned int v1; // ST44_4
      char *Buf1; // [esp+6Ch] [ebp-Ch]
    
      sub_12341340(0);
      v0 = strlen("npointer{");
      v1 = strlen("}");
      Buf1 = (char *)sub_123416AD(v0 + v1 + 33);
      print("Input the correct keys: ");
      sub_12341050("%s", Buf1, v0 + v1 + 33);
      if ( !memcmp(Buf1, "npointer{", strlen("npointer{"))
        && Buf1[strlen(Buf1) - 1] == asc_12343738[0]
        && (Buf1[strlen(Buf1) - 1] = 0, sub_12341090(sub_12343798, 0x30D3u, (int)&Buf1[strlen("npointer{")])) )
      {
        sub_12341340(1);
        print("Congrats!\n");
      }
      else
      {
        print("Sorry, the gate remains closed.\n");
      }
      return system("pause");
    }
    
    int __cdecl sub_12341090(void *Src, SIZE_T dwSize, int a3)
    {
      HANDLE v4; // eax
      int v5; // ST38_4
      LPVOID Dst; // [esp+1Ch] [ebp-14h]
    
      Dst = VirtualAlloc(0, dwSize, 0x3000u, 0x40u);
      if ( !Dst )
        return 0;
      memcpy(Dst, Src, dwSize);
      v4 = GetCurrentProcess();
      FlushInstructionCache(v4, Dst, dwSize);
      sub_12341240();
      v5 = ((int (__fastcall *)(_DWORD, int, unsigned int))Dst)(0, a3, strlen((const char *)a3));
      VirtualFree(Dst, 0, 0x8000u);
      return v5;
    }
    

    所以主要的验证部分在sub_12343798处,在进入检测代码之前设置了一些参数信息:

    //edx --> char* flag
    //ecx --> int idx
    //edi --> payload_base
    //ebx --> MAX_STEP (len(flag))
    

    天堂之门

    在Windows64操作系统下,所有的32位程序会被装载到WoW64子系统中运行。而某些windows kernel调用,WoW64会将其钩取为64位调用。在这个过程中,运行的程序会从兼容模式暂时地切换成64位模式运行。利用这个特性,我们可以在程序运行过程中主动切换为64位模式来执行64位代码,以达到某种保护程序(如使静态分析失败、动态跟踪混乱)的目的。
    这种保护方法被称为Heaven's Gate,直译就是天堂之门。架构的切换对于运行在 Wow64 环境下的程序是必不可少的,运行在 64 位 Windows 系统下的 32 位程序在进入系统调用前需要完成下述操作:

    32-bit ntdll.dll -> wow64cpu.dll’s Heaven’s Gate -> 64-bit ntdll.dll -> syscall into the kernel
    

    WoW64根据段寄存器cs的值来确定程序的运行模式,如果cs的值为0x33,则当前是64位模式;如果cs的值为0x23,则当前为兼容模式。
    这道题采用了这样的方式来切换运行模式:

    // open_gate_template_x86
    push 0x33 // cs:0x33
    sub esp, 4
    mov dword ptr ss:[esp], grid_dst_ip // next grid
    add qword ptr ss:[esp], edi // payload_base
    inc ecx // step++
    retf
    
    // open_gate_template_x64
    push 0x23 // cs:0x23
    sub rsp, 8
    mov qword ptr ss:[rsp], grid_dst_ip
    add qword ptr ss:[rsp], rdi
    inc rcx // step++
    retfq
    

    因为架构的切换过程加上验证部分中间依旧穿插了部分花指令,x86到x64的切换大致过程如下

    b = random.randint(0, 0x7f)
    a = 0x33 ^ b
    open_gate_template_x86_mutated = f"""
            open_gate_label:
                push {hex(a)}
                xor dword ptr ss:[esp], {hex(b)}
                call next_label // sub esp, 4
                .byte {random.randint(0,255)}
                next_label:
                mov dword ptr ss:[esp], grid_dst_ip
                add dword ptr ss:[esp], edi
                inc ecx
                retf"""
    

    再弄明白这个架构的切换之后,可以看出来验证部分主要是一个迷宫(图搜索),不过需要确定最最开始的两条路径(即最前面两个字母需要时f4)然后后面的路径过程基本一致,可以写出遍历程序求解。
    补一些关于天堂之门的学习链接
    http://rce.co/knockin-on-heavens-gate-dynamic-processor-mode-switching/
    https://medium.com/@fsx30/hooking-heavens-gate-a-wow64-hooking-technique-5235e1aeed73
    https://www.malwaretech.com/2014/02/the-0x33-segment-selector-heavens-gate.html
    这部分代码调试需要使用WinDbg(x64),这个调试器可以自动完成32到64位的切换,我试了一下使用这个调试器,在执行retf后,寄存器从eax变成了rax,在执行retfq后,寄存器从rax变成了eax.

    solve

    膜Thiner

    #!/usr/bin/python
    
    
    """
    Solution Author: Thiner @ NeSE
    
    
    The challenge is solved with some luck.
    """
    
    
    """
    note here
    rbx=total
    rcx=0
    rdx=buffer without npointer{}
    rdi=&code
    init is 32bit
    return value is check result
    
    
    offset  : 0x2198
    virtaddr: 0x12343798
    code len: 0x30d3
    """
    
    
    # gatesXgame_un.exe is the upx unpacked binary
    
    
    import capstone as cs
    with open('gatesXgame_un.exe') as f:
        raw = f.read()
    
    
    
    
    code = raw[0x2198:0x2198+0x30d3]
    
    
    # """
    for i in range(len(code)-5):
        if code[i:i+5] == '\xe8\x01\x00\x00\x00':
            code = code[:i+5]+'\xf1'+code[i+6:]
        if code[i:i+5] == '\xe8\x02\x00\x00\x00':
            code = code[:i+5]+'\xcd\x81'+code[i+7:]
        if code[i:i+5] == '\xe8\x03\x00\x00\x00':
            code = code[:i+5]+'\xcd\x82\xf1'+code[i+8:]
    
    
    # code=code.replace('e803000000e4de78'.replace())
    
    
    md32 = cs.Cs(cs.CS_ARCH_X86, cs.CS_MODE_32)
    md64 = cs.Cs(cs.CS_ARCH_X86, cs.CS_MODE_64)
    
    
    
    
    def dis32(addr):
        print ' code 32 {:#x} '.format(addr).center(80, '-')
        for h, i in enumerate(md32.disasm(code[addr:addr+0x100], addr)):
            print '{:3d}{:>#8x} {:>7s} {}'.format(h, i.address, i.mnemonic, i.op_str)
    
    
    
    
    def dis64(addr):
        print ' code 64 {:#x} '.format(addr).center(80, '-')
        for h, i in enumerate(md64.disasm(code[addr:addr+0x100], addr)):
            print '{:3d}{:>#8x} {:>7s} {}'.format(h, i.address, i.mnemonic, i.op_str)
    
    
    
    
    def ext32(addr):
        #dis32(addr)
        head = list(md32.disasm(code[addr:addr+0x80], addr))
        # 3+2k
        l = []
        for i in range(10):
            if head[3+2*i].mnemonic != 'cmp':
                break
            ch = int(head[3+2*i].op_str.split(',')[-1], 0)
            tar = int(head[4+2*i].op_str, 0)
            inst = list(md32.disasm(code[tar:tar+0x30], tar))[4]
            addr = int(inst.op_str.split(',')[-1], 0)
            l.append((ch, addr))
        return l
    
    
    
    
    def ext64(addr):
        #dis64(addr)
        head = list(md64.disasm(code[addr:addr+0x80], addr))
        # 3+5k
        l = []
        for i in range(10):
            if head[3+5*i].mnemonic != 'cmp':
                break
            ch = int(head[3+5*i].op_str.split(',')[-1], 0)
            tar = int(head[4+5*i].op_str, 0)
            inst = list(md64.disasm(code[tar:tar+0x30], tar))[5]
            addr = int(inst.op_str.split(',')[-1], 0)
            l.append((ch, addr))
        return l
    
    
    
    
    flag = 'f4'
    addr = 0x558
    aset = set([0, 0x49e, 0x558])
    switch = 0
    
    
    
    
    def dfs(flag, addr, aset, switch):
        if switch == 0:
            # 32bit
            extract = ext32
        else:
            extract = ext64
        try:
            ca = extract(addr)
        except IndexError:
            print "flag may be"
            print 'npointer{'+flag+'}'
            return
        for c, a in ca:
            if a not in aset:
                dfs(flag+chr(c), a, aset | set([a]), switch ^ 1)
        else:
            #print('dead at',repr(flag))
            pass
    
    
    
    
    dfs(flag, addr, aset, switch)
    

    代码最前面的修改部分是用来修改花指令的,但是使用int指令代替nop主要是因为int指令可以不改变一条指令的长度

    感想

    我好菜我好菜我好菜,出题人原话"本题作为逆向公开赛的第一题,其难度较低",哎继续努力吧.

    补一个控制转移指令的总结

    Call指令

    一条call指令的字节数可以是(2,5,6)byte,对应不同的操作数,下面分情况讨论。

    1. 假设我们在函数src中调用函数dst,对应的call指令是的机器码长度是5 byte,其中第一个字节是e8代表指令,后面四个字节是一个相对偏移offset,dst=src_next+offset,其中src_next是下一条指令的地址,也可以看出是当前指令的地址加上该指令的长度。可以称这种情况为直接转移。
    2. 假设有一个全局函数指针变量g_pDst指向dst函数,我们在函数src中call这个变量,则对应的机器码长度是6 byte,前两个字节是ff 15代表指令,后面四个字节是一个绝对地址即变量g_pDst的地址。可以称这种情况为内存间接寻址。
    3. 假设函数src中有一个函数指针变量pDst指向dst函数,在函数src中call该变量,因为这个指针式保存在栈中的,编译时无法知道它的全局位置,只知道它相对于栈底的位置,所以根据pDst距离栈底ebp的距离。当距离小于0xff时,机器码长度为3,前两个字节是ff 55代表指令,后面一个字节代表相对ebp的偏移(注意偏移都是负数,比如f8,则ebp-8即为实际内存地址)。当距离大于0xff时,机器码长度为6,前两个字节为ff 95表指令,后四个字节是相对ebp的偏移,计算方法与上面相同。可以称这种情况为栈间接寻址。
    4. call寄存器,比如call eax、call ebx;还有call eax + 4。可以称这种情况为寄存器直接寻址。
    5. call寄存器间接转移,比如call [eax],call [ebx];以及call [eax] + 4。可以称这种情况为寄存器间接寻址。
      注:对于第一种情况,还有短转移,也就是偏移为一个字节,但是有些编译器(如MSVC)不会编译这种短的call,总之现在比较少见。为什么没有call functionA + 10 这类代码呢,这是因为编译器在编译的时候会自动计算相加后的地址。

    Ret指令

    ret指令比较简单,只有两种情况,ret和ret n。

    Jmp指令

    直接的jmp分3种
    Short Jump(短跳转)机器码 EB rel8
    只能跳转到256字节的范围内
    Near Jump(近跳转)机器码 E9 rel16/32
    可跳至同一个段的范围内的地址
    Far Jump(远跳转)机器码EA ptr 16:16/32
    可跳至任意地址,使用48位/32位全指针
    要注意的是,短跳转和近跳转指令中包含的操作数都是相对于(E)IP的偏移,而远跳转指令中包含的是目标的绝对地址,所以短/近跳转会出现跳至同一目标的指令机器码不同,不仅会不同,而且应该不同。而远跳转中包含的是绝对地址,因此转移到同一地址的指令机器码相同

    条件转移指令

    条件转移类指令与jmp指令的分类大体类似,但是有一个重要的区别就是,条件转移类指令只有直接转移,没有后面四种,所以条件转移类指令全部是相对偏移的,每种指令可分为长跳转和短跳转。

    相关文章

      网友评论

          本文标题:Nu1L第一次RE公开赛WP

          本文链接:https://www.haomeiwen.com/subject/vheuphtx.html