第一次完整地参加这样的找flag活动。有些题连蒙带猜,有些题压根没做出来,但也有几题很有把握地答上了。全程凭感觉手工分析,ai只帮忙给了些提示。请大佬多多指教
第一题 送分题
不知道有没有不用关注公众号的办法
第二题 Windows 初级题
用ida静态分析,从start函数开始。函数开头出现了memset、strlen和memcpy等函数调用,可以推测这部分代码是用于命令行参数的处理。而最终用于返回的变量result来自sub_1CD130这个函数的返回值,这类似于c语言main函数的返回值,因此可以确定主函数就是sub_1CD130。
// write access to const memory has been detected, the output may be wrong!
int __usercall start@<eax>(int a1@<ebx>, int a2@<ebp>, int a3@<edi>, int a4@<esi>, char a5)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
dword_1DF054 = 0;
v32 = retaddr;
v31 = a2;
v30 = a3;
v29 = a4;
v28 = a1;
v27 = &a5;
memset(v26, 0, sizeof(v26));
StackBase = NtCurrentTeb()->NtTib.StackBase;
while ( 1 )
{
v6 = _InterlockedCompareExchange(&dword_1DF030, (signed __int32)StackBase, 0);
if ( !v6 )
{
v7 = 0;
if ( dword_1DF034 == 1 )
goto LABEL_38;
goto LABEL_6;
}
if ( StackBase == (PVOID)v6 )
break;
Sleep(0x3E8u);
}
v7 = 1;
if ( dword_1DF034 == 1 )
{
LABEL_38:
amsg_exit(31);
if ( dword_1DF034 == 1 )
goto LABEL_39;
LABEL_9:
if ( v7 )
goto LABEL_10;
goto LABEL_40;
}
LABEL_6:
if ( dword_1DF034 )
{
dword_1DF010 = 1;
}
else
{
dword_1DF034 = 1;
initterm(&dword_1E100C, &dword_1E1018);
}
if ( dword_1DF034 != 1 )
goto LABEL_9;
LABEL_39:
initterm(&First, &Last);
dword_1DF034 = 2;
if ( v7 )
goto LABEL_10;
LABEL_40:
_InterlockedExchange(&dword_1DF030, v7);
LABEL_10:
if ( TlsCallback_0 )
TlsCallback_0(0, 2, 0);
sub_10A9E0();
dword_1DF06C = (int)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
sub_121680(nullsub_1);
sub_10A810();
dword_1DF008 = 0x100000;
v8 = _p__acmdln();
v9 = 0;
v10 = *v8;
if ( !v10 )
goto LABEL_25;
while ( 1 )
{
v11 = *v10;
if ( *v10 <= 32 )
break;
if ( v11 == 34 )
v9 ^= 1u;
LABEL_17:
++v10;
}
if ( !v11 )
goto LABEL_24;
if ( (v9 & 1) != 0 )
{
v9 = 1;
goto LABEL_17;
}
do
v12 = *++v10;
while ( v12 && v12 <= 32 );
LABEL_24:
dword_1DF004 = (int)v10;
LABEL_25:
if ( dword_1DF054 )
{
v13 = 10;
if ( (v26[22] & 1) != 0 )
v13 = v26[24];
dword_1D1000 = v13;
}
v14 = dword_1DF024;
v15 = 4 * dword_1DF024 + 4;
v16 = malloc(v15);
v24 = (int)v16;
if ( v14 > 0 )
{
v17 = v16;
v18 = (const char **)dword_1DF020;
v23 = v15 - 4;
v25 = dword_1DF020 + v15 - 4;
do
{
v19 = *v18;
++v17;
++v18;
Size = strlen(v19) + 1;
v21 = malloc(Size);
*(v17 - 1) = v21;
memcpy(v21, *(v18 - 1), Size);
}
while ( (const char **)v25 != v18 );
v16 = (_DWORD *)(v24 + v23);
} //以上代码用于命令行参数处理等初始化工作
*v16 = 0;
dword_1DF020 = v24;
sub_10A650();
_initenv = dword_1DF01C;
result = sub_1CD130(dword_1DF024); //主函数
dword_1DF018 = result;
if ( !dword_1DF014 )
exit(result);
if ( !dword_1DF010 )
{
cexit();
return dword_1DF018;
}
return result;
}
再看sub_1CD130。这个函数首先出现了大量提示文字字符串,说明这里确实包含了主函数的逻辑。然后查看Enter the password:字符串下方的代码。函数sub_1C5840的第二个参数传的是指针,容易联想到c语言中的scanf函数。可以断定inputted存储的是输入的文本。
int __cdecl sub_1CD130(char a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v25 = &a1;
v17 = sub_1CC800;
p_Time = &Time;
v18 = dword_1CE3D4;
v19 = &savedregs;
v20 = sub_1CD4EA;
sub_10C330(v15);
sub_10A650();
v16 = -1;
v1 = time(0);
srand(v1);
SetConsoleOutputCP(0xFDE9u);
sub_1C7E50(&dword_1D27C0, "========================================");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, " CrackMe Challenge v2.5 - 2026 ");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "========================================");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "Keywords: 52pojie, 2026, Happy new year");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "Hint: Fake flag; length is key");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "----------------------------------------");
sub_1015B0(Time);
inputted = &v24;
v23 = 0;
v24 = 0;
v16 = 1;
v13 = &inputted;
sub_1C7E50(&dword_1D27C0, "\n[?] Enter the password: ");
v13 = &inputted;
v2 = ((int (*)(void))sub_101560)();
v13 = &inputted;
sub_1C5840(&dword_1D25E0, (int)&inputted, (void *)v2);
…………
接下来是验证flag的部分。这段代码进行了多次验证:
-
调用sub_101740进行验证,验证成功后只会提示You're getting closer...,说明这是假验证
-
通过对比byte_1D3032的值进行验证,验证成功后只会提示Nice try, but not quite right...,说明这是假验证
-
flag长度验证,观察提示文字The length is your first real challenge.可推测if判断中调用的函数(原名sub_120EB8)是strlen,31为正确flag的长度
-
调用doVerify(原名sub_1016D0)验证,成功后提示文字最多,所以是真验证
-
Str += ++v9 * v8;计算校验值进行验证
int __cdecl sub_1CD130(char a1)
…………
Str = inputted;
v3 = sub_101740((int)inputted); //第一次flag验证
v4 = '5';
v5 = v3;
v6 = 0;
if ( !v5 ) //第一次flag验证结果
{
while ( Str[v6] == v4 )
{
if ( ++v6 == 16 ) //第二次flag验证提示
{
v16 = 1;
sub_1C7E50(&dword_1D27C0, "\n[!] You're getting closer...");// 52pojie2026Happy
v16 = 1;
goto exit_1;
}
v4 = byte_1D3032[v6]; //第二次flag验证
}
if ( strlen(Str) != 31 ) //flag长度验证
{
v16 = 1;
sub_1C7E50(&dword_1D27C0, "\n[!] Hint: The length is your first real challenge.");
goto exit_1;
}
v16 = 1;
if ( doVerify((int)Str, 31) ) //第三次flag验证
{
Str = 0;
v8 = *inputted;
if ( !*inputted )
goto chksum_failed;
v9 = 0;
do
{
Str += ++v9 * v8;
v8 = inputted[v9];
}
while ( v8 );
if ( Str != (char *)44709 ) //第四次flag验证
{
chksum_failed:
v16 = 1;
sub_1C7E50(&dword_1D27C0, "\n[!] Checksum failed! Something is wrong...");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "[!] Expected: 44709, Got: ");
sub_17DE70(Str);
sub_1015B0(v10);
exit:
v16 = 1;
goto exit_0;
}
v16 = 1; //flag验证成功提示
sub_1C7E50(&dword_1D27C0, "\n========================================");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, " *** SUCCESS! *** ");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "========================================");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "[+] Congratulations! You cracked it!");
sub_1015B0(Time);
v11 = sub_1C7E50(&dword_1D27C0, "[+] Correct flag: ");
sub_1C4330(v11, inputted, v23);
}
else
{
v16 = 1;
sub_1C7E50(&dword_1D27C0, "\n[X] Access Denied!");
sub_1015B0(Time);
sub_1C7E50(&dword_1D27C0, "[X] Wrong password. Keep trying!");
}
sub_1015B0(Time);
goto exit;
} //第一次flag验证成功提示
sub_1C7E50(&dword_1D27C0, "\n[!] Nice try, but not quite right...");// pojie_2026_HappyNewYearXXXX
exit_1:
sub_1015B0(Time);
exit_0:
sub_1017C0();
sub_1B1AE0(v13);
sub_10C600(v15);
return 0;
}
所以,正确的flag应该藏在doVerify中。分析这个函数的逻辑,先是使用malloc分配内存,再调用fillBlock获取对比文本,也就是正确的flag。然后将对比文本与输入字符串inputtedStr比较,统计正确字符的个数,当所有字符均与flag时返回true。
fillBlock填充了一系列加密后的数值,再对数值进行了异或计算。
bool __cdecl doVerify(char *inputtedStr, int valueIs31)
{
unsigned __int8 *Block; // ebp
int index; // eax
int countOfCorrectChars; // ebx
bool isCurrentCharCorrect; // dl
Block = (unsigned __int8 *)malloc_wrapped(0x64u);
fillBlock((int)Block);
if ( valueIs31 <= 0 )
{
countOfCorrectChars = 0;
}
else
{
index = 0;
countOfCorrectChars = 0;
do
{
isCurrentCharCorrect = inputtedStr[index] == Block[index];
++index;
countOfCorrectChars += isCurrentCharCorrect;
}
while ( valueIs31 != index );
}
j_j_free(Block);
return valueIs31 == countOfCorrectChars;
}
char *__cdecl fillBlock(int *memblk)
{
char *result; // eax
*memblk = 0x2D327077;
memblk[1] = 0x63272B28;
memblk[2] = 0x701D6363;
memblk[3] = 0x1D747072;
memblk[4] = 0x3232230A;
memblk[5] = 0x272C1D3B;
memblk[6] = 0x273B1D35;
*((_BYTE *)memblk + 30) = 0x63;
*((_WORD *)memblk + 14) = 0x3023;
result = (char *)memblk;
do
*result++ ^= 0x42u; // xor byte by byte
while ( result != (char *)memblk + 31 );
*((_BYTE *)memblk + 31) = 0;
return result;
}
只需执行一遍fillBlock函数就能得到正确的flag,我将这个函数改写为了python形式:
import array
memblk=array.array("L",bytes(32))
memblk[0] = 0x2D327077;
memblk[1] = 0x63272B28;
memblk[2] = 0x701D6363;
memblk[3] = 0x1D747072;
memblk[4] = 0x3232230A;
memblk[5] = 0x272C1D3B;
memblk[6] = 0x273B1D35;
memblk=array.array("B",memblk.tobytes())
memblk[30] = 0x63;
memblk=array.array("H",memblk.tobytes())
memblk[14] = 0x3023;
memblk=array.array("B",memblk.tobytes())
for i in range(len(memblk)-1):memblk[i]^=0x42
print(memblk.tobytes())
执行后输出b'52pojie!!!_2026_Happy_new_year!\x00',其中\x00是c字符串的尾部,去掉就是flag了
第三题 Android 初级题
这道题我是靠游戏通关得到flag的。apk拖进jeb分析了半天没找到思路。。。
第四题 PyInstaller
通过观察exe的图标,可看出这是通过PyInstaller进行的打包。
找到pyinstxtractor工具进行解包,得到运行组件库和pyc文件。再找到了一个在线还原pyc文件的网站
得到源代码:
import hashlib
import base64
import sys
def xor_decrypt(data,key):
result = bytearray()
for i,byte in enumerate(data):
result.append(byte^key^i&255)
return result.decode('utf-8',errors='ignore')
def get_encrypted_flag():
enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
return base64.b64decode(enc_data)
def generate_flag():
encrypted = get_encrypted_flag()
key = 78
result = bytearray()
for i,byte in enumerate(encrypted):
result.append(byte^key)
return result.decode('utf-8')
def calculate_checksum(s):
total = 0
for i,c in enumerate(s):
total += ord(c)*i+1
return total
def hash_string(s):
return hashlib.sha256(s.encode()).hexdigest()
def verify_flag(user_input):
correct_flag = generate_flag()
if len(user_input) != len(correct_flag):
return False
else:
for i in range(len(correct_flag)):
if user_input[i] != correct_flag[i]:
continue
return False
return True
def fake_check_1(user_input):
fake_hash = 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890'
return hash_string(user_input) == fake_hash
def fake_check_2(user_input):
fake_hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
return hash_string(user_input) == fake_hash
def main():
print('==================================================')
print(' CrackMe Challenge - Python Edition')
print('==================================================')
print('Keywords: 52pojie, 2026, Happy New Year')
print('Hint: Decompile me if you can!')
print('--------------------------------------------------')
user_input = input('\n[?] Enter the password: ').strip()
if fake_check_1(user_input):
print('\n[!] Nice try, but not quite right...')
input('\nPress Enter to exit...')
return None
else:
if fake_check_2(user_input):
print('\n[!] You\'re getting closer...')
input('\nPress Enter to exit...')
return None
else:
if verify_flag(user_input):
checksum = calculate_checksum(user_input)
expected_checksum = calculate_checksum(generate_flag())
if checksum == expected_checksum:
print('\n==================================================')
print(' *** SUCCESS! ***')
print('==================================================')
print('[+] Congratulations! You cracked it!')
print(f'''[+] Correct flag: {user_input}''')
else:
print('\n[!] Checksum failed!')
else:
print('\n[X] Access Denied!')
print('[X] Wrong password. Keep trying!')
input('\nPress Enter to exit...')
return None
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('''
[!] Interrupted by user''')
sys.exit(0)
将generate_flag执行一次就能得到flag了
第五题 Nuitka
这个题我是靠动态调试和猜测破开的。。
首先用nuitka-extractor解包,得到的内容中crackme_hard.dll很明显是需要分析的dll。ida解析后,唯一一个导出函数是run_code。大概翻下来一看,这个函数先进行了初始化,加载了.bytecode、python对象和其他东西,然后在这个函数体即将结束的部分出现了一个函数指针的调用
ai告诉我,nuitka打包的程序“编译后的函数对象是本地(native)编译函数,code object 的 co_code 为空且没有字节码”,并且“无法对已编译的函数进行字节码级别的调试/追踪”。因此我认为只是分析初始化部分的逻辑可能不太行,所以在函数指针部分附近下断点开始动态调用。
配置调试器时发现crackme_hard无法直接启动,找了一个启动器,然后像这样配置
开始运行
没断下来,但执行到了下方的sub_7FFC540D6110,这个函数内容很简单
_DWORD *__fastcall sub_7FFC540D6110(__int64 a1)
{
return main_sub_7FFDBF84D9B0(a1, "__main__");
}
出现了__main__,看来再跟几步就能出现主函数。sub_7FFDBF84D9B0又包含了很多函数逻辑,但同时也有PyUnicode_FromString这样的python库调用。我认为这个函数应该是nuitka自行插入的功能通用函数,而主函数藏在其中某个调用中,所以我狂按f8步过,同时观察控制台窗口,当提示文字出现就说明执行的最后一行代码会进入主函数。
按这样的方式步入sub_7FFDBF84D9B0、sub_7FFC5E78DC90、sub_7FFC5E78A9B0、sub_7FFC5E76B340、sub_7FFC5E783A00,最后在sub_7FFC5E783A00中进入一个函数指针,找到主函数sub_7FFC5E769C50,我把它命名为real_main。
- 这个函数只有一个参数,并且这个参数会传给其他函数的第一个参数,所以我假设它为python库的上下文结构体。
- 经过逐个的单步调试可以确定printstrfunc这个函数相当于python的print,并以此排除这个函数前半段冗长的打印提示文字的部分。这个函数的第三个参数是一个包含了PyUnicode结构体的PyTuple结构体,而对于PyUnicode结构体,其字符串就跟在开头的元数据后方
- 直接运行然后直接暂停,ida这边会因为得不到控制权而断不下来。这时在控制台随便输入一个文本然后回车,ida这边才处于暂停状态,shift+f7让函数返回,回到real_main得到获取输入文本的位置。
- 获得输入文本后,会再次出现大量不知所谓的代码,但经过这些代码后会开始调用一个返回python312__Py_FalseStruct的函数,并且python312__Py_FalseStruct会被get_bool_val处理为0。我猜测这应该是校验flag的逻辑。
- 但是整个函数中类似的逻辑出现了好几次,那么可以大致说明这里也有和前几题一样的假验证和真验证。给每个验证加上断点逐个测试,最后发现输出结果至v112后就会出现Access Denied的提示。那么我猜测真验证大概就在这里了
// write access to const memory has been detected, the output may be wrong!
__int64 __fastcall real_main(_QWORD *ctx)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
//初始化部分
………………
v13 = (__int64 *)printstrfunc((__int64)ctx, v12, eqeqqeqeqeeq);
…………
v20 = (__int64 *)printstrfunc((__int64)ctx, v19, crkmechellenge);
…………
v25 = (__int64 *)printstrfunc((__int64)ctx, v24, eqeqqeqeqeeq);
………………
v35 = (__int64 *)printstrfunc((__int64)ctx, v34, hint);
………………
v40 = (__int64 *)printstrfunc((__int64)ctx, v39, trytrytry);
………………
v45 = (__int64 *)printstrfunc((__int64)ctx, v44, mmmmm);
………………
if ( !cfunc )
{
………………
} // -----------------------------初始化结束
if ( v54 ) // 读取输入文本
inputStrObj = (__int64 *)verify_func((__int64)ctx, (__int64)v53, v54);
else
inputStrObj = sub_7FFC5E783A00((__int64)ctx, (__int64)v53);
if ( !inputStrObj ) //不知所谓的代码
{
v89 = ctx[12];
is1stsucceeded = 0i64;
inputstrptr = 0i64;
v90 = 99i64;
ctx[12] = 0i64;
goto LABEL_115;
}
adict1.m128_u64[0] = (unsigned __int64)inputStrObj;
inputstrptr = (*(__int64 (__fastcall **)(__int64, __m128 *, __int64, _QWORD))(PyMethodDescr_Type
+ *(_QWORD *)(*(_QWORD *)(PyMethodDescr_Type + 8)
+ 56i64)))(
PyMethodDescr_Type,
&adict1,
1i64,
0i64);
if ( (int)*inputStrObj >= 0 )
{
v55 = *inputStrObj - 1;
*inputStrObj = v55;
if ( !v55 )
(*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
}
if ( !inputstrptr )
{
v89 = ctx[12];
is1stsucceeded = 0i64;
inputStrObj = 0i64;
v90 = 99i64;
ctx[12] = 0i64;
goto LABEL_115;
}
v56 = *((_QWORD *)adict + 4);
if ( !*(_BYTE *)(v56 + 0xA) ) // 1
{
v165 = (__int64 *)sub_7FFC5E768B20((__int64)adict, *(__int64 *)&fakechk);
if ( v165 )
{
ptrval1 = *v165;
goto LABEL_243;
}
goto LABEL_150;
}
val46hreg = *(_DWORD *)(v56 + 0xC);
if ( !val46hreg )
{
v58 = ctx[2];
val46hreg = *(_DWORD *)(v58 + 270616);
*(_DWORD *)(v58 + 0x42118) = val46hreg + 1;
*(_DWORD *)(v56 + 12) = val46hreg;
}
if ( val46h != val46hreg )
{
val46h = val46hreg;
v59 = sub_7FFC5E7803A0(v56, *(__int64 *)&fakechk, *(_QWORD *)(*(_QWORD *)&fakechk + 24i64));
qword_7FFC5E7C8730 = v59;
if ( v59 >= 0 )
goto LABEL_63;
LABEL_150:
v106 = *(_QWORD *)&fakechk;
goto LABEL_151;
}
v59 = qword_7FFC5E7C8730;
if ( qword_7FFC5E7C8730 < 0 )
goto LABEL_150;
LABEL_63:
__name__ = v56 + (1i64 << *(_BYTE *)(v56 + 9)) + 0x20;
ptrval1 = *(_QWORD *)(__name__ + 16 * v59 + 8);
if ( ptrval1 )
goto LABEL_64;
v221 = *(_QWORD *)&fakechk;
v170 = sub_7FFC5E7803A0(v56, *(__int64 *)&fakechk, *(_QWORD *)(*(_QWORD *)&fakechk + 24i64));
v106 = v221;
qword_7FFC5E7C8730 = v170;
if ( v170 < 0 )
{
LABEL_151:
v107 = (__int64 *)sub_7FFC5E768B20(dict2, v106);
if ( !v107 || (ptrval1 = *v107) == 0 )
{
is1stsucceeded = 0i64;
inputStrObj = 0i64;
sub_7FFC5E781640((__int64)ctx, (__int64 *)&adict1, *(__int64 *)&fakechk);
v89 = adict1.m128_u64[0];
v90 = 101i64;
goto LABEL_115;
}
goto LABEL_64;
}
ptrval1 = *(_QWORD *)(__name__ + 16 * v170 + 8);
LABEL_243:
if ( !ptrval1 )
goto LABEL_150;
LABEL_64:
*(_DWORD *)(v4 + 0x28) = 'e'; // a->e
pybool_1 = verify_func((__int64)ctx, ptrval1, (_DWORD *)inputstrptr);// 第一次假校验,返回false即为失败
inputStrObj = (__int64 *)pybool_1;
if ( pybool_1 )
{
is1stsucceeded = (unsigned int)get_bool_val(pybool_1);
v64 = *inputStrObj;
if ( (_DWORD)is1stsucceeded == -1 )
{
v89 = ctx[12];
ctx[12] = 0i64;
if ( (int)v64 >= 0 )
{
v167 = v64 - 1;
*inputStrObj = v167;
if ( !v167 )
(*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
}
v90 = 101i64;
is1stsucceeded = 0i64;
inputStrObj = 0i64;
goto LABEL_115;
}
if ( (int)v64 >= 0 )
{
v65 = v64 - 1;
*inputStrObj = v65;
if ( !v65 )
(*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
}
if ( (_DWORD)is1stsucceeded )
{
……………………
}
else
{
*(_DWORD *)(v4 + 40) = 'j';
v112 = invoke((__int64)ctx, (__int64)v16, *(__int64 *)&verify, (_DWORD *)inputstrptr);// 在此步入,如果这里返回false校验就会失败
inputStrObj = (__int64 *)v112;
if ( v112 )
{
is2ndsucceed = get_bool_val(v112);
v115 = *inputStrObj;
if ( is2ndsucceed == -1 )
{
v89 = ctx[12];
ctx[12] = 0i64;
if ( (int)v115 >= 0 )
{
v206 = v115 - 1;
*inputStrObj = v206;
if ( !v206 )
(*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
}
v90 = 106i64;
is1stsucceeded = 0i64;
inputStrObj = 0i64;
goto LABEL_115;
}
if ( (int)v115 >= 0 )
{
v116 = v115 - 1;
*inputStrObj = v116;
if ( !v116 )
(*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
}
if ( !is2ndsucceed )
{
v156 = sub_7FFC5E7624F0(printStr, v113);
*(_DWORD *)(v4 + 40) = 119;
v157 = (__int64 *)printstrfunc((__int64)ctx, v156, qword_7FFC5E7C8298);// 输出: [X] Access Denied!
…………
}
接下来进一步步入,经过以下函数:
__int64 __fastcall invoke(__int64 a1, __int64 a2, __int64 a3, _DWORD *a4)
{
………………
v8 = *(_QWORD *)(a2 + 8);
v9 = *(__int64 (__fastcall **)(__int64, __int64))(v8 + 144);
if ( !v9 )
{
………………
}
v10 = v9(a2, a3); // PyObject_GenericGetAttr->null
v11 = (_QWORD *)v10;
if ( !v10 )
return 0i64;
v12 = *(_QWORD *)(v10 + 8);
v13 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v12 + 272);
if ( v13 && *(_QWORD *)(v12 + 280) )
{
………………
}
result = verify_func(a1, (__int64)v11, a4); // 在此步入,返回false即为失败
if ( (int)*v11 >= 0 )
{
…………
}
return result;
}
__int64 __fastcall verify_func(__int64 a1, __int64 a2, _DWORD *str)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
m_object = *(_QWORD *)(a2 + 8);
str_0 = str;
a22 = a2;
if ( (__int64 *)m_object == &qword_7FFC5E7B5260 )
{
……………………
return 0i64;
}
if ( (_UNKNOWN *)m_object == &null )
{
……………………
PyErr_Format(
PyExc_TypeError,
"unbound compiled_method %s%s must be called with %s instance as first argument (got %s instance instead)",
CALLABLE_NAME,
CALLABLE_DESC,
CLASS_NAME,
INSTANCE_CLASS_NAME);
}
return v10;
}
a22 = *(_QWORD *)(a2 + 16);
if ( *(_BYTE *)(a22 + 80) )
{
v14 = *(_QWORD *)(a22 + 64);
if ( v14 == 2 )
{
v49 = *(_DWORD *)v10;
v88 = (_DWORD *)v10;
v50 = v49 + 1;
if ( v50 )
*(_DWORD *)v10 = v50;
v51 = *str_0;
v89 = str_0;
v52 = v51 + 1;
if ( v52 )
*str_0 = v52;
return (*(__int64 (__fastcall **)(__int64, __int64, _DWORD **))(a22 + 120))(a1, a22, &v88); //在此步入,函数指针,进入验证函数
}
if ( v14 == *(_QWORD *)(a22 + 160) + 2i64 )
{
v43 = alloca(sub_7FFC5E79BFC0());
………………
}
最后进入验证函数verify_1(即sub_7FFC5E767880),在这里进行最后的分析。
- 从第一句开始,省略掉冗余的代码后,出现了一个函数调用,被调用后返回的是PyLong类型的对象,分析这个函数(见下方代码注释)后,可得出这个函数功能是用于字符串长度(这里记为len),进而推测出这里存在长度验证
- 再往后,后续代码中疑似出现了有关数值比较的逻辑(见下方代码注释),根据这些逻辑,反推出正确的长度的变量位于totlen1,查看内存得到正确长度30。
- 重新开始调试,这次输入长度30的字符串
123456789012345678901234567890,调试会进入后续的for循环代码块,这里应该开始进行字符串的对比
- 继续单步调试,过上几步后会再次遇见函数调用,第三个参数是PyUnicode类型,根据它的字符串内容可判断这里是在获取正确flag的字符,因此分析这个调用返回的内容就能获得flag
- 但是这里只返回了第一个字符
5。所以将输入的内容改为523456789012345678901234567890再次进行调试,以此类推得到第二个字符2,第三个p……最终拼凑出整个flag
// write access to const memory has been detected, the output may be wrong!
int *__fastcall verify_1(_QWORD *a1, __int64 a2, __int64 **a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v3 = typetype;
total_len_source = *a3;
instr = a3[1]; //a3应该是*args,而a3[1]应该是该函数对应的python代码的第一个参数
if ( !typetype )
{
LABEL_5:
………………
}
v6 = *(_QWORD *)typetype;
if ( *(__int64 *)typetype > 1 )
{
………………
}
if ( *(_QWORD *)(typetype + 16) )
{
………………
}
LABEL_6:
v7 = (__int64 *)a1[7];
v8 = *v7;
*(_QWORD *)(v3 + 80) = *v7;
*v7 = v3 + 72;
if ( v8
&& (*(_BYTE *)(v8 + 70) == 1
|| *(_QWORD *)(v8 + 56) >= (unsigned __int64)(*(_QWORD *)v8 + 2i64 * *(int *)(*(_QWORD *)v8 + 176i64) + 192)) )
{
………………
}
v11 = *(_DWORD *)v3;
*(_DWORD *)(v3 + 64) = 0;
v12 = v11 + 1;
if ( v12 )
*(_DWORD *)v3 = v12;
instrlen = len((__int64)a1, (__int64)instr); //输入的长度
/*
len函数(sub_7FFC5E781CB0)中包含字符串"object of type '%s' has no len()",因此我给该函数取名len
*/
if ( !instrlen )
{
…………
return 0i64;
}
totlen1 = sub_7FFC5E797280((__int64)total_len_source, _total_len);// 正确的flag长度,步过此句后,查看totlen1的内存可知,正确的flag字符串长度应为0x1E也就是30
totlen2 = (_QWORD *)totlen1;
if ( !totlen1 )
{
v126 = (_QWORD *)a1[12];
a1[12] = 0i64;
v176 = v126;
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v127 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v127;
if ( !v127 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
}
instrlen = 0i64;
v94 = 48i64;
goto LABEL_195;
}
v17 = *(_QWORD *)(totlen1 + 8);
v18 = PyLong_Type[0];
if ( v17 == PyLong_Type[0] )
{
v28 = 0;
if ( instrlen != totlen1 )
{
v28 = 1;
v45 = *(_QWORD *)(instrlen + 16) >> 3;
v14 = v45 * (1 - (*(_QWORD *)(instrlen + 16) & 3i64));
if ( v14 == (totlen2[2] >> 3) * (1 - (totlen2[2] & 3i64)) )
{
do
{
if ( v45-- == 0 )
{
v28 = 0;
goto LABEL_30;
}
}
while ( *(_DWORD *)(instrlen + 4 * v45 + 24) == *((_DWORD *)totlen2 + v45 + 6) );
v28 = 1;
}
}
goto LABEL_30;
}
v19 = *(_QWORD *)(v17 + 344);
if ( v19 )
{
v20 = *(_QWORD *)(v19 + 16);
if ( v20 > 0 )
{
v21 = 0i64;
while ( *(_QWORD *)(v19 + 8 * v21 + 24) != PyLong_Type[0] )
{
if ( v20 == ++v21 )
goto LABEL_168;
}
LABEL_23:
v22 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v17 + 200);
if ( v22 )
{
v167 = PyLong_Type[0];
v23 = v22(totlen2, instrlen, 3i64); //出现了疑似比较数值大小的调用
v18 = PyLong_Type[0];
v24 = (_QWORD *)v23;
if ( v23 != Py_NotImplementedStruct[0] )
{
if ( v23 )
goto LABEL_26;
goto LABEL_188;
}
v131 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
if ( v131 )
{
v132 = v131(instrlen, totlen2, 3i64); //出现了疑似比较数值大小的调用
v18 = PyLong_Type[0];
v99 = (_QWORD *)v132;
if ( (_QWORD *)v132 != v24 )
goto LABEL_170;
}
LABEL_190:
v28 = instrlen != (_QWORD)totlen2;
goto LABEL_30;
}
v97 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
if ( !v97 )
goto LABEL_190;
goto LABEL_169;
}
}
else
{
v133 = *(_QWORD *)(totlen1 + 8);
while ( 1 )
{
v133 = *(_QWORD *)(v133 + 256);
if ( !v133 )
break;
if ( v133 == PyLong_Type[0] )
goto LABEL_23;
}
if ( PyLong_Type[0] == PyBaseObject_Type[0] )
goto LABEL_23;
}
LABEL_168:
v97 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
if ( !v97 )
{
LABEL_185:
v106 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v17 + 200);
if ( v106 )
{
v167 = v18;
v107 = v106(totlen2, instrlen, 3i64); //出现了疑似比较数值大小的调用
v18 = v167;
v24 = (_QWORD *)v107;
if ( v107 != Py_NotImplementedStruct[0] )
{
if ( v107 )
{
LABEL_26:
bool_val = get_bool_val((__int64)v24);
v18 = v167;
v26 = bool_val;
if ( (int)*v24 >= 0 )
{
v27 = *v24 - 1i64;
*v24 = v27;
if ( !v27 )
{
(*(void (__fastcall **)(_QWORD *))(v24[1] + 48i64))(v24);
v18 = v167;
}
}
v28 = v26 != 0;
goto LABEL_30;
}
LABEL_188:
v28 = -1;
goto LABEL_30;
}
}
goto LABEL_190;
}
LABEL_169:
v98 = v97(instrlen, totlen2, 3i64); //出现了疑似比较数值大小的调用
v18 = PyLong_Type[0];
v99 = (_QWORD *)v98;
if ( v98 == Py_NotImplementedStruct[0] )
goto LABEL_185;
LABEL_170:
if ( !v99 )
goto LABEL_188;
v177 = v18;
v100 = get_bool_val((__int64)v99);
v18 = v177;
v101 = v100;
if ( (int)*v99 >= 0 )
{
v102 = *v99 - 1i64;
*v99 = v102;
if ( !v102 )
{
(*(void (__fastcall **)(_QWORD *))(v99[1] + 48i64))(v99);
v18 = v177;
}
}
v28 = v101 != 0;
LABEL_30:
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v29 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v29;
if ( !v29 )
{
v168 = v18;
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
v18 = v168;
}
}
if ( (int)*totlen2 >= 0 )
{
v30 = *totlen2 - 1i64;
*totlen2 = v30;
if ( !v30 )
{
v169 = v18;
(*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
v18 = v169;
}
}
if ( v28 == -1 )
{
v129 = (_QWORD *)a1[12];
v94 = 48i64;
totlen2 = 0i64;
instrlen = 0i64;
a1[12] = 0i64;
v176 = v129;
goto LABEL_195;
}
if ( v28 == 1 )
{
v31 = (int *)Py_FalseStruct; //出现了布尔值,大概是判断长度验证是否通过
if ( Py_FalseStruct != -1 )
++Py_FalseStruct;
v32 = 0i64;
instrlen = 0i64;
goto criticalFailure; //未通过将直接返回False
}
v170 = v18;
v47 = sub_7FFC5E797280((__int64)total_len_source, _total_len);
instrlen = v47;
if ( !v47 )
{
v134 = (_QWORD *)a1[12];
totlen2 = 0i64;
a1[12] = 0i64;
v94 = 51i64;
v176 = v134;
goto LABEL_195;
}
range = sub_7FFC5E789720(a1, v47);
v49 = v170;
totlen2 = (_QWORD *)range;
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v50 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v50;
if ( !v50 )
{
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
v49 = v170;
}
}
if ( !totlen2 )
goto LABEL_240;
v171 = v49;
v51 = sub_7FFC5E7995A0(totlen2);
v52 = v171;
v53 = (__int64 *)v51;
if ( (int)*totlen2 >= 0 )
{
v54 = *totlen2 - 1i64;
*totlen2 = v54;
if ( !v54 )
{
(*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
v52 = v171;
}
}
totlen2 = 0i64;
if ( !v53 )
{
LABEL_240:
v130 = (_QWORD *)a1[12];
instrlen = 0i64;
a1[12] = 0i64;
v94 = 51i64;
v176 = v130;
goto LABEL_195;
}
v181 = v52;
for ( i = 0i64; ; i = (__int64 *)instrlen ) //这里出现了循环
{
_iternext = (_DWORD *)(*(__int64 (__fastcall **)(__int64 *))(v53[1] + 224))(v53);
instrlen = (__int64)_iternext;
if ( !_iternext )
{
v139 = i;
if ( !(unsigned __int8)sub_7FFC5E7615A0(a1, PyExc_StopIteration) )//这里出现了迭代有关的处理逻辑,意味着这里大概存在字符串的对比
{
v157 = (_QWORD *)a1[12];
a1[12] = 0i64;
v176 = v157;
if ( !i )
{
v94 = 51i64;
LABEL_164:
if ( (int)*v53 >= 0 )
{
v96 = *v53 - 1;
*v53 = v96;
if ( !v96 )
{
v183 = v94;
(*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
v94 = v183;
}
}
goto LABEL_195;
}
instrlen = (__int64)i;
v94 = 51i64;
goto LABEL_161;
}
v140 = a1[7];
v141 = *(_QWORD *)(*(_QWORD *)v140 + 48i64);
v142 = *(__int64 **)(v141 + 16);
*(_QWORD *)v140 = *(_QWORD *)(*(_QWORD *)v140 + 8i64);
*(_DWORD *)(v141 + 64) = -1;
if ( v142 )
{
v143 = *v142;
*(_QWORD *)(v141 + 16) = 0i64;
if ( (int)v143 >= 0 )
{
v144 = v143 - 1;
*v142 = v144;
if ( !v144 )
(*(void (**)(void))(v142[1] + 48))();
}
}
if ( (int)*(_QWORD *)v141 >= 0 )
{
v145 = *(_QWORD *)v141 - 1i64;
*(_QWORD *)v141 = v145;
if ( !v145 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(v141 + 8) + 48i64))(v141);
}
*(_QWORD *)(v141 + 80) = 0i64;
if ( i )
{
if ( (int)*i >= 0 )
{
v146 = *i - 1;
*i = v146;
if ( v146 )
{
v147 = *v53;
if ( (int)*v53 >= 0 )
goto LABEL_277;
}
else
{
(*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
v147 = *v53;
if ( (int)*v53 >= 0 )
{
LABEL_277:
v148 = v147 - 1;
*v53 = v148;
if ( !v148 )
(*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
if ( Py_TrueStruct != -1 )
++Py_TrueStruct;
v32 = totlen2;
instrlen = (__int64)v139;
v31 = (int *)Py_TrueStruct; //验证成功时返回的布尔值True
goto LABEL_49;
}
}
v164 = (int *)Py_TrueStruct;
v165 = Py_TrueStruct + 1;
if ( Py_TrueStruct == -1 )
{
LABEL_312:
v32 = totlen2;
instrlen = (__int64)v139;
v31 = v164;
goto LABEL_50;
}
LABEL_311:
*v164 = v165;
goto LABEL_312;
}
v147 = *v53;
if ( (int)*v53 >= 0 )
goto LABEL_277;
v164 = (int *)Py_TrueStruct;
v165 = Py_TrueStruct + 1;
if ( Py_TrueStruct != -1 )
goto LABEL_311;
}
else
{
v147 = *v53;
if ( (int)*v53 >= 0 )
goto LABEL_277;
v164 = (int *)Py_TrueStruct;
if ( Py_TrueStruct != -1 )
++Py_TrueStruct;
}
v32 = totlen2;
v31 = v164;
goto LABEL_53;
}
if ( i )
{
if ( (int)*i < 0 )
{
v58 = *_iternext + 1;
if ( *(_DWORD *)instrlen == -1 )
goto LABEL_93;
}
else
{
v57 = *i - 1;
*i = v57;
if ( v57 )
{
if ( *(_DWORD *)instrlen != -1 )
{
++*(_DWORD *)instrlen;
v57 = *i;
}
goto LABEL_90;
}
(*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
v58 = *(_DWORD *)instrlen + 1;
if ( *(_DWORD *)instrlen == -1 )
{
v57 = *i;
goto LABEL_90;
}
}
*(_DWORD *)instrlen = v58;
v57 = *i;
LABEL_90:
if ( (int)v57 >= 0 )
{
v59 = v57 - 1;
*i = v59;
if ( !v59 )
(*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
}
goto LABEL_93;
}
v79 = *_iternext + 1;
if ( v79 )
*(_DWORD *)instrlen = v79;
LABEL_93:
if ( !total_len_source )
{
v149 = PyUnicode_FromFormat(
"cannot access local variable '%U' where it is not associated with a value",
qword_7FFC5E7C8150);
v176 = (_QWORD *)verify_func((__int64)a1, PyExc_UnboundLocalError, (_DWORD *)v149);
if ( (int)*(_QWORD *)v149 >= 0 )
{
v150 = *(_QWORD *)v149 - 1i64;
*(_QWORD *)v149 = v150;
if ( !v150 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(v149 + 8) + 48i64))(v149);
}
sub_7FFC5E798040(a1[31], v176);
v94 = 52i64;
goto LABEL_161;
}
*(_DWORD *)(v3 + 40) = 52;
v32 = (_QWORD *)invoke(
(__int64)a1,
(__int64)total_len_source,
*(__int64 *)&get_char_at_position, //获取字符串指定位置的字符
(_DWORD *)instrlen);
if ( !v32 )
{
v151 = (_QWORD *)a1[12];
v94 = 52i64;
a1[12] = 0i64;
v176 = v151;
goto LABEL_161;
}
if ( totlen2 )
{
if ( (int)*totlen2 >= 0 )
{
v60 = *totlen2 - 1i64;
*totlen2 = v60;
if ( !v60 )
(*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
}
}
if ( v32 == (_QWORD *)Py_NoneStruct[0] )
break;
v61 = sub_7FFC5E762300((__int64)a1, (__int64)instr, instrlen);
v62 = (_QWORD *)v61;
if ( !v61 )
goto LABEL_257;
v63 = *(_QWORD *)(v61 + 8);
if ( v32 == (_QWORD *)v61 )
{
if ( v63 == v181 )
{
if ( (int)*v32 >= 0 )
{
v105 = *v32 - 1i64;
*v32 = v105;
if ( !v105 )
(*(void (__fastcall **)(_QWORD *))(v181 + 48))(v32);
}
goto LABEL_140;
}
if ( v63 == PyList_Type || v63 == PyTuple_Type )
{
if ( (int)*v32 >= 0 )
{
v108 = *v32 - 1i64;
*v32 = v108;
if ( !v108 )
(*(void (__fastcall **)(_QWORD *))(v63 + 48))(v32);
}
goto LABEL_140;
}
LABEL_131:
v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
if ( !v80 )
goto LABEL_179;
goto LABEL_132;
}
v14 = v32[1];
if ( v14 == v63 )
goto LABEL_131;
v64 = *(_QWORD *)(v14 + 344);
if ( v64 )
{
v65 = *(_QWORD *)(v64 + 16);
if ( v65 <= 0 )
goto LABEL_176;
v66 = 0i64;
while ( *(_QWORD *)(v64 + 8 * v66 + 24) != v63 )
{
if ( v65 == ++v66 )
goto LABEL_176;
}
}
else
{
v138 = v32[1];
while ( 1 )
{
v138 = *(_QWORD *)(v138 + 256);
if ( !v138 )
break;
if ( v138 == v63 )
goto LABEL_108;
}
if ( v63 != PyBaseObject_Type[0] )
{
LABEL_176:
v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
v63 = v32[1];
if ( !v80 )
{
LABEL_177:
v103 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64, __int64))(v14 + 200);
if ( !v103 )
goto LABEL_179;
v82 = (_QWORD *)v103(v32, v62, 3i64, v63);
if ( v82 == (_QWORD *)Py_NotImplementedStruct[0] )
goto LABEL_179;
LABEL_133:
if ( v82 )
{
v73 = get_bool_val((__int64)v82) != 0;
if ( (int)*v82 >= 0 )
{
v83 = *v82 - 1i64;
*v82 = v83;
if ( !v83 )
{
v174 = v73;
v75 = v82[1];
v76 = v82;
goto LABEL_137;
}
}
goto LABEL_146;
}
LABEL_254:
if ( (int)*v62 >= 0 )
{
v135 = *v62 - 1i64;
*v62 = v135;
if ( !v135 )
(*(void (__fastcall **)(_QWORD *, __int64, __int64, __int64))(v62[1] + 48i64))(v62, v69, v14, v63);
}
LABEL_257:
v136 = (_QWORD *)a1[12];
totlen2 = v32;
a1[12] = 0i64;
v94 = 53i64;
v176 = v136;
LABEL_161:
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v95 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v95;
if ( !v95 )
{
v182 = v94;
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
v94 = v182;
}
}
goto LABEL_164;
}
LABEL_132:
v175 = v63;
v81 = v80(v62, v32, 3i64);
v63 = v175;
v82 = (_QWORD *)v81;
if ( v81 != Py_NotImplementedStruct[0] )
goto LABEL_133;
v14 = v175;
goto LABEL_177;
}
}
LABEL_108:
v67 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v14 + 200);
if ( !v67 )
{
v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
v63 = v32[1];
if ( !v80 )
goto LABEL_179;
goto LABEL_132;
}
v172 = v63;
v68 = v67(v32, v62, 3i64);
v63 = v172;
v70 = v68;
if ( v68 != Py_NotImplementedStruct[0] )
{
if ( !v68 )
goto LABEL_254;
v173 = (_QWORD *)v68;
v71 = get_bool_val(v68);
v72 = v173;
v73 = v71 != 0;
if ( (int)*v173 >= 0 )
{
v74 = *v173 - 1i64;
*v173 = v74;
if ( !v74 )
{
v174 = v73;
v75 = v72[1];
v76 = v72;
LABEL_137:
(*(void (__fastcall **)(_QWORD *))(v75 + 48))(v76);
v73 = v174;
if ( (int)*v62 >= 0 )
{
v84 = *v62 - 1i64;
*v62 = v84;
if ( !v84 )
goto LABEL_181;
}
goto LABEL_139;
}
}
LABEL_146:
if ( (int)*v62 >= 0 )
{
v87 = *v62 - 1i64;
*v62 = v87;
if ( !v87 )
goto LABEL_181;
}
goto LABEL_139;
}
v137 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64, __int64))(v172 + 200);
if ( v137 )
{
v179 = (_QWORD *)v70;
v82 = (_QWORD *)v137(v62, v32, 3i64, v63);
if ( v82 != v179 )
goto LABEL_133;
}
LABEL_179:
v73 = v32 != v62;
if ( (int)*v62 >= 0 )
{
v104 = *v62 - 1i64;
*v62 = v104;
if ( !v104 )
{
LABEL_181:
v178 = v73;
(*(void (__fastcall **)(_QWORD *))(v62[1] + 48i64))(v62);
v73 = v178;
}
}
LABEL_139:
if ( v73 )
break;
LABEL_140:
v85 = a1[2];
if ( (*(_DWORD *)(*(_QWORD *)(v85 + 96) + 1236i64) || *(_DWORD *)(v85 + 416)) && (int)Py_MakePendingCalls() < 0 )
{
v93 = (_QWORD *)a1[12];
if ( v93 )
goto LABEL_159;
}
if ( *(_DWORD *)(v85 + 372) )
{
PyEval_SaveThread();
PyEval_AcquireThread(a1);
v86 = a1[16];
if ( !v86 )
goto LABEL_145;
LABEL_150:
v88 = *(_QWORD *)(v86 + 8);
v89 = (__int64 *)a1[12];
a1[16] = 0i64;
v90 = *(_DWORD *)(v88 + 168);
if ( (v90 & 0x40000000) == 0 )
{
if ( v90 < 0 && (*(_BYTE *)(v86 + 171) & 0x40) != 0 )
{
v91 = sub_7FFC5E783A00((__int64)a1, v86);
v176 = v91;
if ( v91 )
{
v14 = v91[1];
if ( (*(_BYTE *)(v14 + 171) & 0x40) != 0 )
goto LABEL_155;
v158 = PyUnicode_FromFormat("calling %R should have returned an instance of BaseException, not %R", v86);
v159 = verify_func((__int64)a1, PyExc_TypeError, (_DWORD *)v158);
if ( (int)*(_QWORD *)v158 >= 0 )
{
v160 = *(_QWORD *)v158 - 1i64;
*(_QWORD *)v158 = v160;
if ( !v160 )
{
v184 = v159;
(*(void (__fastcall **)(__int64))(*(_QWORD *)(v158 + 8) + 48i64))(v158);
v159 = v184;
}
}
v161 = (__int64 *)a1[12];
a1[12] = v159;
if ( v161 )
{
if ( (int)*v161 >= 0 )
{
v162 = *v161 - 1;
*v161 = v162;
if ( !v162 )
(*(void (**)(void))(v161[1] + 48))();
}
}
if ( (int)*v176 >= 0 )
{
v163 = *v176 - 1i64;
*v176 = v163;
if ( !v163 )
(*(void (__fastcall **)(_QWORD *))(v176[1] + 48i64))(v176);
}
}
}
else
{
v152 = PyUnicode_FromFormat("exceptions must derive from BaseException", *(_QWORD *)(v88 + 24));
v153 = verify_func((__int64)a1, PyExc_TypeError, (_DWORD *)v152);
if ( (int)*(_QWORD *)v152 >= 0 )
{
v154 = *(_QWORD *)v152 - 1i64;
*(_QWORD *)v152 = v154;
if ( !v154 )
{
v180 = v153;
(*(void (__fastcall **)(__int64))(*(_QWORD *)(v152 + 8) + 48i64))(v152);
v153 = v180;
}
}
v155 = (__int64 *)a1[12];
a1[12] = v153;
if ( v155 )
{
if ( (int)*v155 >= 0 )
{
v156 = *v155 - 1;
*v155 = v156;
if ( !v156 )
(*(void (**)(void))(v155[1] + 48))();
}
}
}
v176 = 0i64;
goto LABEL_155;
}
if ( *(_DWORD *)v86 != -1 )
++*(_DWORD *)v86;
v176 = (_QWORD *)v86;
LABEL_155:
a1[12] = v176;
if ( v89 )
{
if ( (int)*v89 >= 0 )
{
v92 = *v89 - 1;
*v89 = v92;
if ( !v92 )
{
(*(void (__fastcall **)(__int64 *))(v89[1] + 48))(v89);
v93 = (_QWORD *)a1[12];
LABEL_159:
v176 = v93;
}
}
}
a1[12] = 0i64;
totlen2 = v32;
v94 = 51i64;
goto LABEL_161;
}
v86 = a1[16];
if ( v86 )
goto LABEL_150;
LABEL_145:
totlen2 = v32;
}
v31 = (int *)Py_FalseStruct;
if ( Py_FalseStruct != -1 )
++Py_FalseStruct;
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v77 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v77;
if ( !v77 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
}
if ( (int)*v53 >= 0 )
{
v78 = *v53 - 1;
*v53 = v78;
if ( !v78 )
(*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
}
criticalFailure:
v33 = a1[7];
v34 = *(_QWORD *)(*(_QWORD *)v33 + 48i64);
v35 = *(__int64 **)(v34 + 16);
*(_QWORD *)v33 = *(_QWORD *)(*(_QWORD *)v33 + 8i64);
*(_DWORD *)(v34 + 64) = -1;
if ( v35 )
{
v36 = *v35;
*(_QWORD *)(v34 + 16) = 0i64;
if ( (int)v36 >= 0 )
{
v37 = v36 - 1;
*v35 = v37;
if ( !v37 )
(*(void (**)(void))(v35[1] + 48))();
}
}
if ( (int)*(_QWORD *)v34 >= 0 )
{
v38 = *(_QWORD *)v34 - 1i64;
*(_QWORD *)v34 = v38;
if ( !v38 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(v34 + 8) + 48i64))(v34);
}
*(_QWORD *)(v34 + 80) = 0i64;
LABEL_49:
if ( instrlen )
{
LABEL_50:
if ( (int)*(_QWORD *)instrlen >= 0 )
{
v39 = *(_QWORD *)instrlen - 1i64;
*(_QWORD *)instrlen = v39;
if ( !v39 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
}
}
LABEL_53:
if ( v32 )
{
if ( (int)*v32 >= 0 )
{
v40 = *v32 - 1i64;
*v32 = v40;
if ( !v40 )
(*(void (__fastcall **)(_QWORD *))(v32[1] + 48i64))(v32);
}
}
if ( (int)*total_len_source >= 0 )
{
v41 = *total_len_source - 1;
*total_len_source = v41;
if ( !v41 )
(*(void (__fastcall **)(__int64 *))(total_len_source[1] + 48))(total_len_source);
}
if ( (int)*instr >= 0 )
{
v42 = *instr - 1;
*instr = v42;
if ( !v42 )
(*(void (__fastcall **)(__int64 *))(instr[1] + 48))(instr);
}
return v31;
}
第六题 番外
这道题我本以为和第二题一样也可以通过通关游戏获得flag,看到easy模式没有flag,就用hard模式玩了几局,输了几局之后就转而直接动手了。用ida分析提示可能有壳,但用stud_pe查壳又没查出结果。
然后看见目录下有lua5.1的库,感觉这个游戏的源代码可能是现成的,用winrar打开果然看到lua文件,能轻松找到生成flag的函数:
local function getWinMessage()
local content = nil
if love.filesystem.getInfo("assets/flag.dat") then
content = love.filesystem.read("assets/flag.dat")
end
if not content or currentDifficulty ~= "hard" then
return "You WIN!"
end
local key = "52pojie"
local keyLen = #key
local result = {}
local bit = require("bit")
for i = 1, #content do
local b = string.byte(content, i)
local k = string.byte(key, ((i - 1) % keyLen) + 1)
table.insert(result, string.char(bit.bxor(b, k)))
end
return table.concat(result)
end
后面把这个函数改写后拿到单独的lua引擎上执行时,发现lua5.1不支持位操作,也没有bit库,就从网上找了一个实现位操作的代码补上。最终代码:
function Xor(num1,num2)
local tmp1 = num1
local tmp2 = num2
local str = ""
repeat
local s1 = tmp1 % 2
local s2 = tmp2 % 2
if s1 == s2 then
str = "0"..str
else
str = "1"..str
end
tmp1 = math.modf(tmp1/2)
tmp2 = math.modf(tmp2/2)
until(tmp1 == 0 and tmp2 == 0)
return tonumber(str,2)
end
local content = nil
local f,err=io.open("flag.dat",'rb')
if err then print(err) end
local content=f:read("*a")
print(#content)
local key = "52pojie"
local keyLen = #key
local result = {}
-- local bit = require("bit")
for i = 1, string.len(content ) do
local b = string.byte(content, i)
local k = string.byte(key, ((i - 1) % keyLen) + 1)
print(b)
table.insert(result, string.char(Xor(b, k)))
-- table.insert(result, b k)
end
print( table.concat(result))
第七题 HEX ME
看题干隐约感觉到,这道题的flag由检查正误和主动显示转变为了加密密码,指望调程序输出flag恐怕不行了。
- 拿到题目我最先打开的是任务说明.html,但发现文件中的注释“\<!-- 这不是 Web 题,没有额外的彩蛋、线索哦 --\>”后就没再进一步查看了。
- 然后直接用ida打开CM1,提示可能存在压缩,用stud_pe查壳没查出结果(可能因为工具太老旧)
- 最后像第六题一样强行用winrar打开,观察列出的文件名才发现是UPX的壳……
脱壳后进ida,因为我的ida是论坛里的,整合有一些插件,就用findcrypt进行搜索,搜索到一个crc相关参数和一个aes相关参数。
难道是整合了crc和aes的加密算法?这能破解吗?我开始怀疑这道题到底能不能解开,但是回过头一看,这时候已经有很多大佬把这道题完成了,甚至很多是在几分钟之内完成的。我想那说明应该还是有解,于是硬着头皮做下去。
起先,我真以为这里用到了crc和aes算法,将crc的部分复制给ai,ai分析认为确实是crc,致使我误认为不能靠分析算法代码得到flag。我开始仔细阅读html里的那个故事,html的内容和任务的描述是一样的,唯一的区别是多了一行暴力枚举不可取的提示。 起初我以为这个提示的意思是flag是以隐写形式隐藏的,然后我开始搜索题目文件每个细节,检查CM1资源中的png图片有无隐写,用binwalk扫描exe……忙活了一天之后一无所获。
最后发现不是这样。。
先看解密函数do_decode:
__int64 __fastcall do_decode(char *Str, FILE *Stream, FILE *a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
crc_init((__int64 *)cipherblk);
crc_fill((__int64 *)cipherblk, "52pojie_2026_", 14i64);
v6 = strlen(Str);
crc_fill((__int64 *)cipherblk, Str, v6);
crc_val = crc_final(cipherblk);
fread(Buffer, 16ui64, 1ui64, Stream);
v8 = prepare_aes_tuple((confuration *)cipherblk, crc_val, (encryptedFile *)Buffer);
result = 1i64;
if ( v8 )
{
fseek(Stream, 0, 2); //指针移至文件结尾
v10 = ftell(Stream);
result = 2i64;
v11 = v10 - 0x10; // v11 = v10 - 0x10 把文件当前大小(v10,由 ftell 得到)减去 16(0x10)。
if ( (v10 & 7) == 0 )
{
fseek(Stream, 16, 0);
datagrm = (char *)malloc(v10 - 0x10);
datagrm2 = datagrm;
if ( datagrm )
{
fread(datagrm, 1ui64, v10 - 0x10, Stream);
decode((confuration *)cipherblk, datagrm2, v10 - 0x10);
if ( (unsigned __int8)checksum_verify((confuration *)cipherblk) )
{
v14 = (unsigned __int8)datagrm2[v10 - 17];
if ( v11 < v14 )
{
free(datagrm2);
return 5i64;
}
else
{
fwrite(datagrm2, 1ui64, v11 - v14, a3);
free(datagrm2);
return 0i64;
}
}
else
{
free(datagrm2);
return 4i64;
}
}
else
{
return 3i64;
}
}
}
return result;
}
代码中将52pojie_2026_和输入的密钥flag{...}组合起来计算crc,然后传入prepare_aes_tuple中,调用decode解密,最后checksum_verify通过比较校验值是否相等来判断密码是否正确。同时读取已加密的文件,回避了文件开头的16个字节。
再看prepare_aes_tuple和子函数的代码:
__int64 __fastcall prepare_aes_tuple(confuration *conf, __int64 crc, encryptedFile *fileBuffer)
{
__int64 result; // rax
__int32 checksum; // eax
result = 0i64;
if ( fileBuffer )
{
if ( fileBuffer->cm26_header == '62MC' )
{
getaeslong(conf, crc, (__int64)&fileBuffer->iv_or_salt2);
checksum = fileBuffer->checksum;
conf->computed_checksum = -1;
conf->correct_checksum = checksum;
return 1i64;
}
}
return result;
}
void *__fastcall getaeslong(confuration *a1, __int64 crc, __int64 iv)
{
__int64 v3; // rax
// 不是。这个函数没有执行 AES 加密运算;它的操作是:
//
// 将 a2(你说是 CRC 结果)写入 a1[0](8 字节)。
// 将从 a3 开始的 16 字节逐字节拷贝到 a1+16 偏移处(即把 16 字节的块/IV/密钥材料复制到结构中)。
// 最后调用 memcpy 将名为 RijnDael_AES_LONG_7FF69282A270 的 0x100(256)字节数据拷贝到 a1+4*8(即把一个 256 字节的表/常量块复制进结构),并返回该 memcpy 的返回值。
v3 = 0i64;
*(_QWORD *)a1->crc = crc;
do
{
a1->iv[v3] = *(_BYTE *)(iv + v3);
++v3;
}
while ( v3 != 16 ); // 16个字节
return memcpy(a1->tbl, &RijnDael_AES_LONG_7FF65C3FA270, sizeof(a1->tbl));
}
既然ai说不是aes,那我也就继续放心分析了
为了分析方便,定义了结构体encryptedFile和configuration(代码里打错了。。),构成根据这里的代码逻辑而得出:
struct encryptedFile
{
__int32 cm26_header;
__int32 checksum;
__int64 iv_or_salt2; //盐?
byte data[]; //加密数据
};
struct confuration
{
byte crc[16];
byte iv[16]; //盐?
byte tbl[256]; //RijnDael_AES_LONG_7FF65C3FA270
__int32 computed_checksum;
__int32 correct_checksum;
};
(文件中的crc只有32位、iv只有64位,但是configuration中留了16字节)
接下来是核心解密函数decode,这里的代码有些混淆的部分,我在注释中将逻辑理了出来
__int64 __fastcall decode(confuration *conf, char *data, __int64 dataLen)
{
char *data_minus_24627; // rbx
char *data_eof_minus_24627; // rdi
__int32 *p_computed_checksum; // r12
__int64 result; // rax
data_minus_24627 = data - 24627;
// 这个表达式指代文件头部
data_eof_minus_24627 = &data[(dataLen & 0xFFFFFFFFFFFFFFF8ui64) - 24627];
// dataLen是固定的,=0x160=0b0001_0110_0000
// 又因为0xFFFFFFFFFFFFFFF8=0b1111_..._1111_1000
// 因此 dataLen&0xFFFFFFFFFFFFFFF8也是固定的,=0x160
// 这个表达式指代文件尾部
if ( data - 24627 < data_eof_minus_24627 )
{
p_computed_checksum = &conf->computed_checksum;
do
{
data_minus_24627 += 8;
xor_by_iv(conf->crc, (byte *)data_minus_24627 + 24619);
result = get_checksum(p_computed_checksum, (unsigned __int64)(data_minus_24627 + 24619), 8i64);
}
while ( data_minus_24627 < data_eof_minus_24627 );
// 循环体代码翻译:
// i=-8
// do {
// i+=8
// ...(...,&data[i])
// result = ...(...,&data[i],...)
// } while(i<data_len)
}
return result;
}
从这里的循环体可以看出,数据应该是以8个字节为单位进行解密的
最后是xor_by_iv,它将先前计算的crc进行移位,再查找RijnDael_AES_LONG_7FF65C3FA270的值并做或运算,最后通过计算crc[i]^iv[i]^data_3解密数据。
byte __fastcall xor_by_iv(byte *crc_and_ivNo2, byte *data)
{
int countDown; // r8d
__int64 data_1; // r11
__int64 iv_1; // rax
byte *iv_minus_21569; // rcx
unsigned __int64 iv_shfted; // rax
byte *data_plus_37688; // rdx
char data_3; // al
byte result; // al
countDown = 8;
data_1 = *(_QWORD *)data;
iv_1 = *(_QWORD *)crc_and_ivNo2;
iv_minus_21569 = crc_and_ivNo2 - 21569;
iv_shfted = __ROL8__(iv_1, 3);
do
{
iv_shfted = (iv_shfted << 8) | crc_and_ivNo2[(HIBYTE(iv_shfted) | 0x221300) - 2233056];
// HIBYTE(iv_shfted) 一定小于 0xff
// 所以(HIBYTE(iv_shfted) | 0x221300) - 0x2212E0
// =HIBYTE(iv_shfted)+0x221300-0x2212E0
// =HIBYTE(iv_shfted)+0x20
// =HIBYTE(iv_shfted)+32d
// =...->tbl[HIBYTE(iv_shfted)]
//
--countDown;
}
while ( countDown );
*(_QWORD *)crc_and_ivNo2 = iv_shfted;
data_plus_37688 = data + 37688;
// i=0
do
{
data_3 = *(data_plus_37688 - 37688);
// data_3=data[i]
++iv_minus_21569;
++data_plus_37688;
// ++i
result = iv_minus_21569[21568] ^ iv_minus_21569[21584] ^ data_3;
// =crc_and_ivNo2[i]^crc_and_ivNo2[16+i]^data_3
// =crc[i]^iv[i]^data_3
*(data_plus_37688 - 37689) = result;
// data[i]=result
}
while ( iv_minus_21569 != crc_and_ivNo2 - 21561 );
// while(i!=8) // only 8 bytes used
*((_QWORD *)crc_and_ivNo2 + 2) = data_1;
return result;
}
仔细分析后发现,这个函数中唯一的解密运算就是那行result = iv_minus_21569[21568] ^ iv_minus_21569[21584] ^ data_3;,看来确实不是aes,不然不会这么简单。又想到题目中反复强调被加密的文件是一个png图片,于是开始考虑用png文件已知的文件头数据来破解这个算法。起先我以为png顶多只有头4个字节是固定的,一搜,居然有8个字节。这时我才焕然大悟,第一轮解密的明文是已知的,也就是result=crc[i]^iv[i]^data_3中result、iv、data_3都是已知的。于是用python脚本计算第一轮正确的crc值:
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("data", help="encrypted data file")
args = ap.parse_args()
with open(args.data,'rb') as d:
file_hdr_and_checksum=d.read(8)
file_iv=d.read(8)
enc_data=d.read()
PNG=0xA1A0A0D474E5089
# 取 enc_data 的前 8 字节作为整数(与 PNG 头大小匹配)
ENC = int.from_bytes(enc_data[0:8], "little")
IV = int.from_bytes(file_iv[0:8], "little")
print("{0:016X}".format(PNG^ENC^IV))
算出来结果为95CC2E27B526E487。然后在*(_QWORD *)crc_and_ivNo2 = iv_shfted;上打上断点,断点触发后修改rax的值为前面计算出的正确的crc值,禁用断点再运行,解密成功
之前搞错方向时找到了一个检查png图片隐写的工具StegSolve,现在用它打开解密的图片,发现其中包含的tEXt区块就是flag
第八题 哈吉米
没解出来。
我用了多台安卓设备来尝试,都无法运行这道题的apk,安卓10、9、8、6都有尝试,即使是干净的安卓设备也是如此。要么安装后运行时闪退要么安装包解析失败,我也并未通过adb连接来安装。不知道是题目本身如此还是安卓系统版本太低导致的。总之没法像第二题那样当普通游戏来玩了
然后用jeb分析。在dex中找到了与解析flag有关的字符串,相关的代码疑似用的是kotlin的协程来实现的,但是主要的验证逻辑都指向了libhajimi.so里。
用ida分析libhajimi.so,一开始选择的是x86架构的so,结果ida识别不出入口函数JNI_OnLoad的JNIEnv结构体,原因是加载不出android ARM的类型库,换成arm64才正常。再后面发现有些代码是经过混淆和加密的,而且有个函数名称里就包含有"decrypt",可能需要动态调试才能理清逻辑。但是我的设备又运行不了,最后放弃了。
第九题 web语音验证码
之前没接触过wasm,所以这题是连蒙带猜的。
首先分析assets里js代码的初始化部分
………………
function gen(uid, voice) {
const ptr0 = passStringToWasm0(voice, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.gen(uid, ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
__exports.gen = gen;
………………
async function checkCode(code, expectedHash) {
………………
for (let i = 0; i < 0x2026; i++) {
current = await crypto.subtle.digest('SHA-256', current)
}
………………
return hashHex === expectedHash
}
async function init() {
………………
try {
const challenge = wasm_bindgen.gen(uid, voice)
currentHash = challenge.h
………………
document.getElementById('verifyBtn').addEventListener('click', async () => {
…………
const code = input.value.slice(5, -1)
const isValid = await checkCode(code, currentHash)
if (isValid) {
msgArea.textContent = "验证码正确,快去网站提交 flag 吧!"
msgArea.className = "success-text"
} else {
input.focus()
msgArea.textContent = "验证码与语音不匹配。"
…………
}
init()
可以看到验证码通过wasm_bindgen.gen方法生成,而且还需要输入自己的uid,检验是通过对输入内容掉去除flag{}后的0x2026次sha-256哈希与gen方法返回的challenge对象的h的值比较实现的。而gen方法来自开头闭包加载的wasm中。
所以,这道题需要逆向WebAssembly。我之前从来没有逆向过wasm,去网上搜索逆向方法,搜到的说法有f12、wabt、ida、jeb。先打开html,在f12的sources中把wasm导出出来
然后开始摸索分析wasm的方法。
-
f12可以直接调试,但是只能看见汇编码,而且诸如交叉引用之类浏览代码的功能也一点没有。
-
然后我想还是用jeb或ida会方便点,但是比较低的版本识别不了wasm,所以就又下载来了前几个月刚出的新版来尝试
- jeb5.36能加载出wasm的汇编码,但是反汇编直接失败,而且交叉引用貌似也用不了,因为是demo版吗?
- ida 9.3列出了wasm加载器,但是一加载就报错param type不正确,原因不明。
-
最后老实用网上大佬前些年介绍的传统方法:先用wabt2c转换为c,再编译为o。也就是论坛里发布的wasm一键转c这个工具。分析完后再用f12动态调试吧。。
先看一下f12给出的汇编代码的结构:
(func $gen (;50;) (export "gen") (param $var0 i32) (param $var1 i32) (param $var2 i32) (result i32) (result i32) (result i32)
(local $var3 i32)
……
(local $var31 i64)
(local $var32 externref)
global.get $global0
i32.const 16
i32.sub
local.tee $var15
global.set $global0
local.get $var1
local.set $var17
local.get $var2
local.set $var16
i32.const 0
local.set $var1
global.get $global0
i32.const 640
i32.sub
local.tee $var3
global.set $global0
local.get $var3
i32.const 96
i32.add
i32.const 0
i32.store8
local.get $var3
i32.const 88
i32.add
i64.const 0
i64.store
local.get $var3
i64.const 0
i64.store offset=80
local.get $var3
i32.const 80
i32.add
i32.const 17
call $wbg.__wbg_getRandomValues_1c61fac11405ffdc
i32.const 5412468
i32.load
local.set $var2
i32.const 5412472
i32.load
local.set $var4
i32.const 5412468
i64.const 0
i64.store align=4
还好,输入输出的变量名至少给出来了。分析js代码可知输入的是uid, ptr0, len0输出的分别是处理结果、异常、是否出现异常。
现场学习一下wasm的汇编,其指令能读写三个内存区域:
-
线性内存(Linear Memory)
- 说明: WebAssembly 的线性内存是一块连续的、可增长的字节数组,模块通过内存索引访问。
- 典型指令: load/store 系列(i32.load、i64.store、f32.load 等)。
- 访问方式: 使用基址(来自栈或局部/全局变量)加上立即数偏移和对齐参数进行读写。
- 用途示例: 存放堆数据、数组、字符串、与 JS 共享的大块缓冲区。
-
表(Table)/函数表(Function Table)
- 说明: 表主要用于间接函数调用,存储函数引用(或在引用类型启用时存储引用类型值);也可在某些扩展中存放引用对象。
- 典型指令: table.get、table.set、call_indirect(或在新版规范中 call_ref/等)。
- 访问方式: 通过索引读写表项,索引越界会触发运行时错误(或陷阱)。
- 用途示例: 支持动态/多态调用、实现语言运行时的虚函数表。
-
全局变量(Globals)与本地变量(Locals)/栈(Stack)
- 说明: 虽然本地变量和操作数栈不是“内存”区域的传统意义,但在汇编层面它们是指令直接读写的存储位置;全局变量是模块级的可导出/导入存储单元。
- 典型指令: get_local / set_local(在二进制/文本新语法中为 local.get/local.set)、global.get、global.set。
- 访问方式: 通过变量索引读写;全局可分为可变(var)和不可变(const)。
- 用途示例: 存放函数局部状态、中间计算值、模块范围配置或状态。
也就是说,flag可能藏在这三个地方的其中一个。
(虽然线索断了,但是先前学的内存区域的知识后期会用到)
最后决定先用ida进行整体上的分析。用ida 9.3分析o,与汇编相比似乎更难分析了,gen函数大到f5拒绝分析,说明gen这个函数一定把很多不同的逻辑都塞在里面了。强行修改f5插件的函数大小限制后,gen的参数个数和类型甚至都与f12的汇编对不上了。
但是好歹交叉引用功能能用了,就用graph功能将函数关系直接图形化
可见,排除掉wasm内置函数的情况下,gen引用的函数有孤立的也有相互引用的。从js的代码可以看出这个wasm的基本功能是提供flag、flag哈希和音频,我就猜测引用复杂的那部分大概与哈希和音频有关,而孤立的那些函数可能与flag有关(我知道这么分析很勉强,但是没别的办法了)
孤立函数中,func9与gen一样大到f5拒绝分析,所以我就从func9入手再次在f12中进行动态调试
接下来就是纯纯胡猜了。简单看了看,先从var0指向的线性内存地址读取了一系列数据,再进入一个大循环然后出现了一系列异或移位?尾部又是写入了一系列数据,同时也没有引用其他函数,应该是什么加解密相关的函数。
shift+f11离开func9,发现func9调用完成后紧接着就是一个大循环,循环的代码太多,而且操作大多是读写内存,所以我选择查看内存。
再次学习wasm的汇编。读取(load)和写入(store)指令的目标内存地址在栈当中储存,栈在f12中可以直接查看。
然后就是把func9调用结束后访问的内存全部查看一遍了
第一次调用处在循环起点,好像没什么东西
第二次在循环中部,也没有。
第三次是在某个块的尾部被调用。这次后续的写入出现了offset参数,也就是说操作位置要在目标地址的基础上增加一些偏移
第四次同第二次。。
到了第五次开始出现转机了。虽然汇编代码操作的内存位置对不上,但是还是在内存中看见了这一串字符
我盲猜这个就是flag,用控制台输入代码new TextDecoder().decode(new Uint8Array(memories.$memory.buffer,0x000FFE98,50))导出出来(flag每次生成的都不同,这里的截图是另一轮生成的验证码flag,所以与前面的截图对不上)
禁用断点,继续运行,然后验证,还真是flag
第十 windows 高级题
没解出来。
经过ida分析,似乎是对所有常量字符串都进行了加密,而且大部分函数中间还穿插了大量花指令和动态跳转,以致ida的f5都无法完整分析。可能需要一些特定技巧才能解除这些干扰,但是我不会。。故放弃。
第十一 MCP
我对ai的认知还停留在只能web端上交互,像MCP、openclaw那样需要花费api配额的用法都是富哥才敢玩我只能敬而远之的想法上。所以这一题我没像题目md里描述的那样通过ai配置连接,而是找了一个官方发布的调试工具MCP Inspector来手动分析,然后用python脚本得到flag。。。(现在想来可能还不如用ai分析,自找苦吃了)
这款调试工具基于nodejs开发,通过npm install下载和部署,然后用npm run来运行。填写mcp地址和类型(Streamable HTTP)就可以连接了
题目的提示没补全的时候我都是在瞎转。后面把复核这条说明补上后,我以为还有初核需要探索,就依然在瞎转。。。最后看见有许多人突然都做出来了,才发现说明里又加入了审计的提示,而且把流程也明写出来了。(既然有人能做出来,就说明肯定有解了)
翻阅mcp的协议规范,规范上指出mcp可以提供Prompts、Resources、Tools几种服务,而用调试工具查看,这个mcp上提供的只有tools,分别是:
start_session
list_documents
get_document
search_documents
review_document
log_sensitive_access
get_audit_log
execute_tool_chain
update_tool_chain
list_all_documents
get_system_config
除了start_session以外,其他都需要start_session返回的会话id作为参数(调试工具上操作时每次调用都需要自行复制粘贴会话id)。继续尝试瞎转,list_documents、get_document、search_documents都能直接使用,而后面的会报permission_denied。
通过list_documents和get_document得到mcp中包含的文本资源如下:
{
"id": "public:/docs/annual_report",
"title": "云台岁终告谕·新岁启卷",
"content": "云台司岁终告谕:凡尘与灵脉同庆,诸峰钟鼓齐鸣。请诸弟子于新岁晨钟后赴山门广场,恭听掌门示诫。偈语首字:玄霄。",
"access_level": 1,
"department": "云台司"
}
{
"id": "public:/docs/employee_handbook",
"title": "门规行仪录",
"content": "行走尘寰,当守门规:不得擅离山门,不得窥探禁阁。新岁值守次序已悬于功德堂。偈语末字:密令。",
"access_level": 1,
"department": "功德堂"
}
{
"id": "public:/docs/false_scroll",
"title": "伪卷疑章",
"content": "残页妄载:偈语首字作“玄冥”,末字作“密符”。此卷断简残缺,疑点甚多,务必慎辨真伪。",
"access_level": 1,
"department": "杂录坊"
}
{
"id": "public:/docs/challenge_hint",
"title": "玄堂复核提示",
"content": "复核字段:\nctf_request=<passphrase>\naudit_log_id=<audit_log_id>\nseal=<seal>\nseal 公式:sha256(audit_log_id + passphrase),取前8位小写十六进制。\n常见误区:\n- 复核目标文档:confidential:/audit/logs。\n- ctf_request 填的是复核口令本体,不是 access_token;口令线索散在公开卷宗中。\n- audit_log_id 要用触发拒绝后返回的完整审计编号,截断或改写都会失配。\n- 复核仅认可同一会话中最近一次拒绝访问返回的审计编号;跨会话或旧编号均会失效。\n- 审计链路讲究单一职责:复核阶段的链路配置越纯粹,越不容易断链。\n- 复核凭据具时效与一次性,校验失败后请重新触发拒绝并获取新编号。\n- 复核阶段拿到的凭据用于打通审计流程,最终读取密卷仍需后续凭据。",
"access_level": 1,
"department": "玄堂"
}
{
"id": "public:/docs/board_route_hint",
"title": "议会卷宗路引",
"content": "终卷路径由三段拼接:前缀 secret:/,域 board,条目 resolutions。按此组合即可得到最终文档 URI。",
"access_level": 1,
"department": "玄堂"
}
这里面,玄堂复核提示和议会卷宗路引是后期才被加上的。我一开始看着开头那三个文本摸不着头脑,丢给ai,不出意料地ai也看不懂。还以为“偈语”是指某个特定的加密算法的密钥。。但是又看不见密文在哪。。
到后期我才反应过来,会报permission_denied的那些工具可能才是分析的重点。update_tool_chain貌似是构建工具链的,只能运行list_documents、get_document、search_documents和get_audit_log这几个工具,而且只能在参数中填写工具名称,没有填写工具自身的参数的地方。将链参数设置为get_audit_log,会在输出中出现ctf的字眼。
玄堂复核提示也更新了好几次。更新的最终版本增加了复核目标文档,给出了审核文件的地址,经过瞎摸,这个地址应使用review_document这个工具。设置doc_id为confidential:/audit/logs,输出中的guide字段直接把解法写出来了:
{
"error": "ctf_missing_field",
"missing_field": "review_notes",
"guide": {
"review_doc_id": "confidential:/audit/logs",
"audit_log_source_doc": "secret:/board/resolutions",
"review_template": "ctf_request=<passphrase>\naudit_log_id=<audit_log_id>\nseal=<seal>",
"seal_formula": "sha256(audit_log_id + passphrase) 前8位小写十六进制",
"chain_config_required": [
"get_audit_log"
],
"token_flow_hint": "tool_access_token -> update_tool_chain/execute_tool_chain -> _audit_token",
"decoy_warning": "compliance_blob_b64 为干扰信息,不作为判题依据",
"required_public_clues": [
"public:/docs/annual_report",
"public:/docs/employee_handbook",
"public:/docs/challenge_hint",
"public:/docs/board_route_hint"
],
"unread_public_clues": [],
"target_doc_hint_doc": "public:/docs/board_route_hint"
}
}
输出的error中也包含ctf的字样,说明解题路径至少打通了(但是这个guide字段有时在调试工具中看不见,不知为何)
整理一下这里面的信息,并再次经过一轮的瞎摸可知,解题需要经过token_flow_hint描述的三个流程,所以我直接借这个提示列出我采用的过程:
- tool_access_token
- 先根据“玄堂复核提示”,以
required_public_clues中列出的所有文本id和secret:/board/resolutions为doc_id参数执行get_document(注意这里有坑:对于每个新的会话id,不能只访问secret:/board/resolutions,并且访问有顺序要求,应该最后访问它),拿到拒绝访问的提示和audit日志编号
- 然后获取audit的访问权限。在get_audit_log和update_tool_chain之间,只有update_tool_chain才会在报错时出现“ctf”字样,但是update_tool_chain有一参数正是还没找到的access_token,所以还需从其他工具入手
- 经过瞎摸发现有个工具叫review_document,它需要review_notes这个参数
- guide提示给出了review_template,正是对应review_notes
- doc_id使用
review_doc_id所给的confidential:/audit/logs
- 正确执行后就得到access_token
- 同时也出现了compliance_blob_b64以及compliance_note:
玄堂复核戒律:chain_config 仅提交 get_audit_log;混入其他工具会触发 invalid_chain 且当前凭据作废之类的字段,忽略不管
- update_tool_chain/execute_tool_chain
- 填写了update_tool_chain中的access_token参数后再执行,达成“玄堂复核提示”说的“打通审计流程”
- 但是执行update_tool_chain后再执行execute_tool_chain依旧会返回失败,而已有提示中没有提及相关信息
- 我的做法是转而执行get_audit_log,成功了,拿到了_audit_token
- _audit_token
- 以_audit_token为access_token,再以
secret:/board/resolutions为doc_id,执行get_document就得到flag了
review_template中的seal需要计算sha256,而且输入所需的audit编号又是动态获取,手工计算很麻烦,所以我最后将整个过程以脚本执行的方式进行。脚本中除了上述过程外还附加了第零步:获取mcp-session-id,用于完成mcp协议握手。先让chatGPT写出握手的代码,再照猫画虎手工补全后续步骤,调试工具会将输入输出的json结构体展示出来,因此可以直接复制粘贴(但是工具展示得不全,比如没有jsonrpc和id,所以代码脚本中需要补上)。服务器响应中除了json还包含其他东西,因此还需要截断响应体
import argparse
import requests,hashlib
import time
import json
# 用法:xxx.py tools 玄霄密令
# 其他参数作废
ap = argparse.ArgumentParser()
apsp=ap.add_subparsers(required=True,dest='command')
apspt=apsp.add_parser("tools", help="tools compromising")
apspt.add_argument("phrase",nargs="?",default="111")
apsp.add_parser("res", help="resource")
n=ap.parse_args()
url = "https://9863968daeea51ea32f40575dd41dd113.52pojie.cn:3000/mcp"
headers = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json, text/event-stream",
"User-Agent": "curl/8.13.0",
"MCP-Protocol-Version": "2024-11-05",
# 'mcp-session-id': '761814da-f993-0182-0156-c4d97b6314e0',
# "Authorization": "Bearer YOUR_TOKEN", # 如需认证则取消注释
}
payload = {
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": {"name": "test-client", "version": "1.0.0"},
"capabilities": {}
},
"id": 1
}
def presp(resp):
"""
打印服务器响应
"""
print("Status:", resp.status_code)
print("Response headers:", resp.headers)
print(resp.text)
resp = requests.post(url, json=payload, headers=headers, stream=False)
presp(resp)
uid=resp.headers["mcp-session-id"]
presp(resp)
resp.close()
headers.update({'mcp-session-id':(uid)})
if n.command=="tools":
sespayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "start_session",
"arguments": {},
"_meta": {
"progressToken": 1
}
},
"id": 1
}
syspayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_system_config",
"arguments": {
"session_id": None,
},
"_meta": {
"progressToken": 2
}
},
"id": 1
}
resp = requests.post(url, json=sespayload, headers=headers, stream=False)
presp(resp)
idx=resp.text.index("{") # 截断响应体
sid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])['session_id']
for doc in [
"public:/docs/challenge_hint",
"public:/docs/annual_report",
"public:/docs/board_route_hint",
"public:/docs/employee_handbook",
"secret:/board/resolutions",
]:
docpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_document",
"arguments": {
"session_id": sid,
"doc_id": doc
},
"_meta": {
"progressToken": 3
}
},
"id": 1
}
resp = requests.post(url, json=docpayload, headers=headers, stream=False)
presp(resp)
idx=resp.text.index("{")
aid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])['audit_log_id']
ph=n.phrase
revpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "review_document",
"arguments": {
"session_id": sid,
"doc_id": "confidential:/audit/logs",
"review_notes":f"""ctf_request={ph}
audit_log_id={aid}
seal={hashlib.sha256((aid+ph).encode('utf-8')).hexdigest()[:8].lower()}
"""
},
"_meta": {
"progressToken": 4
}
},
"id": 1
}
print(revpayload:=json.dumps(revpayload, ensure_ascii=False))
resp = requests.post(url, data=revpayload.encode("utf-8"), headers=headers, stream=False)
resp.encoding = 'utf-8'
idx=resp.text.index("{")
acid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])["tool_access_token"]
presp(resp)
# assert False
chainpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "update_tool_chain",
"arguments": {
"session_id": sid,
"chain_config": ["get_audit_log"],
"access_token": acid,
"audit_log_id": aid
},
"_meta":
{
"progressToken":5
}
},
"id": 1
}
print(chainpayload:=json.dumps(chainpayload, ensure_ascii=False))
# assert False
resp = requests.post(url, data=chainpayload.encode("utf-8"), headers=headers, stream=False)
idx=resp.text.index("{")
presp(resp)
# 这一条报错log_not_found,不知是否必要
exechainpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "execute_tool_chain",
"arguments": {
"session_id": sid
},
"_meta": {
"progressToken": 6
}
},
"id": 1
}
print(exechainpayload:=json.dumps(exechainpayload, ensure_ascii=False))
# assert False
resp = requests.post(url, data=exechainpayload.encode("utf-8"), headers=headers, stream=False)
idx=resp.text.index("{")
presp(resp)
getlogpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_audit_log",
"arguments": {
"session_id": sid,
"log_id": aid,
"access_token": acid
},
"_meta": {
"progressToken": 1
}
},
"id": 1
}
print(getlogpayload:=json.dumps(getlogpayload, ensure_ascii=False))
# assert False
resp = requests.post(url, data=getlogpayload.encode("utf-8"), headers=headers, stream=False)
idx=resp.text.index("{")
presp(resp)
resp.encoding = 'utf-8'
idx=resp.text.index("{")
audid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])["_audit_token"]
print("_audit_token is",audid)
# 最后一步!
docpayload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_document",
"arguments": {
"session_id": sid,
"doc_id": "secret:/board/resolutions",
"access_token":audid
},
"_meta": {
"progressToken": 3
}
},
"id": 1
}
resp = requests.post(url, json=docpayload, headers=headers, stream=False)
resp.encoding = 'utf-8'
presp(resp) # flag输出
elif n.command=="res":
respayload = {
"jsonrpc": "2.0",
"method": "resources/list",
"id": 1
}
resp=requests.post(url, json=respayload, headers=headers, stream=False)
presp(resp)
本来还在纠结口令到底是玄霄还是玄冥,结果直接用真卷给的口令就试出来了。。脚本最后一行的输出就是secret:/board/resolutions的内容,也就是flag了
data: {"result":{"content":[{"type":"text","text":"{\n \"id\": \"secret:/board/resolutions\",\n \"title\": \"至高议会密议\",\n \"content\": \"flag{new_year_2026_keep_warm}\",\n \"access_level\": 4,\n \"department\": \"玄霄议会\"\n}"}]},"jsonrpc":"2.0","id":1}
当然调试工具中也可得到