看雪·深信服 2021 KCTF 春季赛 | 第七题设计思路及解析
2021-05-24 19:22:00 Author: mp.weixin.qq.com(查看原文) 阅读量:132 收藏

KCTF 看雪学院

少年长成,初为武林盟主。在本题,让我们和飞停一起踏上寻亲之旅,找寻他的身世之谜。
在经过4天的鏖战后,共有3支战队完成本题的战斗。mininep 战队再一次抢的一血,获得240分,成功升至第一名。金左手和hzqmwne紧随其后,分别获得181、178的好成绩。

接下来,我们一起来看看这道题的奥秘吧!

出题团队简介

KuCha128(Archaia战队队长),武汉科锐35期学员,喜欢打游戏,痴迷于软件安全技术研究。主要擅长windows平台的逆向,研究最多的是壳和代码流的防御和攻击手段。

赛题设计思路

技术  要点

PE加密壳:

CRC校验:只校验外壳代码,在攻击者patch外壳代码的时候会导致解压缩代码节数据不对。

反调试:使用了两种反调试“IsDebuggerPresent”和“模拟NtQueryInformationProcess”。

加密导入表:如果不脱此加密壳就着手研究,在“首代蜗牛”和“末代蜗牛”调用API的地方,不能直接看到被调用API的名字和地址。
 
蜗牛壳:

SMC加密:使用简单的加密算法保护后代蜗牛的代码,防止破解者看到未来太远的代码。

地址随机化:每一代蜗牛展开蜗牛壳,抛出下一代蜗牛时,随机指定下一代蜗牛的执行地址。

切片技术:将核心算法的代码切成切片,然后将每个切片贴到每层壳上(有可能有的层是没有核心算法代码的切片的),每层壳被执行的时候,相应的切片也会被执行。

代码独立:蜗牛壳在运作的时候,不调用任何外部的API,不使用外部的堆栈。

对抗脚本:破解者在看了前面几层壳的架势之后,不会真的一层一层单步跟踪,而会写脚本脱壳取肉。这是对比之前两题主要改进的地方,利用混淆工具对中间层进行随机混淆,加大破解者寻找蜗牛壳行为特征写脚本的难度。

预期  破解思路

思路1(难以实现):

1. 脱去最外层的"加密壳",得到只有"蜗牛壳"的应用程序。

"蜗牛壳"和最外层的"加密壳"不会相互依赖,所以可以确保最外层的"加密壳"脱掉后能正常运行,当然可能大佬不需要脱"加密壳"就能进行下面的破解步骤。

脱壳比较简单,找到OEP(0x140001088),dump,修复导入表项(导入表项只有system/scanf/printf/GetModuleHandleA/ExitProcess/VirtualProtect六项), 即可完成脱壳。

2. 使用x64dbg(或者ida等其他工具)的trace功能,得到"蜗牛壳"所有的执行过的代码。

(1) 从“首代蜗牛”输入完成一直跟踪到“末代蜗牛”完成比较输出结果,得到中间所有的代码。

(2) 观察并写代码对上面得到的代码进行优化:
去掉跳转代码;
多行汇编代码合并成一行(比如把mov rax, 1和add rax, 2 合并成mov rax, 3)。
 
(3) 经过上面的步骤就能发现,“中间的每一代蜗牛”代码基本上一样,根据特征分离出关键算法代码。

3. 根据关键算法代码写逆算法,此题在算法上没有过多设计,参考大佬们写的《三道八佛》wp算法部分和base64算法稍作修改即可写出KeyGen。

https://bbs.pediy.com/thread-256746.htm

https://bbs.pediy.com/thread-256835.htm
相信有不少选手想尝试思路1的“trace所有的执行过的代码”,都会遇到一个很尴尬的问题:很难trace完,或者莫名其妙trace结束了。

笔者在自己测试时候,使用x64dbg的trace into功能,大概跟踪半小时后, x64dbg会停止trace,直接把程序运行起来,而且还附带着"系统库名称符号不见了"/"Ctrl+G无法跳转地址"等一系列奇怪的现象。这应该是x64dbg的BUG吧,输出数据太多导致无法alloc内存。

思路2

断掉trace的念头(trace可能是条不归路),自己跟踪个一两层代码找特征,然后编写调试脚本能整层整层跑蜗牛壳。
 
要想破解蜗牛壳保护的程序,最关键的步骤就是从万千代码海中提取出“肉代码”,这也是此题的难点所在。
 
比较容易发现利用的特征是每层修改栈时候对gs:[30]写入值,这个在之前比赛的wp中也有选手提到过。

然后利用esp定律,对栈下访问断点(因为每次执行“肉代码”都要先恢复寄存器环境,栈地址将对偏移都是固定的),直接运行到"肉代码"处,然后一直单步跟完这层的肉代码。

脚本log出来的数据需要自己提取出有用的“肉代码”。人肉搞定,或者脚本一通字符串操作也能搞定。
 
所有的"肉代码"拼在一起,就是一个完整的算法函数了,接着思路1的步骤(3)继续完成即可。

题目  小彩蛋

如果直接对printf函数下断点,在最后输出提示信息的时候,rdx/r8/r9寄存器会有字符串。

赛题解析

本赛题解析由看雪论坛 mb_mgodlfyn 给出:

多层自解密,每一层会先清空上一层,解密下一层,然后跳过去。每层的解密逻辑都不完全相同。

真实指令分散在每一层中,还有call-pop-add之类的混淆。
 
有反调试,不能直接调试器启动,但可以启动到输入之后再x64dbg附加。
 
发现输入之后会进入0x140001000的函数对serial做hexdecode,这个函数位置总是固定的。

在它的ret指令处(0x140001084)下断点,r10寄存器指示了serial保存的地方,在这里下硬件读写断点。
 
一次F9到达断点,是一句rep movsq指令,在此时开始运行下面的脚本:跟踪rep movsq指令对serial的复制,并对复制的目标位置下硬件断点。
loop:
find rip, "F348A5", 3cmp $result, 0je out
bphcstistifind rdi-0x60, "7D4DBA7A", 0xc0bph $result+0xc, r, 1
run
jmp loop
out:
(注:$result+0xc是调整后的结果,因为真正处理serial的代码读的并不是第0个字节,而是第0xc个字节)
 
几秒钟后脚本停下来,是movzx r12d, byte ptr [r13 + 0x25]指令,这是真正开始处理serial的地方。
 
dump内存,记录下所有寄存器的值(以及gs:[0x8]和gs:[0x10]两个位置的值)
 
考虑用unicorn模拟执行+capstone反汇编,目的是方便的跟踪程序执行流程以及后续的分析。

直接写了一个基础版本,发现根本跑不完(毕竟原始程序直接执行都需要几秒钟)
 
基于模拟执行的分析肯定要反复尝试,速度必须要足够快,因此做了很多努力来加速:
1. 先在x64dbg执行到真实验证逻辑第一次读取serial时再dump内存,而不是从程序开头就模拟执行。
2. unicorn跑起来后,首先发现卡在了rep movsq指令中(作用是复制代码块)(UC_HOOK_CODE相当于F7单步执行,会在rep的每次重复都断下来,因此需要中断的次数等于rcx寄存器的值,大约几十万到几百万),可以主动读取rdi、rsi、rcx寄存器进行内存复制然后跳过rep movsq指令的执行。对rep stosq同理。
3. 之后发现卡在了一组指令的循环中(这组指令的作用是解密代码块,因此每层壳都会有),循环次数与之前rep movsq复制的长度差不多。循环模式相对固定,从sub rcx, 1 / dec rcx开始(紧跟jne),然后是mov al, [reg],对al做简单运算,然后mov [reg], al,最后是inc reg / dec reg,进入下一轮循环。这里的麻烦是每个循环对al做的运算都不一样,因此不仅需要在hook函数中识别循环,还要找出对al寄存器的运算。之后同样主动计算,然后把rcx赋值为1跳过剩余的循环。
4.(此时还没有加UC_HOOK_MEM)现在大约能在7-8分钟左右跑完一轮trace(从真实逻辑首次读取serial,到最终计算出的name与输入的name进行比较),还是有些慢,发现瓶颈在上一步,用Python做循环解密的速度不够快(python的循环性能确实不高。当然如果完全靠unicorn模拟执行的性能根本跑不出来),对此可以用C写一个dll然后用Python加载。
现在,程序能够在1分钟以内生成完整的trace,这个速度差不多可以接受了。
 
trace有大约80万行,关键指令又分散在垃圾指令中,还是不能人工分析。
下一步考虑基于trace做数据流跟踪(受到 https://bbs.pediy.com/thread-258262.htm 这篇文章启发),看输入的serial从内存中被读出来以及后续计算的流程。

对于任何一条语句,都有来源操作数(内存/寄存器/常量)和目的操作数(内存/寄存器),如果任何一个来源操作数与serial有关("tainted"),则相关的目的操作数也要标记为"tainted",相反的,如果所有来源操作数都没有tainted,则所有目的操作数的tainted标记都要取消。

实现的难度很大,capstone好像没有办法直接获得一条指令相关的源寄存器和目的寄存器(也可能有办法,只是我不知道),只好手动处理部分指令,目标是零漏报的基础上尽可能减少误报。
 
代码如下:
# https://github.com/unicorn-engine/unicorn/blob/master/bindings/python/sample_x86.py# https://github.com/aquynh/capstone/blob/master/bindings/python/capstone/x86.py# https://github.com/aquynh/capstone/blob/master/bindings/python/capstone/x86_const.py
from unicorn import *from unicorn.x86_const import *from capstone import *from capstone.x86_const import *import tracebackimport ctypes

'''// cl /O2 -c dodecryptblock.c// link -DLL -out:libdodecryptblock.dll dodecryptblock.obj
__declspec(dllexport) void dodecryptblock(unsigned char *buf, int len, unsigned char table[256]) { for (int i = 0; i < len; i++) { buf[i] = table[buf[i]]; }}'''libdodecryptblock = ctypes.CDLL("./libdodecryptblock.dll")
#global_serial_start = None#global_serial_len = 32
global_tainted_memlist = set()global_tainted_reglist = set()
global_last_inst = None
class LoopContext: __slots__ = ["tracing", "tracecount", "operations", "direction", "address", "memreg"] def __init__(self): self.clear() def clear(self): self.tracing = False self.tracecount = 0 self.operations = [] self.direction = None self.address = None self.memreg = None
class HoopPrintContext: __slots__ = ["initial_rsp", "last_rsp", "last_address", "last_inst"] def __init__(self): self.initial_rsp = None self.last_rsp = None self.last_address = None self.last_inst = None
def handlekeyboardinterupt(func): def wrapper_func(*args, **kwargs): try: r = func(*args, **kwargs) except KeyboardInterrupt: import os os._exit(0) except Exception: traceback.print_exc() import os import sys sys.stdout.flush() sys.stderr.flush() os._exit(0) return r return wrapper_func
def capstone_reg_to_unicorn_reg(i): d = { X86_REG_RAX : UC_X86_REG_RAX, X86_REG_RBX : UC_X86_REG_RBX, X86_REG_RCX : UC_X86_REG_RCX, X86_REG_RDX : UC_X86_REG_RDX, X86_REG_RBP : UC_X86_REG_RBP, X86_REG_RSP : UC_X86_REG_RSP, X86_REG_RSI : UC_X86_REG_RSI, X86_REG_RDI : UC_X86_REG_RDI, X86_REG_R8 : UC_X86_REG_R8, X86_REG_R9 : UC_X86_REG_R9, X86_REG_R10 : UC_X86_REG_R10, X86_REG_R11 : UC_X86_REG_R11, X86_REG_R12 : UC_X86_REG_R12, X86_REG_R13 : UC_X86_REG_R13, X86_REG_R14 : UC_X86_REG_R14, X86_REG_R15 : UC_X86_REG_R15,
X86_REG_AL : UC_X86_REG_AL, X86_REG_BL : UC_X86_REG_BL, X86_REG_CL : UC_X86_REG_CL, X86_REG_DL : UC_X86_REG_DL, X86_REG_SIL : UC_X86_REG_SIL, X86_REG_DIL : UC_X86_REG_DIL,
X86_REG_RIP : UC_X86_REG_RIP, } return d[i]
def capstone_reg_to_normal_reg(i): d = { X86_REG_RAX : X86_REG_RAX, X86_REG_RBX : X86_REG_RBX, X86_REG_RCX : X86_REG_RCX, X86_REG_RDX : X86_REG_RDX, X86_REG_RBP : X86_REG_RBP, X86_REG_RSP : X86_REG_RSP, X86_REG_RSI : X86_REG_RSI, X86_REG_RDI : X86_REG_RDI, X86_REG_R8 : X86_REG_R8, X86_REG_R9 : X86_REG_R9, X86_REG_R10 : X86_REG_R10, X86_REG_R11 : X86_REG_R11, X86_REG_R12 : X86_REG_R12, X86_REG_R13 : X86_REG_R13, X86_REG_R14 : X86_REG_R14, X86_REG_R15 : X86_REG_R15,
X86_REG_EAX : X86_REG_RAX, X86_REG_EBX : X86_REG_RBX, X86_REG_ECX : X86_REG_RCX, X86_REG_EDX : X86_REG_RDX, X86_REG_EBP : X86_REG_RBP, X86_REG_ESP : X86_REG_RSP, X86_REG_ESI : X86_REG_RSI, X86_REG_EDI : X86_REG_RDI, X86_REG_R8D : X86_REG_R8, X86_REG_R9D : X86_REG_R9, X86_REG_R10D : X86_REG_R10, X86_REG_R11D : X86_REG_R11, X86_REG_R12D : X86_REG_R12, X86_REG_R13D : X86_REG_R13, X86_REG_R14D : X86_REG_R14, X86_REG_R15D : X86_REG_R15,
X86_REG_AX : X86_REG_RAX, X86_REG_BX : X86_REG_RBX,        ...完整代码请点击阅读原文
代码写的不好,修了很久的bug,但是生成的trace还存在问题。
 
加上数据流跟踪之后,跑一遍trace大约需要2分钟。

附件trace.rar里的t6.txt文件是以KCTF为name、以给出的公开serial为serial作为输入得到的结果。用下面的代码筛选出trace到的指令:
with open("t6.txt", "r") as f:    lines = f.readlines()
smallregs = [ "ax", "bx", "cx", "dx", "si", "di", "bp", "sp", "r8w", "r9w", "r10w", "r11w", "r12w", "r13w", "r14w", "r15w",# "al", "ah", "bl", "bh", "cl", "ch", "dl", "dh", "sil", "dil", "bpl", "r8b", "r9b", "r10b", "r11b", "r12b", "r13b", "r14b", "r15b",]
for i in range(len(lines)-3): #if lines[i].startswith("0x") and "rsp" not in lines[i] and "pop" not in lines[i] and "push" not in lines[i] and "cmov" not in lines[i]: if lines[i].startswith("0x"):
line = lines[i] #print(line) inst_str = line.split('\t')[1] mnemonic = inst_str[:inst_str.find(' ')] op_str = inst_str[inst_str.find(' ')+1:] tmp = op_str.split(",") if len(tmp) >= 2: leftop = tmp[0].strip() rightop = tmp[1].strip() else: leftop = None rightop = None
#if lines[i+1].startswith(" tainted"): #if (lines[i+1].startswith(" tainted:") and leftop in smallregs) \ if (lines[i+1].startswith(" tainted:") and "rsp" not in op_str) \ or ("ptr [r13" in lines[i] and (lines[i+1].startswith(" tainted >>>") or lines[i+2].startswith(" tainted >>>"))): print(lines[i], end="") print(lines[i+1], end="") if lines[i+2].startswith(" tainted"): print(lines[i+2], end="")
筛选结果在tt6.txt中,大约2500行,这个长度人工分析是可以接受的。
 
基于数据流分析的结果是可以做死代码消除的,由于没有做,还是有很多垃圾指令。(希望有一种方便的办法能够生成可执行的程序让ida帮忙做这件事)

突破口是读写内存的指令,从这些地方逐渐向上找。
 
最终分析下来的流程是:

1. serial的前16字节和后16字节分别做一次变换

2. 一个类似base64的解码过程,每4字节通过查表和移位得到3字节

3. 上一步的得到24字节,前8字节做一些变换之后判断是否与name的前8字节相等;后16字节做一些变换(与第一步的变换相同)之后判断是否与name的16字节相等(如果输入的name不足16字节,内存中有一些原始数据用来补齐,可以直接提取出来)
数据流跟踪的代码有bug导致这里有些指令的trace丢失了(特别是第3步里对前8字节的处理),只好根据上下文去猜。
 
serial的验证以及从name生成serial的程序如下:
def p16(n):    n &= 0xffff    return n.to_bytes(2, 'little')
def u16(s): assert(len(s) == 2) return int.from_bytes(s, 'little')
def p64(n): n &= 0xffffffffffffffff return n.to_bytes(8, 'little')
def u64(s): assert(len(s) == 8) return int.from_bytes(s, 'little')
def rol16(n, k): n &= 0xffff return ((n << k) | (n >> (16-k))) & 0xffff
def ror16(n, k): n &= 0xffff return ((n >> k) | (n << (16-k))) & 0xffff
def doencrypt1(buf, indextable): assert(len(buf) == 16)
buf1 = bytearray(buf)
v1 = u16(buf1[0:2]) # s1 ^ k4 v2 = u16(buf1[2:4]) # s2 ^ k4 v3 = u16(buf1[4:6]) # s3 + k3 v4 = u16(buf1[6:8]) # s4 - k3 v5 = u16(buf1[8:0xa]) # s5 + k2 v6 = u16(buf1[0xa:0xc]) # s6 + k2 v7 = u16(buf1[0xc:0xe]) # s7 ^ k1 v8 = u16(buf1[0xe:0x10]) # s8 ^ k1
t1 = v2 ^ v1 # s2 ^ s1 t2 = (v4 + v3) & 0xffff # s4 + s3 t3 = (v5 - v6) & 0xffff # s5 - s6 cl = bin(v8 ^ v7).lstrip("0b").count('1') # s8 ^ s7# hamming distance
v7 = ror16(v7, cl) v8 = ror16(v8, cl)
k1 = ( (t2 & t1) | ((~t1) & t3) ) & 0xffff k2 = ( ((k1 * t1) >> cl) + 0x18) & 0xffff k3 = ( k2 ^ t3 ) & 0xffff # 0x79c6 k4 = ( (k2 & k1) | (k3 & (k2 | k1)) ) & 0xffff
s1 = p16(v1 ^ k4) s2 = p16(v2 ^ k4) s3 = p16(v3 - k3) s4 = p16(v4 + k3) s5 = p16(v5 - k2) s6 = p16(v6 - k2) s7 = p16(v7 ^ k1) s8 = p16(v8 ^ k1)
buf1[indextable[0]] = s1[1] buf1[indextable[8]] = s1[0] buf1[indextable[1]] = s2[1] buf1[indextable[9]] = s2[0] buf1[indextable[2]] = s3[1] buf1[indextable[10]] = s3[0] buf1[indextable[3]] = s4[1] buf1[indextable[11]] = s4[0] buf1[indextable[4]] = s5[1] buf1[indextable[12]] = s5[0] buf1[indextable[5]] = s6[1] buf1[indextable[13]] = s6[0] buf1[indextable[6]] = s7[1] buf1[indextable[14]] = s7[0] buf1[indextable[7]] = s8[1] buf1[indextable[15]] = s8[0]
return buf1

def dodecrypt1(serial, indextable): assert(len(serial) == 16)
buf1 = bytearray(serial)
s1 = (buf1[indextable[0]] << 8) | buf1[indextable[8]] s2 = (buf1[indextable[1]] << 8) | buf1[indextable[9]] s3 = (buf1[indextable[2]] << 8) | buf1[indextable[10]] s4 = (buf1[indextable[3]] << 8) | buf1[indextable[11]] s5 = (buf1[indextable[4]] << 8) | buf1[indextable[12]] s6 = (buf1[indextable[5]] << 8) | buf1[indextable[13]] s7 = (buf1[indextable[6]] << 8) | buf1[indextable[14]] s8 = (buf1[indextable[7]] << 8) | buf1[indextable[15]]
''' s1 = u16(buf1[0xb:0xd]) # 0x80c8 s2 = u16(buf1[9:0xb]) # 0x8b84 s3 = u16(buf1[7:9]) # 0xefa8 s4 = u16(buf1[5:7]) # 0xf12e s5 = u16(buf1[3:5]) # 0x0x7a s6 = u16(buf1[1:3]) # 0xba4d s7 = (buf1[0] << 8) | buf1[0xf] # 0x7d70 s8 = u16(buf1[0xd:0xf]) # 0x31b8 '''
t1 = s2 ^ s1 # 0xb4c t2 = (s4 + s3) & 0xffff # 0xe0d6 t3 = (s5 - s6) & 0xffff # 0xe22d cl = bin(s8 ^ s7).lstrip("0b").count('1') # 6 # hamming distance
print(hex(s1), hex(s2), hex(s3), hex(s4), hex(s5), hex(s6), hex(s7), hex(s8)) print(hex(t1), hex(t2), hex(t3), hex(cl))
k1 = ( (t2 & t1) | ((~t1) & t3) ) & 0xffff # 0xe065 buf1[0xc:0xc+2] = p16(rol16(s7 ^ k1, cl)) buf1[0xe:0xe+2] = p16(rol16(s8 ^ k1, cl))
k2 = ( ((k1 * t1) >> cl) + 0x18) & 0xffff # 0x9beb buf1[0x8:0x8+2] = p16(s5 + k2) buf1[0xa:0xa+2] = p16(s6 + k2)
k3 = ( k2 ^ t3 ) & 0xffff # 0x79c6 buf1[0x6:0x6+2] = p16(s4 - k3) buf1[0x4:0x4+2] = p16(s3 + k3)
k4 = ( (k2 & k1) | (k3 & (k2 | k1)) ) & 0xffff buf1[0x0:0x0+2] = p16(s1 ^ k4) buf1[0x2:0x2+2] = p16(s2 ^ k4)
print(hex(k1), hex(k2), hex(k3), hex(k4))
return buf1
def doencode2(s): assert(len(s) % 3 == 0) chartable = [149, 226, 128, 198, 234, 195, 213, 141, 158, 197, 179, 98, 100, 77, 118, 186, 146, 253, 222, 127, 66, 114, 129, 173, 121, 84, 115, 133, 134, 94, 241, 132, 106, 245, 99, 216, 254, 168, 192, 200, 79, 201, 199, 3, 123, 229, 223, 2, 0, 13, 31, 60, 19, 34, 37, 59, 43, 23, 170, 160, 246, 151, 89, 88, 109, 15, 12, 6, 61, 27, 14, 33, 20, 38, 45, 4, 18, 28, 9, 1, 46, 51, 53, 35, 32, 11, 56, 26, 50, 44, 57, 124, 209, 242, 92, 117, 161, 8, 36, 16, 40, 41, 63, 49, 7, 10, 47, 17, 25, 30, 62, 21, 29, 42, 58, 52, 22, 5, 54, 55, 39, 48, 24, 108, 74, 122, 68, 152, 150, 105, 196, 235, 204, 73, 190, 181, 72, 113, 148, 225, 163, 177, 120, 250, 83, 70, 64, 203, 188, 71, 131, 193, 238, 249, 232, 97, 169, 251, 194, 210, 76, 85, 218, 247, 126, 217, 143, 172, 227, 82, 96, 155, 233, 86, 156, 137, 87, 180, 81, 125, 176, 116, 142, 162, 157, 237, 182, 224, 95, 252, 75, 110, 165, 65, 208, 167, 187, 240, 140, 145, 101, 185, 212, 230, 135, 184, 191, 248, 236, 159, 154, 211, 111, 147, 93, 102, 136, 67, 91, 80, 243, 130, 183, 206, 103, 231, 244, 255, 175, 205, 214, 220, 171, 104, 90, 138, 221, 239, 228, 189, 78, 164, 119, 178, 69, 166, 153, 112, 219, 107, 215, 144, 207, 174, 139, 202] rchartable = [None]*256 for i, c in enumerate(chartable): rchartable[c] = i
r = b"" for i in range(0, len(s), 3): x, y, z = s[i], s[i+1], s[i+2] a = (x & 0x3) | ((z & 0xf) << 2) b = (y & 0x3c) | ((x & 0xc) >> 2) c = (x >> 4) | ((y & 0x3) << 4) d = ((y & 0xc0) >> 2) | (z >> 4) r += bytes([rchartable[a], rchartable[b], rchartable[c], rchartable[d]]) return r

def dodecode2(s): assert(len(s) % 4 == 0) chartable = [149, 226, 128, 198, 234, 195, 213, 141, 158, 197, 179, 98, 100, 77, 118, 186, 146, 253, 222, 127, 66, 114, 129, 173, 121, 84, 115, 133, 134, 94, 241, 132, 106, 245, 99, 216, 254, 168, 192, 200, 79, 201, 199, 3, 123, 229, 223, 2, 0, 13, 31, 60, 19, 34, 37, 59, 43, 23, 170, 160, 246, 151, 89, 88, 109, 15, 12, 6, 61, 27, 14, 33, 20, 38, 45, 4, 18, 28, 9, 1, 46, 51, 53, 35, 32, 11, 56, 26, 50, 44, 57, 124, 209, 242, 92, 117, 161, 8, 36, 16, 40, 41, 63, 49, 7, 10, 47, 17, 25, 30, 62, 21, 29, 42, 58, 52, 22, 5, 54, 55, 39, 48, 24, 108, 74, 122, 68, 152, 150, 105, 196, 235, 204, 73, 190, 181, 72, 113, 148, 225, 163, 177, 120, 250, 83, 70, 64, 203, 188, 71, 131, 193, 238, 249, 232, 97, 169, 251, 194, 210, 76, 85, 218, 247, 126, 217, 143, 172, 227, 82, 96, 155, 233, 86, 156, 137, 87, 180, 81, 125, 176, 116, 142, 162, 157, 237, 182, 224, 95, 252, 75, 110, 165, 65, 208, 167, 187, 240, 140, 145, 101, 185, 212, 230, 135, 184, 191, 248, 236, 159, 154, 211, 111, 147, 93, 102, 136, 67, 91, 80, 243, 130, 183, 206, 103, 231, 244, 255, 175, 205, 214, 220, 171, 104, 90, 138, 221, 239, 228, 189, 78, 164, 119, 178, 69, 166, 153, 112, 219, 107, 215, 144, 207, 174, 139, 202]
tmp = bytearray(len(s)) for i, c in enumerate(s): tmp[i] = chartable[c]
for c in tmp: assert(c < 0x40)
r = b"" for i in range(0, len(tmp), 4): a, b, c, d = tmp[i], tmp[i+1], tmp[i+2], tmp[i+3] x = ((c << 4) & 0xff) | ((b & 0x3) << 2) | (a & 0x3) y = ((d & 0xf0) << 2) | (b & 0x3c) | ((c & 0x3f) >> 4) z = ((d << 4) & 0xff) | ((a & 0x3f) >> 2) r += bytes([x, y, z]) return r
def doencode3(r): assert(len(r) == 8) r = p64(u64(r)^0x8267f5d9b0ea143c) s = bytearray(8) s[3] = r[0] s[2] = r[2] ^ s[3] s[1] = r[1] ^ s[2] s[0] = r[3] ^ s[1] s[7] = r[4] s[6] = r[6] ^ s[7] s[5] = r[5] ^ s[6] s[4] = r[7] ^ s[5] return s
def dodecode3(s): assert(len(s) == 8) r = bytearray(8) r[0] = s[3] r[1] = s[1] ^ s[2] r[2] = s[2] ^ s[3] r[3] = s[0] ^ s[1] r[4] = s[7] r[5] = s[5] ^ s[6] r[6] = s[6] ^ s[7] r[7] = s[4] ^ s[5] return p64(u64(r)^0x8267f5d9b0ea143c)
def name_to_serial(name): assert(len(name) <= 16)
buf5 = bytearray(b"\x9a\x1e\x1d\x1c\x1b\x1a\x19\x18\x17\x16\x15\x14\x13\x12\x11\x10") buf5[:len(name)] = name if len(name) < 16: buf5[len(name)] = 0 print(buf5.hex())
# stage4
indextable3 = [0xb, 0xd, 0xf, 0x1, 0x3, 0x5, 0x7, 0x9, 0xc, 0xe, 0x0, 0x2, 0x4, 0x6, 0x8, 0xa] buf3_2 = doencrypt1(buf5, indextable3) print(buf3_2.hex())
# stage3
print("buf5", buf5[:8]) buf3_1 = doencode3(buf5[:8]) print(buf3_1.hex())
buf3 = buf3_1 + buf3_2
# stage2
buf2 = doencode2(buf3) print(buf2.hex())
# stage1
indextable1 = [0xc, 0xa, 0x8, 0x6, 0x4, 0x2, 0x0, 0xe, 0xb, 0x9, 0x7, 0x5, 0x3, 0x1, 0xf, 0xd] indextable2 = [0xb, 0x9, 0x7, 0x5, 0x3, 0x1, 0xf, 0xd, 0x8, 0x6, 0x4, 0x2, 0x0, 0xe, 0xc, 0xa]
serial = doencrypt1(buf2[:16], indextable1) + doencrypt1(buf2[16:], indextable2) print("serial") print(serial.hex())
return serial
def serial_to_name(serial): assert(len(serial) == 32)
# stage1
indextable1 = [0xc, 0xa, 0x8, 0x6, 0x4, 0x2, 0x0, 0xe, 0xb, 0x9, 0x7, 0x5, 0x3, 0x1, 0xf, 0xd] indextable2 = [0xb, 0x9, 0x7, 0x5, 0x3, 0x1, 0xf, 0xd, 0x8, 0x6, 0x4, 0x2, 0x0, 0xe, 0xc, 0xa]
buf2 = dodecrypt1(serial[:16], indextable1) + dodecrypt1(serial[16:], indextable2) print("buf2:", buf2.hex(), buf2)
# stage2
buf3 = dodecode2(buf2) print("buf3", buf3.hex())
# stage3
print(buf3[:8].hex()) buf4 = dodecode3(buf3[:8])
# stage4
indextable3 = [0xb, 0xd, 0xf, 0x1, 0x3, 0x5, 0x7, 0x9, 0xc, 0xe, 0x0, 0x2, 0x4, 0x6, 0x8, 0xa] buf5 = dodecrypt1(buf3[8:], indextable3)
print(buf5)
# stage5
assert(buf4 == buf5[:8])
return bytes(buf5)
test_serial = bytes.fromhex(("7D 4D BA 7A 9C 2E F1 A8 EF 84 8B C8 80 B8 31 70" + "43 1F 37 E5 99 04 8D BB 88 C3 06 BC 40 35 79 D1").replace(" ", ""))
#print(name_to_serial(b"FE0C37052AED0E33"))#serial_to_name(test_serial)
serial = name_to_serial(b"KCTF")print(serial_to_name(serial))
print(serial.hex().upper()) # B91AE5FCDA57D87406968CBDB8829799790A77302D7E8754B705894489B37A10
附件(请点击阅读原文,去原帖下载附件):

memdump.rar:运行到真正的验证逻辑首次开始读取serial时的内存状态以及寄存器状态,用于模拟执行。

trace.rar:基于模拟执行生成的完整trace文件以及过滤结果。t6.txt和tt6.txt直接对应memdump,输入为KCTF / 公开的序列号;t7.txt和tt7.txt则是在unicorn中把serial对应的内存覆盖为真正的serial后得到的结果。

往期解析

1. 看雪·深信服 2021 KCTF 春季赛 | 第二题设计思路及解析

2. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析
3. 看雪·深信服 2021 KCTF 春季赛 | 第四题设计思路及解析
4. 看雪·深信服 2021 KCTF 春季赛 | 第五题设计思路及解析
5. 看雪·深信服 2021 KCTF 春季赛 | 第六题设计思路及解析
主办方
看雪CTF(简称KCTF)是圈内知名度最高的技术竞技之一,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
看雪CTF比赛历史悠久、影响广泛。自2007年以来,看雪已经举办十多个比赛,与包括金山、360、腾讯、阿里等在内的各大公司共同合作举办赛事。比赛吸引了国内一大批安全人士的广泛关注,历年来CTF中人才辈出,汇聚了来自国内众多安全人才,高手对决,精彩异常,成为安全圈的一次比赛盛宴,突出了看雪论坛复合型人才多的优势,成为企业挑选人才的重要途径,在社会安全事业发展中产生了巨大的影响力。
合作伙伴
深信服科技股份有限公司成立于2000年,是一家专注于企业级安全、云计算及基础架构的产品和服务供应商,致力于让用户的IT更简单、更安全、更有价值。目前深信服在全球设有50余个分支机构,员工规模超过7000名。
比赛火热进行中!立即挑战!

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

球分享

球点赞

球在看

“阅读原文一起来充电吧!

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