CatCTF 2022 BugCat复现

CatCTF 2022 BugCat复现

脱壳

拿到题目,发现题目是套了壳子的,经过检查发现是UPX改,使用 x32dbg 进行调试,在TLS回调中过掉下图所示的对 start 函数头部是否存在断点的检测。

Untitled

然后在壳解压缩并释放完成之前就没有什反调试了,反正我没发现,在解密操作完成之后使用自带的插件dump即可。

去除花指令

由于程序存在大量的花指令导致IDA无法正常识别汇编代码,所以在分析之前还需要去除花指令,编写了简单的脚本

from idaapi import *
from idc import *

def nop(addr, end):
    while addr < end:
        patch_byte(addr, 0x90)
        addr += 1

def anti_flower(start, end):
    del_items(start, 0, end - start)
    addr = start
    while addr < end:
        create_insn(addr)
        if print_insn_mnem(addr) == 'stc':
            create_insn(addr + 1)
            if print_insn_mnem(addr + 1) == 'jb':
                target = get_operand_value(addr + 1, 0)
                if addr + 1 < target < end:
                    nop(addr, target)
                    addr = target
                    continue
        elif print_insn_mnem(addr) == 'jmp':
            target = get_operand_value(addr, 0)
            if addr < target < end and target < addr + 0x10:
                nop(addr, target)
                addr = target
                continue
        elif print_insn_mnem(addr) == 'call':
            target = get_operand_value(addr, 0)
            create_insn(target)
            if print_insn_mnem(target) == 'add' and get_operand_value(target, 0) == 0x4:
                off = get_operand_value(target, 1)
                naddr = addr + get_item_size(addr) + off
                nop(addr, naddr)
                addr = naddr
                continue
        elif print_insn_mnem(addr) == 'clc':
            create_insn(addr + 1)
            if print_insn_mnem(addr + 1) == 'jnb':
                target = get_operand_value(addr + 1, 0)
                if addr + 1 < target < end:
                    nop(addr, target)
                    addr = target
                    continue
        addr += get_item_size(addr)
    add_func(start, end)

大体流程分析

main函数内主要做了接收输入,以及对check函数的调用,如图所示参数是按栈传递的,所以反编译效果好像不太行。

Untitled

除此之外,应该格外留意在main函数的最开始,注册了一个 VEH

Untitled

check函数的开始,判断了长度为 30,以 flag{ 开头,以 } 结尾

Untitled

后面,对flag{}括号中的每一位的取值范围进行校验

Untitled

然后启动了一个线程,可以暂时看成 calFlag(6)

Untitled

calFlag 的代码大致如下:

Untitled

看起来好像是 6 个一组计算了 crc 然后结果xor后对比。

但是这是,这其中夹杂着一行 int 2D ,所以从这里会跳到刚才注册的 VEH 里

Untitled

不仅如此,注意到还有 TryLevel ,所以还应该有 SEH,查看汇编发现关于 SEH 的调用

Untitled

在SEH 代码里,又找到了对于 UEH 的注册

Untitled

除此之外,这个程序还有 TLS回调我们还没有分析。而且前面没有提到的是,在壳进入正常代码之前,曾主动调用 TLS回调,下面接着来分析一下它的逻辑:

Untitled

结合数据分析,其实是调用了两个函数, sub_401BB0sub_4013F0

其中 sub_401BB0 中主要是反调试,并且设置了 BeingDebugged

Untitled

sub_4013F0 中主要是对程序的运行信息进行了检测,并注册了 VCH

此处检查导致,在命令行中启动该程序时,如果未在运行程序名后加空格,将导致 VCH 无法注册

Untitled

至此,我们终于可以回到我们上述的 calFlag 函数(我自己起的名字),继续分析

按照我大哥 ChatGPT 的说法 [手动狗头], int 2d 后首先会走到 VEH

Untitled

即图所示位置,显然这里注册了4个硬件断点,然后注意他 return -1 也就是说,这个异常到此为止,不会往后传递了。

Untitled

在每次走到硬件断点之后,还是会来到 VEH,但是这一次他 return 0 ,所以我们还按照我大哥 ChatGPT 的说法,继续往下看 SEH

Untitled

SEH逻辑如下,可以看到,处理了 Dr0, Dr1, Dr3,都没拦截,所以还得继续找 UEH

Untitled

UEH逻辑如下,可以看到对 Dr3 进行了处理而且没再往后传递,而其他情况还是往后传递到 VCH

Untitled

VCH作为全村最后的希望,处理了 Dr2, 并且都给拦截了。

Untitled

结合断点的位置分析,可以得出正确的代码是,

  • 4 个 1 组,见SEH
  • 乘以 257,见VCH
  • 最后 xor 的值是另一个,即 sub_401180() 函数的返回值,见 SEH和UEH
  • 对比要从下标 2 开始,Tls回调中设置了BeingDebugged ,所以开始 index = 1,再加上VEH中的对他进行了 *2

那么,现在问题只有一个了sub_401180() 函数干了啥,代码如下,但是我懒得管了,毁灭吧,Frida启动,进入脚本小子模式

Untitled

// 脚本小子,永不为奴
var GetModuleHandleA = Module.findExportByName("kernel32.dll", "GetModuleHandleA");
Interceptor.attach(GetModuleHandleA, {
    onLeave: function (retval) {
        var hash = 0;
        for (let index = 0; index < 0x4000; index++) {
            const v = retval.add(index).readU8();
            hash  = (31 * hash + v) & 0xffffffff;
        }
        console.log(`GetModuleHandleA onLeave: 0x${hash.toString(16)}`);
    }
})

Untitled

开始接下来爆破就行了。

脚本写的很垃圾,所以效率很低,但是问题不大,因为我这是复现,做之前我就知道 flag。

hashTable = [0x0A876C04, 0xA549A7D9, 0x66BF1BAC, 0x473AC6FC, 0xB3440AD8, 0xA9D1C940, 0x260E16E8, 0x0B465229,
             0xE906517D, 0x50972A2B, 0x74798AA7, 0x5588BA88, 0x4433C0D0, 0x289B6CEF, 0x71A8B6EC, 0x53775C73]
off = 2
flag = []
for i in range(6):
    hash = 0
    for i1 in range(33, 127):
        if hash == -1:
            break
        for i2 in range(33, 127):
            if hash == -1:
                break
            for i3 in range(33, 127):
                if hash == -1:
                    break
                for i4 in range(33, 127):
                    if hash == -1:
                        break
                    hash = i1
                    hash = i2 + hash * 257
                    hash = i3 + hash * 257
                    hash = i4 + hash * 257
                    hash ^= 0x52251ff
                    if hash == hashTable[i + off]:
                        flag.append(i1)
                        flag.append(i2)
                        flag.append(i3)
                        flag.append(i4)
                        hash = -1
                        print(''.join(chr(c) for c in flag))

print(''.join(chr(c) for c in flag))
posted @ 2023-02-01 17:51  gaoyucan  阅读(543)  评论(0编辑  收藏  举报