看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

WriteUp 9个月前 admin
117 0 0

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

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

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

智慧博弈  谁能问鼎


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

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


今天中午12:00第六题《至暗时刻》已截止答题,该题共有11支战队成功提交flag,一起来看下该题的设计思路和解析吧。



出题团队简介


出题战队:天外星系

战队成员:geekfire

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析


设计思路


战队名称:天外星系
战队创建者:geekfire
题目名称:blackclient
输出提示:key正确则输出提示ok!


题目设计说明


算法模型


有一个数独矩阵:


{8, -1, -1, -1, -1, -1, -1, -1, -1},
{-1, -1, 3, 6, -1, -1, -1, -1, -1},
{-1, 7, -1, -1, 9, -1, 2, -1, -1},
{-1, 5, -1, -1, -1, 7, -1, -1, -1},
{-1, -1, -1, -1, 4, 5, 7, -1, -1},
{-1, -1, -1, 1, -1, -1, -1, 3, -1},
{-1, -1, 1, -1, -1, -1, -1, 6, 8},
{-1, -1, 8, 5, -1, -1, -1, 1, -1},
{-1, 9, -1, -1, -1, -1, 4, -1, -1}


它第一个元素值和坐标为 8 0,0 把他们连一起为800 转为16进制为320
这样把数独矩阵里面所有已知数都转为16进制得到:


3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6


这个作为已知数据。


数独矩阵求解和其余需要填充的数据同样转为16进制如下:


11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120


这部分作为未知数据,需要在输入key的时候输入。


算法保护


1、整个数独的验证算法包含在一个shellcode里面

shellcode 通过插入APC异步队列方式执行
求解需要对shellcode进行分析才可以发现数独验证算法


2、在TLS 和 注入shellcode过程中有反调试检测,如果检测到调试行为则会修改初始数据,后面的验证逻辑会进行不下去


3、程序中大多字符串都进行了编码隐藏,另外大部分系统API都采用了native api的方式进行了隐藏,以便尽量减少调试信息。


4、对输入的元素做了顺序限定


{8, 1, 2, 7, 5, 3, 6, 4, 9},
{9, 4, 3, 6, 8, 2, 1, 7, 5},
{6, 7, 5, 4, 9, 1, 2, 8, 3},
{1, 5, 4, 2, 3, 7, 8, 9, 6},
{3, 6, 9, 8, 4, 5, 7, 2, 1},
{2, 8, 7, 1, 6, 9, 5, 3, 4},
{5, 2, 1, 9, 7, 4, 3, 6, 8},
{4, 3, 8, 5, 2, 6, 9, 1, 7},
{7, 9, 6, 3, 1, 8, 4, 5, 2}


如果按照从左到右 从上到下填充那么填入的数据对应的16进制为:


0650CA2BF1F813125E19738C38E19B32E0D70742CD20626C20A1A707D33B1480821B00E914E3443A927E1542813A63430F70940FA3532F028E3BB22C1CA2301053C32FC1D116E1D61731122A33D030A30C2AA17F0B837524B120


这里065 对应 1 0,1->101。


这里从左到右 从上到下 把需要填充的数字加一个序号 比如 矩阵里面第一个元素 8 序号为00 矩阵中第二个元素1 的数据为01。


然后把要填入的数据的序号乱序如下:


677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280


其中两个字符表示一个序号 作为10进制数据,
那么输入的key就要重新排列为:


11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120


验证正确后就会提示 ok!


5、shellcode 在转换数据时如果遇到小写会返回错误的值



赛题解析


本题解析由看雪大牛 Zero*/ 提供:
看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

windows10 或者 windows7 的运行环境,ida打开发现程序逻辑比较少(以下结果基于ida7.7)。

01

过反调试


由于函数较少,扫了一眼看到了两个TLS回调函数,发现是反调试,逐一过掉(其实后续代码里还有个反调试,后文会提到)。

1.IsDebuggerPresent,把跳转patch掉就行了,改为jmp,不管结果如何都不进入if语句。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

2.NtQueryInformationProcess,0x7功能码查调试器信息(这个函数有好几个功能码都可以查调试器),一样的方法,if那里的跳转改为jmp就不会进入if语句了。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

02

程序分析


main函数比较简单,就启动了一个线程,主要来看线程里面的逻辑sub_7FF6D6431630;

v3 = operator new(8ui64);
*v3 = sub_7FF6D6431630;
*ThrdAddr = beginthreadex(0i64, 0, StartAddress, v3, 0, &ThrdAddr[2]);


初始化


将一些内置字符串拷贝到变量中,后续用到的时候会将字符串使用异或来解密,就能看到明文,然后打印提示语句,分配空间接受输入的值。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析


字符串拼接


将我们的输入,头尾都拼接上一些字符串:

# 头部拼接上这串字符串:
3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6
# input尾部拼接上这串字符串:
677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280
# 然后在最头部再次拼接字符串
kctf
看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析


shellcode填充


后续执行的都是一些上文中的字符串异或解密操作,然后就会来到关键函数。
  
ModuleHandleA = GetModuleHandleA(v22);
off_7FF61D229A48 = GetProcAddress(ModuleHandleA, v21);
if ( !off_7FF61D229A48 || (v31 = Source, sub_7FF61D221450(CurrentProcess, (Source + 500))) ){...}

我们进入sub_7FF61D221450函数会发现里面还有个反调试CheckRemoteDebuggerPresent,一样的方法,跳转的位置改为jmp即可。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

接下来我们可以看到主要逻辑是三个函数的调用(sub_7FF61D223529,sub_7FF61D222E4E,sub_7FF61D222A8E),这三个函数都很类似,大概都是长下面这样的:

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

说明核心逻辑在sub_7FF61D222A10函数里面,这个函数跟进去之后会发现其实就是函数指针处理+syscall系统调用的过程,我们在syscall的地方下断点,根据syscall函数原型查看寄存器就可以看到他的参数是什么了。

可以看出来他创建了句柄,然后一个大的for循环将从0x7FF61D228050开始长度为0x92B的内存拿去调用了kernel32_RtlFillMemory函数,这个函数的作用是使用指定数据填充内存块,填充的目的地址在r8寄存器存着,是0x261CD0301F4。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

然后我们继续往后执行,来到一个创建线程的位置,这些函数都是通过上述系统调用的形式执行的,这里创建了新线程,如果继续往后执行他就会比较字符串是110还是120,然后打印换行符,程序结束。这里可以推测这个线程就是执行刚才填充的那段内存的地方。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析


shellcode分析


据上述推测新起来的线程就是执行shellcode的地方,并且返回结果应该是110和120有关的值,这里有两种做法:

第一个是将0x7FF61D228050位置的shellcode给dump下来,然后扔到ida中静态分析,我尝试了一下,虽然代码量不大,但是还是有点复杂的,而且需要自己修复堆栈,比较麻烦。

第二种做法就是调试上述创建的那个线程了:

在sub_7FF61D222A8E执行系统调用之前,线程已经准备好了但是没有执行,这个时候我们可以在ida的线程模块中双击多创建的线程,就可以进入到该线程的领空。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

他停在这里等我们执行,我们直接按g跳转到0x261CD0301F4(这是上述分析中shellcode被填充的地址),可以确认与0x7FF61D228050的内容是一致的,然后我们使用ida快捷键P创建函数,在函数里下个断点,直接f9执行,执行到断点处会发现堆栈平衡了,然后就可以f5大法了。

初始化


通过一个sub_261CD030563函数找到了一系列的函数指针(名字是我重命名的),调试之后发现v17和v19就是我们输入的字符串被拼接之后的结果,从上述字符串判断中可以得出,我们需要程序返回110,说明unk_261CD03062F和unk_261CD030AA3要返回true:

__int64 __fastcall sub_261CD03020A(__int64 a1)
{
do
{
v2 = *v1;
if ( *v1 == 23 )
v2 = 0;
*v1++ = v2;
--a1;
}
while ( a1 );
v26 = 0i64;
v25 = v1;
kernel32_CreateToolhelp32Snapshot = sub_261CD030563(-124919994);
kernel32_OpenProcess = sub_261CD030563(-49588825);
kernel32_VirtualQueryEx = sub_261CD030563(37938943);
kernel32_Process32First = sub_261CD030563(1060402837);
kernel32_Process32Next = sub_261CD030563(-1813961927);
kernel32_CloseHandle = sub_261CD030563(480663025);
kernel32_GetCurrentProcessId = sub_261CD030563(55981281);
v7 = 0i64;
v23 = 568;
v8 = 0;
v9 = kernel32_CreateToolhelp32Snapshot(2i64, 0i64);
v10 = v9;
if ( v9 == -1 )
return 0xFFFFFFFFi64;
v12 = kernel32_Process32First(v9, &v23);
v13 = kernel32_Process32Next;
v14 = kernel32_OpenProcess;
while ( v12 )
{
if ( v24 == kernel32_GetCurrentProcessId() )
{
v7 = v14(0x2000000i64, 0i64);
if ( v7 )
{
v15 = 0i64;
while ( 1 )
{
do
{
if ( !kernel32_VirtualQueryEx(v7, v15, &v19, 48i64) )
{
v13 = kernel32_Process32Next;
v14 = kernel32_OpenProcess;
goto LABEL_23;
}
v15 = v19 + v21;
}
while ( v22 != 4096 || v20 != 64 );
v16 = kernel32_GetCurrentProcessId();
v17 = v19;
if ( v24 == v16 )
v8 = (unk_261CD03062F)(*v19);
if ( v8 )
break;
*v17 = 'm';
v17[1] = 'j';
v17[2] = ')';
v17[3] = '';
v17[67] = '1';
v17[68] = '2';
v17[69] = '0';
}
v18 = v17 + 4;
if ( (unk_261CD030AA3)(v17 + 4) )
{
*(v18 - 4) = 'i';
*(v18 - 3) = 'o';
*(v18 - 2) = ' ';
*(v18 - 1) = 0;
v18[63] = '1';
v18[64] = '1';
}
else
{
*(v18 - 4) = 'm';
*(v18 - 3) = 'j';
*(v18 - 2) = ')';
*(v18 - 1) = '';
v18[63] = '1';
v18[64] = '2';
}
v18[65] = '0';
break;
}
}
LABEL_23:
v12 = v13(v10, &v23);
}
kernel32_CloseHandle(v10);
return (kernel32_CloseHandle)(v7);
}


unk_261CD03062F函数


这个函数的逻辑很简单,就是判断开头是不是kctf,而kctf是程序帮我们拼接的,所以恒成立。

bool __fastcall sub_261CD03062F(int a1)
{
return a1 == 'ftck';
}


unk_261CD030AA3函数


char __fastcall sub_261CD030AA3(__int64 a1)
{
unsigned int v2; // ebx
int v3; // ebx
int v4; // edi

v2 = 0;
while ( (unk_261CD03093B)(a1, v2) && (unk_261CD0309A7)(a1, v2) )
{
if ( ++v2 >= 9 )
{
v3 = 0;
LABEL_6:
v4 = 0;
while ( (unk_261CD030A13)(a1, v3, v4) )
{
v4 += 3;
if ( v4 >= 9 )
{
v3 += 3;
if ( v3 < 9 )
goto LABEL_6;
return 1;
}
}
return 0;
}
}
return 0;
}

这段主要调用了三个函数,每个函数需要返回true才行。

其中unk_261CD03093B的代码如下:

char __fastcall sub_261CD03093B(__int64 a1, unsigned int a2)
{
int v2; // ebx
signed int v5; // eax
__int128 v7[2]; // [rsp+20h] [rbp-38h] BYREF
int v8; // [rsp+40h] [rbp-18h]

memset(v7, 0, sizeof(v7));
v8 = 0;
v2 = 0;
while ( 1 )
{
v5 = (unk_261CD03063B)(a1, a2, v2) - 1;
if ( v5 > 8 || *(v7 + v5) )
break;
++v2;
*(v7 + v5) = 1;
if ( v2 >= 9 )
return 1;
}
return 0;
}

他会遍历v2的0-8然后要求unk_261CD03063B函数的返回值,并且不能重复。

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

结合上图和之前的函数可以得出如下结论:

1.我们输入的字符串拼接上上文的两个数字之后要等于243+120。

2.我们输入的字符串三个为一组,组成的数字的个位十位百位要满足:十位和个位要与传进来的第二和第三个参数相等,返回值是百位的值,并且返回值不能重复。


我们再返回去分析那个大的while循环(即sub_261CD030AA3),他调用的三个函数实际上都和我们分析过的sub_261CD03093B函数类似,通过unk_261CD03063B函数来控制输入的个位十位百位的值。

那么我们结合while循环和函数本身来分析这三个函数的参数会发现如下规律:

1.第一个函数(unk_261CD03093B)的接受while循环中的v2作为十位数传给子函数,对于每个个位都要求返回值不能重复。

2.第二个函数(unk_261CD0309A7)的接受while循环中的v2作为个位数传给子函数,对于每个十位都要求返回值不能重复。

3.第一个函数(unk_261CD030A13)的接受v3和v4,他要求十位和个位每三个数为一组,组成的的结果传递给子函数,要求返回值不能重复,可以罗列一下第三个函数的值如下,要求类似如下九个数的返回值不能重复(v3,v4会+3):
00 01 0210 11 1220 21 22

我们将这些规则整合一起看就像是一个数独游戏,十位和个位是坐标,百位是数独里的值。

00, 01, 02, | 03, 04, 05, | 06, 07, 08,
10, 11, 12, | 13, 14, 15, | 16, 17, 18,
20, 21, 22, | 23, 24, 25, | 26, 27, 28,
----------------------------------------
30, 31, 32, | 33, 34, 35, | 36, 37, 38,
40, 41, 42, | 43, 44, 45, | 46, 47, 48,
50, 51, 52, | 53, 54, 55, | 56, 57, 58,
----------------------------------------
60, 61, 62, | 63, 64, 65, | 66, 67, 68,
70, 71, 72, | 73, 74, 75, | 76, 77, 78,
80, 81, 82, | 83, 84, 85, | 86, 87, 88,

第一条规则对于每个十位要求返回值不能重复即每一行不能有重复的值。
第二条规则对于每个个位要求返回值不能重复即每一列不能有重复的值。
第三条规则对应的就是每一宫不能有重复的值。

回顾我们的输入会和他提供的一串头部字符串拼接,那串字符串同样满足这些条件,那我们就将他的字符串转换为数字,按照十位和个位是横纵坐标,百位是值的规律,填充到数独列表中就是个完整的数独游戏了。

字符串如下:3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6
找一个在线链接填充一下即可,也可以使用chatgpt:

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

我们得到了数独的数,也就是程序中的百位,我们将其和坐标放在一起组成一个完整的三位数。

800, 101, 202, 703, 504, 305, 606, 407, 908,
910, 411, 312, 613, 814, 215, 116, 717, 518,
620, 721, 522, 423, 924, 125, 226, 827, 328,
130, 531, 432, 233, 334, 735, 836, 937, 638,
340, 641, 942, 843, 444, 545, 746, 247, 148,
250, 851, 752, 153, 654, 955, 556, 357, 458,
560, 261, 162, 963, 764, 465, 366, 667, 868,
470, 371, 872, 573, 274, 675, 976, 177, 778,
780, 981, 682, 383, 184, 885, 486, 587, 288

那么最后一个问题就是顺序怎么办,前20个整数的顺序程序已经给我们了,那后60个呢,其实就是子函数(unk_261CD03063B)中判断的逻辑,对于长度大于21(v7>21)的数字,要和程序拼接的后一段字符串进行比较,比较的规则是上图代码中的if ( v24 + v22 + 8 * v24 == v32 * v27 ),所以后60个每一个数(个位*9+十位 == v32),我们写个脚本逆推一下即可。

03

脚本


def generate_hex_string(know):
string = ''
for x in know:
string += ''.join(['{:03x}'.format(x).upper()])

return string

k_l = []


a = [800, 101, 202, 703, 504, 305, 606, 407, 908, 910, 411, 312, 613, 814, 215, 116, 717, 518, 620, 721, 522, 423, 924, 125, 226, 827, 328, 130, 531, 432, 233, 334, 735, 836, 937, 638, 340, 641, 942, 843, 444, 545, 746, 247, 148, 250, 851, 752, 153, 654, 955, 556, 357, 458, 560, 261, 162, 963, 764, 465, 366, 667, 868, 470, 371, 872, 573, 274, 675, 976, 177, 778, 780, 981, 682, 383, 184, 885, 486, 587, 288]

ch = "677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280"
ch_list = [int(ch[x:x+2]) for x in range(0, len(ch), 2)]
two = []
for i in ch_list:
for j in a:
if j % 100 == (i//9)*10 + (i % 9):
two.append(j)

one = []
for x in a:
if x not in two:
one.append(x)
k_l = one + two

print(len(k_l))
print(k_l)

hex_string = generate_hex_string(k_l)
print(len(hex_string))
print(hex_string)

# 3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E611230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120
# 提交的时候只需要提交后半段即可,因为前半段是程序帮我们添加的:11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析


看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析
今天中午12:00
第七题《智能联盟计划》已开赛!

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析
看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

截至发文,本题还无战队攻破:

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

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

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

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

球分享

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

球点赞

看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

球在看


看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

点击阅读原文进入比赛

原文始发于微信公众号(看雪学苑):看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析

版权声明:admin 发表于 2023年9月15日 下午6:00。
转载请注明:看雪2023 KCTF年度赛 | 第六题·至暗时刻-设计思路及解析 | CTF导航

相关文章

暂无评论

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