CatCTF 2022 BugCat复现
脱壳
拿到题目,发现题目是套了壳子的,经过检查发现是UPX改,使用 x32dbg 进行调试,在TLS回调中过掉下图所示的对 start
函数头部是否存在断点的检测。
然后在壳解压缩并释放完成之前就没有什反调试了,反正我没发现,在解密操作完成之后使用自带的插件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函数的调用,如图所示参数是按栈传递的,所以反编译效果好像不太行。
除此之外,应该格外留意在main函数的最开始,注册了一个 VEH
check函数的开始,判断了长度为 30
,以 flag{
开头,以 }
结尾
后面,对flag{}括号中的每一位的取值范围进行校验
然后启动了一个线程,可以暂时看成 calFlag(6)
calFlag 的代码大致如下:
看起来好像是 6 个一组计算了 crc 然后结果xor后对比。
但是这是,这其中夹杂着一行 int 2D
,所以从这里会跳到刚才注册的 VEH 里
不仅如此,注意到还有 TryLevel
,所以还应该有 SEH,查看汇编发现关于 SEH
的调用
在SEH 代码里,又找到了对于 UEH
的注册
除此之外,这个程序还有 TLS回调我们还没有分析。而且前面没有提到的是,在壳进入正常代码之前,曾主动调用 TLS回调,下面接着来分析一下它的逻辑:
结合数据分析,其实是调用了两个函数, sub_401BB0
和 sub_4013F0
其中 sub_401BB0
中主要是反调试,并且设置了 BeingDebugged
而sub_4013F0
中主要是对程序的运行信息进行了检测,并注册了 VCH
此处检查导致,在命令行中启动该程序时,如果未在运行程序名后加空格,将导致
VCH
无法注册
至此,我们终于可以回到我们上述的 calFlag
函数(我自己起的名字),继续分析
按照我大哥 ChatGPT
的说法 [手动狗头], int 2d
后首先会走到 VEH
即图所示位置,显然这里注册了4个硬件断点,然后注意他 return -1
也就是说,这个异常到此为止,不会往后传递了。
在每次走到硬件断点之后,还是会来到 VEH,但是这一次他 return 0
,所以我们还按照我大哥 ChatGPT 的说法,继续往下看 SEH
SEH逻辑如下,可以看到,处理了 Dr0, Dr1, Dr3,都没拦截,所以还得继续找 UEH
UEH逻辑如下,可以看到对 Dr3 进行了处理而且没再往后传递,而其他情况还是往后传递到 VCH
VCH作为全村最后的希望,处理了 Dr2, 并且都给拦截了。
结合断点的位置分析,可以得出正确的代码是,
- 4 个 1 组,见SEH
- 乘以 257,见VCH
- 最后 xor 的值是另一个,即
sub_401180()
函数的返回值,见 SEH和UEH - 对比要从下标 2 开始,Tls回调中设置了
BeingDebugged
,所以开始 index = 1,再加上VEH中的对他进行了 *2
那么,现在问题只有一个了sub_401180()
函数干了啥,代码如下,但是我懒得管了,毁灭吧,Frida启动,进入脚本小子模式
// 脚本小子,永不为奴
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)}`);
}
})
开始接下来爆破就行了。
脚本写的很垃圾,所以效率很低,但是问题不大,因为我这是复现,做之前我就知道 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))
标签:CatCTF,hash,addr,BugCat,insn,flag,2022,print,target
From: https://www.cnblogs.com/gaoyucan/p/17083704.html