这是一场人类与超智能AI的“生死”较量
请立刻集结,搭乘SpaceX,前往AI控制空间站
智慧博弈 谁能问鼎
看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。
*注意:签到题持续开放,整个比赛期间均可提交答案获得积分
今天中午12:00第六题《至暗时刻》已截止答题,该题共有11支战队成功提交flag,一起来看下该题的设计思路和解析吧。
出题团队简介
出题战队:天外星系
战队成员:geekfire
设计思路
战队名称:天外星系
战队创建者: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 在转换数据时如果遇到小写会返回错误的值
赛题解析
过反调试
程序分析
v3 = operator new(8ui64);
*v3 = sub_7FF6D6431630;
*ThrdAddr = beginthreadex(0i64, 0, StartAddress, v3, 0, &ThrdAddr[2]);
初始化
字符串拼接
# 头部拼接上这串字符串:
3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6
# input尾部拼接上这串字符串:
677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280
# 然后在最头部再次拼接字符串
kctf
shellcode填充
ModuleHandleA = GetModuleHandleA(v22);
off_7FF61D229A48 = GetProcAddress(ModuleHandleA, v21);
if ( !off_7FF61D229A48 || (v31 = Source, sub_7FF61D221450(CurrentProcess, (Source + 500))) ){...}
可以看出来他创建了句柄,然后一个大的for循环将从0x7FF61D228050开始长度为0x92B的内存拿去调用了kernel32_RtlFillMemory函数,这个函数的作用是使用指定数据填充内存块,填充的目的地址在r8寄存器存着,是0x261CD0301F4。
shellcode分析
第一个是将0x7FF61D228050位置的shellcode给dump下来,然后扔到ida中静态分析,我尝试了一下,虽然代码量不大,但是还是有点复杂的,而且需要自己修复堆栈,比较麻烦。
第二种做法就是调试上述创建的那个线程了:
在sub_7FF61D222A8E执行系统调用之前,线程已经准备好了但是没有执行,这个时候我们可以在ida的线程模块中双击多创建的线程,就可以进入到该线程的领空。
初始化
__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函数
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;
}
其中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;
}
1.我们输入的字符串拼接上上文的两个数字之后要等于243+120。
2.我们输入的字符串三个为一组,组成的数字的个位十位百位要满足:十位和个位要与传进来的第二和第三个参数相等,返回值是百位的值,并且返回值不能重复。
00 01 02
10 11 12
20 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
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
脚本
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年度赛 | 第六题·至暗时刻-设计思路及解析