看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

WriteUp 8个月前 admin
114 0 0

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

这是一场人类与超智能AI的“生死”较量

请立刻集结,搭乘SpaceX,前往AI控制空间站

智慧博弈  谁能问鼎


看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。

*注意:签到题持续开放,整个比赛期间均可提交答案获得积分


今天中午12:00第五题《争分夺秒》已截止答题,该题共有9支战队成功提交flag,一起来看下该题的设计思路和解析吧。



出题团队简介


出题战队:试试水

战队成员:mb_egsgqdnl

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析


设计思路


模式:Window平台CrackMe方案一


混淆说明:

程序中有大量的计算混淆代码,主要用于干扰逆向算法。混淆代码具有一定的特征,仔细分析对比可识别出。需要解题人根据模式识别剔除无效代码。
混淆代码的C源码:
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析
编译后的汇编代码:
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

算法思路:

Base64统一输入范围,通过CRC32验证输入有效性。通过逆元生成两个唯一Key,使用该Key生成幻方。用生成的幻方进行对称加密。最终使用逆元计算验证加密结果。

解题思路:

解题人首先需要识别出混淆代码,将其nop后才能看到核心算法。大概的核心算法如下。

输入Key的格式如下:
// key的格式如下描述(单位:字节)//  -----------------------------------------------------------//  | 4 | 2 | ####不定长度#### | 4 | 2 | ####不定长度#### |  4  |//  -----------------------------------------------------------////  对应下面解释//  -------------------------------------------------------------//  | 幻|长   |                  | 幻 | 长|               |     |//  | 方|度   |   #key1#         | 方 | 度|     #key2#    |校验和|//  |key|len|                  |key |len|                 |     |//  -------------------------------------------------------------

验证过程:
(1)base64解码
(2)校验和验证
(3)验证key1/key2长度
(4)通过逆元验证两个幻方key唯一性
(5)根据两个幻方key分别生成两个幻方,并分别对key1和key2进行异或
(6)通过逆元验证key1和key2的唯一性
(7)通过以上所有验证,返回成功,否则返回失败



赛题解析


本题解析由看雪大牛 moyiah 提供:
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析


01

静态分析

静态看,程序要是大量加花了不过经过仔细查看,原有正常指令没有任何变形,还是非常能看的状态,于是直接带花分析


分析技巧:
  1. 基本只通过交叉引用来移动位置,而不通过上下滑动。
    查看所有参数与return的交叉引用,基本上相当于去花了。
    只是代码片段有所分散。

  2. 结合动态调试,重点关注call前后的入参、出参变化。




02

整体流程

根据ReadMe.txt与main函数可知,程序通过argv[1]取得序列号输入。
  1. base64解码argv[1]字符串得到字节

  2. 计算除最后4个字节以外的前面字节的crc32,并需要等于最后4个字节。
    即,最后4个字节是前面字节的crc32。

  3. 然后按特定位置、特定长度取了2个4字节整数,用不同的常量,调用了同一个检查函数,实际是某种方程,具体见后面“小整数方程”。

  4. 小整数方程过去之后,发现疑似大整数加法、大整数乘法(0043C130)函数。
    对疑似乘法做日志断点,输出乘法调用前后的参数值。
    基本认定这一步是小整数那一步的放到大整数了。
    大整数方程通过找到在线网站求出解。

  5. 解完大整数,处理完几轮异或即可得出flag。



03

调试器检测暗坑

出现有几处如下代码,可能是暗坑。inside_dbg(ea=0x402220)里面有两个函数做调试器检测。

不管三七二十一,直接静态patch掉调试器判断,使得inside_dbg始终返回0。
if ( inside_dbg() ){  if ( (*Block & 1) != 0 )    *Block += rand() % 3 + 1;  else    *Block += rand() % 4 + 4;}



04

小整数方程

第一组小整数方程 32bit整数:

n = 0x346F8717i64;a = 0x7D45i64;求解x使得 a*x % n = 1;

a = 0xD711i64;n = 0x729969FFi64;求解x使得 a*x % n = 1;

直接for循环枚举出结果,并且有意确认了这里是否会出现多个解。
printf("A = n");unsigned long long n = 0x346F8717i64;unsigned long long a = 0x7D45i64;for (unsigned int i = 0; i < -1; ++i){    unsigned long long x = i;    if (a * x % n == 1)    {        printf("0x%xn", i);    }}printf("nB = n"); a = 0xD711i64;n = 0x729969FFi64;for (unsigned int i = 0; i < -1; ++i){    unsigned long long x = i;    if (a * x % n == 1)    {        printf("0x%xn", i);    }}

很快出结果:
A =0x3153622a0x65c2e9410x9a3270580xcea1f76f B =0x4372a49d0xb60c0e9c

默认取最小整数解,继续向下动静结合分析。


05

日志断点

关键日志断点,主要关注BN_mul与BEF MID AFT 3轮异或处理。


如下:
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析



06

大整数方程

搜索得到一个可用的在线求解网站:https://www.dcode.fr/modular-inverse。


x1求解:
a1 2865244763n1 13407712312341506807290785620513810006013432881349817380059896195876647865383040511615194818142561216049323742693301651262826523009904773019126031607880381917510353811872243158275 求解x使得 a1 * x % n1 = 1

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

x2求解:
a2 = 2207598431n2 = 11504884415388796500219489938877037570907236266221216736789242959943502559932358261444533399595441834388023078747736743001176901105009843107005500447845023069935116586106537423621求解x使得 a2 * x % n2 = 1

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析



07

Python Keygen

求解到大整数方程的解之后,通过python脚本处理好前期步骤。


代码如下,以下脚本python3可以直接运行,输出flag。

不过,编写过程需要与后面辅助异或的C代码交替配合。
from binascii import *from base64 import *import zlibfrom struct import *  # 在线网站 解出来的x1x1 = 6956654053800499230326290699174323596088081767147313465648406350971187122655244055152548005625866216241904992654364553316321828514212984633351921722043990261110066970542167578377  # 把在线网站求解出来的大整数 打印成C语言字节数组 然后复制到C代码中,做异或处理def do_x(x1):    print('='*32)    hx1 = hex(x1)    print(hx1)    x1bs = bytes(reversed(unhexlify(hx1.lstrip('0x'))))    print('x1 c arr', ', '.join('0x{:X}'.format(b) for b in x1bs))    print('x1 len', len(x1bs))    print('x1', hexlify(x1bs))    print('='*32)          do_x(x1)  # 在线网站 解出来的x2x2 = 6030658962721950361155533835702172940609579189992003760553378343560660612412076910216892955114510943618804706539422649399614037254978869753190882492376528406540173851407196912984do_x(x2)   # 由C代码处理了3轮异或之后的x1x1bef = "F550C4D11BE88545A17EDF49A600D30212A17760F814F054F252F5FAC9E3C915B1195D9F2D52F3BBD2CB5690982C85DFBCA0C4102132CFF25E4740F3C9C71174802C59A7FAF98DAEBC52" # 由C代码处理了3轮异或之后的x2x2bef = "54449BC8E9713A6F2DE69ACCEADB2BB55F550294D7F4D887D561C858C12D74AA218E45766D0799391A8C617F5E3FB00FD4995E9E5077100721858261A223FB4773736CE6027275E7A9EC" # 输入字节的hexh = ("""2a 62 53 31     """ + '{:X}'.format(74) + """00 00 00""" + x1bef + """9d a4 72 43     """ + '{:X}'.format(74)  + """ 00 00 00 """ + x2bef + """""")  print('h', h) h = h.replace('n', '').replace(' ', '').replace('_', '') bs = unhexlify(h)b64 = b64encode(bs+pack('<I', zlib.crc32(bs))) # 这个输出的就是flagprint('b64', b64, end='nn');  # 把日志断点输出大整数hex转成大整数def to_bn(s):    return int(hexlify(bytes(reversed(unhexlify(s)))), 16)          a=to_bn('5B2AC8AA000000000000000000000000')x=to_bn('CF43F9DBE60000000000000000000000')p=to_bn('95107386EB9395029A00000000000000') # 验证代码:确认一下那个函数是大整数乘法print('{:X}'.format(a))print('{:X}'.format(x))print('{:X}'.format(p))print(a*x == p)  p=to_bn('95107386EB9395029A00000000000000')   n=to_bn('038DB6BBBD8ADB73AC572C605199E6ECAE5698E1D456BA9F902BF6509F1D23918E28AFB9A4C506C77DAA288EBE4012337E761796CE3B482E832E2C79A4E3410DA3D18E58C0ABD6B8C1D3')m = p % nprint('{:X}'.format(m)) # 把第一个大整数方程的a跟n 打印成10进制整数,然后手工复制到网站上 求解print('a1', a)print('n1', n)    n=to_bn('05B34498B0EF11F7EB3B978CA2FB6D6A5917C6A1988B9C21D48906ECCF8DE77430833788B46C7A2E3D91505E25D18FEF12785BAF3DBAB8EC5F3DFC7B375B2D725CEC122C5957AF41B4B5')a=to_bn('5F479583000000000000000000000000')# 把第二个大整数方程的a跟n 打印成10进制整数,然后手工复制到网站上 求解print('a2', a)print('n2', n)

脚本输出flag为引号内base64字符串:
b64 b'KmJTMUoAAAD1UMTRG+iFRaF+30mmANMCEqF3YPgU8FTyUvX6yePJFbEZXZ8tUvO70stWkJgshd+8oMQQITLP8l5HQPPJxxF0gCxZp/r5ja68Up2kckNKAAAAVESbyOlxOm8t5prM6tsrtV9VApTX9NiH1WHIWMEtdKohjkV2bQeZORqMYX9eP7AP1JlenlB3EAchhYJhoiP7R3NzbOYCcnXnqewe7SYM'

修改程序命令行,进行验证,程序提示OK。

提交到网站,显示答对。


08

大整数求解之后的异或处理

由C代码辅助实现:

#include <stdlib.h>#include <stdio.h> int main(){    // true: 处理x1    false: 处理x2    bool x = false;     unsigned char xbs2[] = {        // x1的大整数字节数组 由python脚本打印出        //0x9, 0x57, 0x68, 0x6C, 0x8A, 0x1, 0xD9, 0x76, 0x34, 0x9B, 0x1C, 0x99, 0x58, 0x3E, 0xDC, 0x6C, 0x94, 0x8A, 0x41, 0xD2, 0xC2, 0x21, 0xA8, 0x48, 0x9A, 0x60, 0xFB, 0x3B, 0xAF, 0x96, 0x6C, 0x5A, 0x8A, 0x53, 0x74, 0xCB, 0x9C, 0xEB, 0x99, 0x61, 0x1C, 0x68, 0xE1, 0x3B, 0x8E, 0xB0, 0xA8, 0xAD, 0x8, 0x1A, 0x0, 0xBF, 0xFD, 0x16, 0xDE, 0xC3, 0xFF, 0x37, 0xF3, 0xD3, 0xA0, 0x61, 0x1C, 0x24, 0xBC, 0x76, 0x36, 0x49, 0x91, 0xE1, 0x90, 0xF7, 0xDE, 0x6D             // x2的大整数字节数组 由python脚本打印出        0x58, 0x7D, 0x5C, 0x2, 0x4B, 0x3A, 0x90, 0x1E, 0xE0, 0xD8, 0x20, 0x2F, 0xF5, 0x7, 0x2, 0x24, 0x62, 0x36, 0x83, 0x4B, 0xF9, 0x9B, 0x25, 0xA2, 0xBD, 0x85, 0xB5, 0xEA, 0xE7, 0x96, 0x7, 0xD3, 0x7E, 0xFB, 0x13, 0x94, 0x80, 0xC2, 0x14, 0xF0, 0x91, 0x20, 0x72, 0xD9, 0x9D, 0x32, 0x5A, 0x64, 0xC5, 0xEF, 0xC, 0xD, 0x27, 0x7C, 0x75, 0x59, 0x94, 0xA, 0xBE, 0xD, 0x3B, 0x72, 0xF2, 0x1B, 0x4B, 0x15, 0xC9, 0x41, 0x8A, 0x9B, 0xD8, 0x1, 0x3F, 0x5F    };    printf("nAFT ");    for (int i = 0; i < sizeof(xbs2); ++i)    {        printf("%02X", xbs2[i]);    }    printf("n    ");    if (x)    {        unsigned char xork2[16];        int ind2;        int xlen;         xlen = 0;        xork2[0] = 0x21;        xork2[1] = -124;        xork2[2] = 16;        xork2[3] = 66;        xork2[4] = 8;        xork2[5] = 33;        xork2[6] = -124;        xork2[7] = 16;        xork2[8] = 66;        xork2[9] = -56;        xork2[10] = 81;        xork2[11] = -121;        xork2[12] = -61;        xork2[13] = 0x80;        xork2[14] = -44;        xork2[15] = 61;        xlen = sizeof xbs2 / 2;        for (ind2 = 0; ind2 < xlen; ++ind2)        {            printf("%02X", xork2[ind2 % 0x10u]);             xbs2[ind2] ^= xork2[ind2 % 0x10u];        }    }    printf("n");     for (int i = 0; i < sizeof(xbs2); ++i)    {        printf("%02X", xbs2[i]);    }    printf("n");     printf("nMID ");    unsigned char xormid[] = {    // x1的xor key 由日志断点MID 输出后提取出字节    //0xFC , 0x7 , 0xAC , 0xBD , 0x91 , 0xE9 , 0x5C , 0x33 , 0x95 , 0xE5 , 0xC3 , 0xD0 , 0xFE , 0x3E , 0xF , 0x6E , 0x86 , 0x2B , 0x36 , 0xB2 , 0x3A , 0x35 , 0x58 , 0x1C , 0x68 , 0x32 , 0xE , 0xC1 , 0x66 , 0x75 , 0xA5 , 0x4F , 0x3B , 0x4A , 0x29 , 0x54 , 0xB1 , 0xB9 , 0x6A , 0xDA , 0xCE , 0xA3 , 0xB7 , 0xAB , 0x16 , 0x9C , 0x2D , 0x72 , 0xB4 , 0xBA , 0xC4 , 0xAF , 0xDC , 0x24 , 0x11 , 0x31 , 0xA1 , 0x70 , 0xB3 , 0x20 , 0x69 , 0xA6 , 0xD , 0x50 , 0x3C , 0x5A , 0x6F , 0xEE , 0x6B , 0x18 , 0x1D , 0x59 , 0x62 , 0x3F         // x2的xor key 由日志断点MID 输出后提取出字节     0xC , 0x39 , 0xC7 , 0xCA , 0xA2 , 0x4B , 0xAA , 0x71 , 0xCD , 0x3E , 0xBA , 0xE3 , 0x1F , 0xDC , 0x29 , 0x91 , 0x3D , 0x63 , 0x81 , 0xDF , 0x2E , 0x6F , 0xFD , 0x25 , 0x68 , 0xE4 , 0x7D , 0xB2 , 0x26 , 0xBB , 0x73 , 0x79 , 0x5F , 0x75 , 0x56 , 0xE2 , 0xED , 0xC5 , 0x8D , 0xC9 , 0x8B , 0xAC , 0x13 , 0xA6 , 0xC3 , 0xD , 0xEA , 0x6B , 0x11 , 0x76 , 0x52 , 0x93 , 0x77 , 0xB , 0x65 , 0x5E , 0xB5 , 0x8F , 0x3C , 0x6C , 0x99 , 0x51 , 0x9 , 0x5C , 0x38 , 0x66 , 0xA5 , 0xA7 , 0x88 , 0xE9 , 0xAD , 0xE6 , 0x96 , 0xB3    };    for (int i = 0; i < sizeof(xbs2); ++i)    {        xbs2[i] ^= xormid[i];        printf("%02X", xormid[i]);    }     printf("n    ");     for (int i = 0; i < sizeof(xbs2); ++i)    {        printf("%02X", xbs2[i]);    }    printf("n");     if (x)    {        printf("nnBEF ");        int ixlen1;        unsigned char xork1[16];        int ind1;         ixlen1 = 0;        xork1[0] = 33;        xork1[1] = -124;        xork1[2] = 16;        xork1[3] = 66;        xork1[4] = 8;        xork1[5] = 33;        xork1[6] = -124;        xork1[7] = 16;        xork1[8] = 66;        xork1[9] = -56;        xork1[10] = 81;        xork1[11] = -121;        xork1[12] = -61;        xork1[13] = 0x80;        xork1[14] = -44;        xork1[15] = 61;        ixlen1 = sizeof(xbs2) / 2;        for (ind1 = 0; ind1 < ixlen1; ++ind1)        {            printf("%02X", xork1[ind1 % 0x10u]);             xbs2[ind1] ^= xork1[ind1 % 0x10u];        }        printf("n    ");        for (int i = 0; i < sizeof(xbs2); ++i)        {            printf("%02X", xbs2[i]);        }    }    return 0;}



09

多解可能

  1. 小整数方程可能多解。
  2. 取大整数长度的时候,取得是2字节整数,2字节长度后面的2个字节没有判断,使得flag里面有4个字节没有被判断。
    随便填什么值似乎都会提示OK。
    不过,提交flag的时候,很懂事的默认按0字节填了。


题目作者非常仁慈,用了一对小整数方程来引导做题。终于不至于“打算来完完题,结果被题玩了”。


看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析
今天中午12:00
第六题《至暗时刻》已开赛!

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析
看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

截至发文,已有2支战队成功提交了flag,第一支是“98k战队”,第二支是“辣鸡战队”。

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析


在这个充满变数的赛场上,没有人能够预料到最终的结局。有时,优势的领先可能只是一时的,一瞬间的失误就足以颠覆一切。而那些一直默默努力、不断突破自我的人,往往会在最后关头迎头赶上,成为最耀眼的存在。

谁能保持领先优势?谁能迎头赶上?谁又能突出重围成为黑马?

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

球分享

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

球点赞

看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

球在看


看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

点击阅读原文进入比赛

原文始发于微信公众号(看雪学苑):看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析

版权声明:admin 发表于 2023年9月13日 下午6:00。
转载请注明:看雪2023 KCTF年度赛 | 第五题·争分夺秒-设计思路及解析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...