看雪2022 KCTF 秋季赛 | 第十题设计思路及解析
2022-12-11 18:0:27 Author: 看雪学苑(查看原文) 阅读量:7 收藏

看雪 2022 KCTF秋季赛 已于11月15日中午12点正式开始!比赛延续上一届的模式并进行优化,对每道题设置了难度值、火力值、精致度等多类积分,用规则引导题目的难度和趣味度。大家请注意:签到题(https://ctf.pediy.com/game-season_fight-216.htm)将持续开放,整个比赛期间均可提交答案,获得积分哦~
今天中午12点,第十题《两袖清风》已经关闭答题通道。

共5个战队攻破此题:
下面一起看看该赛题的设计思路和相关解析吧~

出题团队简介

出题方:天外星系  战队 
战队成员id:geekfire

赛题设计思路

题目名称:NoLimit
输出提示:key正确则输出提示good!
本题注册算法通过两段shellcode实现,分别简称为sc1 sc2

算法的核心步骤是根据生成的很简单的迷宫地图和输入的迷宫路径来验证是否能走出迷宫。

其中sc1负责生成迷宫地图,如果检测到调试信息等会生成错误的伪迷宫。
SC2负责验证迷宫,如果迷宫验证无误,则会修改提示信息为good!否则为no!

如果key正确则输出提示good! 否则输出no!或者不输出任何提示。
另外sc1通过aes 算法加密,私钥的前4个字符需要枚举才能解密,后面的字符被RSA加密。

(1)aes秘钥后部分解密:

给定的rsa算法里面的n可以快速被分解,通过分解n求出rsa私钥,即可解密,得到后部分秘钥为:AllIsNothing。

(2)aes秘钥前四个字符枚举条件 四个字符必须为数字类型

1 00 1
1、把前4个字符任意排列,只要其中一组排列满足:按上表所示,横向 和 纵向 两个字符相加结果要全部相等

2、通过aes秘钥尝试解密sc1,使得sc1解密后执行时不产生异常

枚举通过python + vc代码实现
python负责枚举 vc负责解密sc1并执行

其中vc 部分代码如下:
    SetUnhandledExceptionFilter(callback);    int ScSize = 0;    string FindScDec;    string AesKeyPreffix;    if (argc > 1)        AesKeyPreffix = argv[1];    string AesKeySuffix = "AllIsNothing";    string AesKey;    string aesIV = "ABCDEF0123456789";//128 bits    AES aes;    int size = strlen(FindScEnc) / 2;    TextToHex(FindScEnc, size);
AesKey = AesKeyPreffix + AesKeySuffix; FindScDec = (char*)aes.DecryptCBC((unsigned char*)FindScEnc, size, (unsigned char*)AesKey.c_str(), (unsigned char*)aesIV.c_str());//CBC ScSize = FindScDec.size(); void* ptr = NULL;
ptr = VirtualAlloc( NULL, ScSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (ptr == NULL) { //printf("Failed to allocate memory: error=%u\n", GetLastError()); return 1; } memcpy(ptr, FindScDec.c_str(), ScSize); __try{ ((void(*)())ptr)(); printf("success!\r\n");} __except (filterException(GetExceptionCode(), GetExceptionInformation())) {
printf("the aes preffix %s is error\r\n", AesKeyPreffix.c_str()); } return 0;
把上述代码编译为NoLimit.exe,然后用脚本调用枚举

python 枚举部分代码为:
# -*- coding: UTF8 -*-import osimport threadingimport datetimechrs = '0123456789'AesPreffix = ''a=b=c=d=''AesPreffixS =[]starttime = datetime.datetime.now()threads = []class Getoutofloop(Exception):    passdef runsc(AesPreffix):    cmd = os.popen('NoLimit.exe ' + AesPreffix)    result = cmd.read()    print("result:" + result)    if 'success' in result:        print('AesPreffix is '+ AesPreffix)        AesPreffixS.append(AesPreffix)for a in chrs:    AesPreffix += a    for b in chrs:        AesPreffix += b        for c in chrs:            AesPreffix += c            for d in chrs:                AesPreffix += d                           t = threading.Thread(target=runsc, args=(AesPreffix,))                threads.append(t)                t.start()                   #os._exit(0)                AesPreffix = AesPreffix[:-1]            AesPreffix = AesPreffix[:-1]        AesPreffix = AesPreffix[:-1]    AesPreffix = AesPreffix[:-1]for t in threads:    t.join(2)os.system('taskkill /f /im %s' % 'NoLimit.exe')#print(AesPreffixS)str = ''lastresult = []try:    for p in AesPreffixS:        for i in range(0,4):            a = p[i]            for j in range(0,4):                if j == i :                    continue                b = p[j]                for k in range(0,4):                    if k == j or k == i:                        continue                    c = p[k]                    for l in range(0,4):                        if l == k or l == j or l == i:                            continue                        d = p[l]                        str = a + b + c + d                        #print(str)                        num = ord(str[0]) + ord(str[1])                        if num == ord(str[2]) + ord(str[3]) and num == ord(str[0]) + ord(str[2]) and num == ord(str[1]) + ord(str[3]):                            if p not in lastresult:                                lastresult.append(p)                            break;                            #raise Getoutofloop()except Getoutofloop:    passprint('AesPreffix:')print(lastresult)endtime = datetime.datetime.now()print('time:%ds' % (endtime - starttime).seconds )
枚举完成大概不到3分钟,结果如下:
result:
AesPreffix:
['1441', '1010', '2222', '2424', '3773', '4646']
time:133s
得到6个结果,6个前缀对应的解密后的sc1分别为:
1441    test edi,ebx    jl 1C8D88FFFDD//该跳转不执行    fdiv st(0),st(1)    ret
2222
ret
2424
xor cl,byte ptr ds:[rbx+19] ret
3773 ret
4646 adc ch,al ret
1010 call 1D18DAF0004 接着: 1D18DAF0004:inc eax pop rdi mov ecx,10107B1 xor ecx,1010101 add rdi,1E xor esi,esi cld mov al,byte ptr ds:[rdi] cmp al,11 cmove eax,esi stosb loop 1D18DAF0019 push rbx push rsi push rdi push r12 push r13 ... ...
除了1010外 其他都都是执行两三条汇编指令后就返回了,这时可以判断Aes秘钥为1010AllIsNothing。

(3)解密了sc1后 sc1负责生成迷宫

过了一些调试和一部分条件后生成真实迷宫如下:
01 01 01 01 01 01 01 01 01 0101 01 01 01 01 01 01 01 01 0101 01 01 01 01 01 01 01 01 0100 00 00 00 01 01 01 01 01 0101 01 01 00 01 01 01 01 01 0101 00 00 00 00 00 00 01 01 0101 00 01 01 01 01 00 01 01 0101 00 00 00 00 01 00 00 00 0101 01 01 01 00 01 00 01 01 0101 01 01 01 00 01 01 01 01 01
其中00 表示可以通过的路径,然后sc2 负责验证迷宫路径是否正确。

路径的坐标为:
30 31 32 33 43 53 52 51 61 71 72 73 74 84 94

(4)key的组成

1、用来解密的rsa私钥
2、AES秘钥前4个字符
3、路径长度
4、路径坐标
最后key 为:
1BDF5752B86533B0EF0C488375EBFE389163712709D3FEE35C7679A1AB7A8E697366227CAF168C99DD7F110100F303132334353525161717273748494

(5)最后留了一个干扰项

有个求解方程的逻辑,实际上无解,就算解出来了也会输出no!

赛题解析

本题解析由看雪论坛专家【ThTsOd】提供:

个人主页:https://bbs.pediy.com/user-home-940451.htm

1、NoLimit

输入长度 >= 0x59,<= 0xcd

2、计算rsa

cipher 0xB20446102D1C343D0575674CA28EBC0419BCFE4D75682C2AC81C9502454650BDDAEF6968AF269B54C182
N = 0x4F62187B5F6590C6CFF0FBDBBEBDAF60AA861BD2F66F8F7FFD57A66AE50DB7D2FFFFFFFFFFFFFFFFFFFFF
E = 0x11
N可直接分解
计算出 D 243855937587318030864454877487317725150433472540948954237790304930432311137671434982797927461139896305
转为hex 1BDF5752B86533B0EF0C488375EBFE389163712709D3FEE35C7679A1AB7A8E697366227CAF168C99DD7F1

3、AES解密code

key为 4位数字 + rsa解密内容("AllIsNothing") + "00 CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD"
4位数字要求两两相同,爆破脚本
import base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpad def AES_Decrypt(key, data):  iv = bytearray.fromhex("41 42 43 44 45 46 30 31 32 33 34 35 36 37 38 39")  cipher = AES.new(key, AES.MODE_CBC, iv)  text_decrypted = cipher.decrypt(data) #   try:#     text_decrypted = unpad(text_decrypted, AES.block_size, "pkcs7")#   except:#       return None  if(text_decrypted[-1] != 0x00):    return None# 去补位  return text_decrypted data = bytearray.fromhex("94 C7 A9 05 C7 DC ... 22 6A") #密文 for i in range(10000):    key = b""    t=[]    n=i    for l in range(4):        t.append((n%10)+48)        n//=10    key = bytearray(t)    key += bytearray.fromhex("41 6C 6C 49 73 4E 6F 74 68 69 6E 67 00 CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD")    result = AES_Decrypt(key,data)    if(result != None):        print(key[0:4],(result[0:0x10]).hex())
观察到 1010 像是解密正确的结果。

4、走迷宫?

EnumWindows 里会检查窗口是不是有 ida x64dbg,但是转换了大写,这就检测不到了。

GetTickCount64时间检测,ResumeThread执行解密代码,随后对一处反调试代码校验。

UuidFromStringA获取第2段代码,通过EnumSystemLocalesA回调执行代码:

两个bin都有简单的自解密,处理后dump分析
输入第一个字节是长度,随后是半个字节的范围在0-9
bin1检查输入合法,第一步YX要求X=0,每一步要求与上一步在相邻行或者列(这里写错了,导致可以跨行跨列瞬移),最后一步要求在边界,生成正确的迷宫。
bin2校验输入,同bin1检查输入,同时检查输入长度,以及每一步都是00
可能的解
17 3031323343515253545556617166727374847677788694
17 3031324351525354555661717273746676777884948694
0F 303132334353525161717273748494(有争议,部分机器可行)
实际提交解 1BDF5752B86533B0EF0C488375EBFE389163712709D3FEE35C7679A1AB7A8E697366227CAF168C99DD7F11010173031324351525354555661717273746676777884948694

END

第11题《衣锦昼行》正在进行
https://ctf.pediy.com/game-season_fight-226.htm
欢迎参赛或围观

- End -

球分享

球点赞

球在看

“阅读原文查看详情!

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