Loading... # NCTF 2025 复现 ## SafeProgram 主函数非常清晰了,SM4 ~~~c int __fastcall main(int argc, const char **argv, const char **envp) { _BYTE v4[24]; // [rsp+38h] [rbp-70h] BYREF _BYTE Buf1[16]; // [rsp+50h] [rbp-58h] BYREF _BYTE v6[48]; // [rsp+60h] [rbp-48h] BYREF sub_1400015F0(argc, argv, envp); memset(Buffer, 0, sizeof(Buffer)); memset(byte_14002C380, 0, sizeof(byte_14002C380)); memset(v4, 0, 0x14uLL); printf(Format); Sleep(0x1F4u); printf(aEnterYourFlag); Sleep(0x1F4u); printf(aAndHaveAGoodTi); scanf("%64s", Buffer); if ( strlen(Buffer) != 38 ) { printf(aLengthError); ExitProcess(1u); } if ( !strncmp(Buffer, Str2, 5uLL) && Buffer[37] == 125 ) { sscanf(Buffer, "NCTF{%32s}", byte_14002C380); memcpy(v4, key, 0xAuLL); memcpy(&v4[10], key, 6uLL); sm4((__int64)byte_14002C380, (__int64)v4, (__int64)Buf1); sm4((__int64)&byte_14002C380[16], (__int64)v4, (__int64)v6); } if ( memcmp(Buf1, &unk_14002A000, 0x20uLL) ) { printf(aWrongFlag); ExitProcess(1u); } printf(aCorrect); return 0; } ~~~ 函数很少,看到了TLS,TlsCallback_0注册了两种异常 ~~~c void __fastcall TlsCallback_0(__int64 a1, int a2) { if ( a2 == 1 ) { if ( (unsigned int)sub_140001410() ) { sub_140001920(&unk_14002A020); sub_140001570(); } } } _BOOL8 sub_140001410() { PVOID v2; // [rsp+28h] [rbp-20h] PVOID v3; // [rsp+30h] [rbp-18h] v2 = AddVectoredExceptionHandler(1u, Handler); v3 = AddVectoredExceptionHandler(1u, sub_140001180); return v2 && v3; } __int64 __fastcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo) { if ( ExceptionInfo->ExceptionRecord->ExceptionCode == 0xE0000001 ) // 蓝屏 sub_140001550(); return 0LL; } __int64 __fastcall sub_140001180(struct _EXCEPTION_POINTERS *ExceptionInfo) { if ( ExceptionInfo->ExceptionRecord->ExceptionCode != 0xC0000094 ) // 除0异常 return 0LL; sub_140001480(); ExceptionInfo->ContextRecord->Rip += 2LL; return 0xFFFFFFFFLL; } __int64 sub_140001480() { __int64 result; // rax int i; // [rsp+20h] [rbp-18h] int j; // [rsp+24h] [rbp-14h] for ( i = 0; i < 10; ++i ) { key[i] ^= 0x91u; result = (unsigned int)(i + 1); } for ( j = 0; j < 10; ++j ) { sub_140001E80(byte_14002A0D0, &byte_14002A0D0[key[j]]); result = (unsigned int)(j + 1); } return result; } char *__fastcall sub_140001E80(char *a1, char *a2) { char *result; // rax char v3; // [rsp+0h] [rbp-18h] v3 = *a1; *a1 = *a2; result = a2; *a2 = v3; return result; } ~~~ 可以发现除0异常会导致key做异或处理,同时会做十次sbox交换 设置完异常处理后调用sub_140001920创建了注册表项,然后初始化了dword_14002ADA0 ~~~c LSTATUS __fastcall sub_140001920(const BYTE *a1) { LSTATUS result; // eax HKEY hKey; // [rsp+58h] [rbp-20h] BYREF result = RegCreateKeyExA(HKEY_CURRENT_USER, aSoftwareNctf_0, 0, 0LL, 0, 0x20006u, 0LL, &hKey, 0LL); if ( !result ) return RegSetValueExA(hKey, aChecksum_0, 0, 4u, a1, 4u); return result; } __int64 sub_140001570() { __int64 result; // rax unsigned int v1; // [rsp+0h] [rbp-18h] int i; // [rsp+4h] [rbp-14h] int j; // [rsp+8h] [rbp-10h] for ( i = 0; i < 256; ++i ) { v1 = i; for ( j = 0; j < 8; ++j ) { if ( (v1 & 1) != 0 ) v1 = (v1 >> 1) ^ 0xEDB88320; else v1 >>= 1; } dword_14002ADA0[i] = v1; result = (unsigned int)(i + 1); } return result; } ~~~ 第二个TLS,就是检查前面checksum是否正确设置 ~~~c void __fastcall TlsCallback_1(__int64 a1, int a2) { if ( a2 == 2 || a2 == 1 ) sub_140001520(); } void sub_140001520() { if ( !sub_1400011D0() ) RaiseException(0xE0000001, 0, 0, 0LL); } _BOOL8 sub_1400011D0() { HKEY hKey; // [rsp+38h] [rbp-20h] BYREF DWORD cbData; // [rsp+40h] [rbp-18h] BYREF BYTE Data[4]; // [rsp+44h] [rbp-14h] BYREF cbData = 4; if ( RegOpenKeyExA(HKEY_CURRENT_USER, SubKey, 0, 0x20019u, &hKey) ) return 0LL; if ( RegQueryValueExA(hKey, ValueName, 0LL, 0LL, Data, &cbData) ) return 0LL; return (unsigned int)sub_140001070() == *(_DWORD *)Data; } __int64 sub_140001070() { unsigned int v1; // [rsp+24h] [rbp-44h] unsigned int i; // [rsp+28h] [rbp-40h] HMODULE ModuleHandleW; // [rsp+30h] [rbp-38h] v1 = -1; ModuleHandleW = GetModuleHandleW(0LL); for ( i = 0; i < 0xEC4uLL; ++i ) v1 = dword_14002ADA0[(unsigned __int8)(*((_BYTE *)ModuleHandleW + *(unsigned int *)((char *)ModuleHandleW + *((int *)ModuleHandleW + 15) + *(unsigned __int16 *)((char *)ModuleHandleW + *((int *)ModuleHandleW + 15) + 20) + 36) + i) ^ v1)] ^ (v1 >> 8); return ~v1; } ~~~ 所以现在要去找哪里触发了除0,在main汇编中找到  此处会导致key全部异或0x91,可以解密了,把SBOX换下即可 ~~~python cmp = [0xFB, 0x97, 0x3C, 0x3B, 0xF1, 0x99, 0x12, 0xDF, 0x13, 0x30, 0xF7, 0xD8, 0x7F, 0xEB, 0xA0, 0x6C, 0x14, 0x5B, 0xA6, 0x2A, 0xA8, 0x05, 0xA5, 0xF3, 0x76, 0xBE, 0xC9, 0x01, 0xF9, 0x36, 0x7B, 0x46] S_BOX = [0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99, 0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62, 0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6, 0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8, 0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87, 0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E, 0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1, 0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3, 0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F, 0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51, 0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8, 0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84, 0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48 ] FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc] CK = [ 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279 ] def wd_to_byte(wd, bys): bys.extend([(wd >> i) & 0xff for i in range(24, -1, -8)]) def bys_to_wd(bys): ret = 0 for i in range(4): bits = 24 - i * 8 ret |= (bys[i] << bits) return ret def s_box(wd): """ 进行非线性变换,查S盒 :param wd: 输入一个32bits字 :return: 返回一个32bits字 ->int """ ret = [] for i in range(0, 4): byte = (wd >> (24 - i * 8)) & 0xff row = byte >> 4 col = byte & 0x0f index = (row * 16 + col) ret.append(S_BOX[index]) return bys_to_wd(ret) def rotate_left(wd, bit): """ :param wd: 待移位的字 :param bit: 循环左移位数 :return: """ return (wd << bit & 0xffffffff) | (wd >> (32 - bit)) def Linear_transformation(wd): """ 进行线性变换L :param wd: 32bits输入 """ return wd ^ rotate_left(wd, 2) ^ rotate_left(wd, 10) ^ rotate_left(wd, 18) ^ rotate_left(wd, 24) def Tx(k1, k2, k3, ck): """ 密钥扩展算法的合成变换 """ xor = k1 ^ k2 ^ k3 ^ ck t = s_box(k1 ^ k2 ^ k3 ^ ck) return t ^ rotate_left(t, 13) ^ rotate_left(t, 23) def T(x1, x2, x3, rk): """ 加密算法轮函数的合成变换 """ t = x1 ^ x2 ^ x3 ^ rk t = s_box(t) return t ^ rotate_left(t, 2) ^ rotate_left(t, 10) ^ rotate_left(t, 18) ^ rotate_left(t, 24) def key_extend(main_key): MK = [(main_key >> (128 - (i + 1) * 32)) & 0xffffffff for i in range(4)] # 将128bits分为4个字 keys = [FK[i] ^ MK[i] for i in range(4)] # 生成K0~K3 RK = [] for i in range(32): t = Tx(keys[i + 1], keys[i + 2], keys[i + 3], CK[i]) k = keys[i] ^ t keys.append(k) RK.append(k) return RK def R(x0, x1, x2, x3): # 使用位运算符将数值限制在32位范围内 x0 &= 0xffffffff x1 &= 0xffffffff x2 &= 0xffffffff x3 &= 0xffffffff s = f"{x3:08x}{x2:08x}{x1:08x}{x0:08x}" return s def decode(ciphertext, rk): ciphertext = int(ciphertext, 16) X = [ciphertext >> (128 - (i + 1) * 32) & 0xffffffff for i in range(4)] for i in range(32): t = T(X[1], X[2], X[3], rk[31 - i]) c = (t ^ X[0]) X = X[1:] + [c] m = R(X[0], X[1], X[2], X[3]) return m main_key = [0xDF, 0xD2, 0xC5, 0xD7, 0xA3, 0xA5, 0xFF, 0xF2, 0xE5, 0xF7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] main_key = [main_key[i] ^ 0x91 for i in range(16)] for i in range(10): S_BOX[0], S_BOX[main_key[i]] = S_BOX[main_key[i]], S_BOX[0] main_key = main_key[:10] + main_key[:6] main_key = int(bytes(main_key).hex(), 16) rk = key_extend(main_key) for i in range(0, len(cmp), 16): print(bytes.fromhex(decode(bytes(cmp[i:i+16]).hex(), rk)).decode(), end="") ~~~ ## ezDOS 16位程序,放到现在就算没法反编译ai也能秒掉 sub_10670等函数作用其实是跳过后面的指令,比如sub_10670(手动计算完发现dl+=2)跳过2字节也就是`mov bx, si` ~~~ seg005:0000 sub_10670 proc far ; CODE XREF: seg002:0040↑P seg005:0000 5A pop dx seg005:0001 50 push ax seg005:0002 33 C0 xor ax, ax seg005:0004 B8 0F 00 mov ax, 0Fh seg005:0007 83 E0 07 and ax, 7 seg005:000A D1 E0 shl ax, 1 seg005:000C D1 E0 shl ax, 1 seg005:000E B4 1A mov ah, 1Ah seg005:0010 F6 D4 not ah seg005:0012 D0 EC shr ah, 1 seg005:0014 D0 EC shr ah, 1 seg005:0016 32 C4 xor al, ah seg005:0018 80 EC 1E sub ah, 1Eh seg005:001B 22 C4 and al, ah seg005:001D FE C0 inc al seg005:001F 02 D0 add dl, al seg005:0021 58 pop ax seg005:0022 52 push dx seg005:0023 CB retf seg005:0023 sub_10670 endp ~~~ 后面其他函数同理,可以把他们都视作花指令进行nop ~~~assembly seg002:0000 start: seg002:0000 B8 00 10 mov ax, seg dseg seg002:0003 8E D8 mov ds, ax seg002:0005 assume ds:dseg seg002:0005 8E C0 mov es, ax seg002:0007 assume es:dseg seg002:0007 33 C0 xor ax, ax seg002:0009 85 C0 test ax, ax seg002:000B 90 nop seg002:000C 90 nop seg002:000D 90 nop seg002:000E 90 nop seg002:000F 90 nop seg002:0010 B4 09 mov ah, 9 seg002:0012 8D 16 00 01 lea dx, unk_10100 ; "Show me your flag:" seg002:0016 CD 21 int 21h ; DOS - seg002:0018 B4 0A mov ah, 0Ah seg002:001A 8D 16 68 01 lea dx, unk_10168 ; 输入 seg002:001E CD 21 int 21h ; DOS - BUFFERED KEYBOARD INPUT seg002:001E ; DS:DX -> buffer seg002:0020 90 nop seg002:0021 90 nop seg002:0022 90 nop seg002:0023 90 nop seg002:0024 90 nop seg002:0025 8D 1E 68 01 lea bx, unk_10168 seg002:0029 43 inc bx seg002:002A 80 3F 26 cmp byte ptr [bx], 26h ; '&' 长度38 seg002:002D 74 03 jz short loc_104E2 seg002:002F E9 D5 00 jmp loc_105B7 seg002:0032 ; --------------------------------------------------------------------------- seg002:0032 seg002:0032 loc_104E2: ; CODE XREF: seg002:002D↑j seg002:0032 33 F6 xor si, si seg002:0034 33 FF xor di, di seg002:0036 33 C9 xor cx, cx seg002:0038 B9 00 01 mov cx, 100h seg002:003B seg002:003B loc_104EB: ; CODE XREF: seg002:003D↓j seg002:003B 57 push di seg002:003C 47 inc di seg002:003D E2 FC loop loc_104EB ; 0-255 seg002:003F seg002:003F loc_104EF: ; CODE XREF: seg002:0052↓j seg002:003F 5B pop bx seg002:0040 90 nop seg002:0041 90 nop seg002:0042 90 nop seg002:0043 90 nop seg002:0044 90 nop seg002:0045 90 nop seg002:0046 90 nop seg002:0047 88 9C 00 00 mov [si+0], bl ; 逆序存入内存 内存前256字节255-0 seg002:004B 46 inc si seg002:004C 81 FE 00 01 cmp si, 100h seg002:0050 73 02 jnb short loc_10504 seg002:0052 EB EB jmp short loc_104EF seg002:0054 ; --------------------------------------------------------------------------- seg002:0054 seg002:0054 loc_10504: ; CODE XREF: seg002:0050↑j seg002:0054 33 F6 xor si, si seg002:0056 33 FF xor di, di seg002:0058 BB 34 01 mov bx, 134h ; 字符串"NCTf2024nctF"长度地址 seg002:005B 8A 0F mov cl, [bx] seg002:005D seg002:005D loc_1050D: ; CODE XREF: seg002:00B9↓j seg002:005D 8A 94 00 00 mov dl, [si+0] ; S[i] seg002:0061 03 FA add di, dx ; j+=S[i] seg002:0063 8B C6 mov ax, si ; si相当于下标i seg002:0065 F6 F1 div cl ; i%12,12是key长度 seg002:0067 8A C4 mov al, ah seg002:0069 32 E4 xor ah, ah seg002:006B BB 35 01 mov bx, 135h ; 字符串"NCTf2024nctF"地址 seg002:006E 03 D8 add bx, ax ; 相当于key[i%12] seg002:0070 8A 07 mov al, [bx] seg002:0072 50 push ax seg002:0073 90 nop seg002:0074 90 nop seg002:0075 90 nop seg002:0076 90 nop seg002:0077 90 nop seg002:0078 90 nop seg002:0079 90 nop seg002:007A 90 nop seg002:007B 90 nop seg002:007C 90 nop seg002:007D 90 nop seg002:007E D1 E0 shl ax, 1 seg002:0080 D1 E0 shl ax, 1 seg002:0082 D1 E0 shl ax, 1 seg002:0084 8B D0 mov dx, ax seg002:0086 58 pop ax seg002:0087 52 push dx seg002:0088 90 nop seg002:0089 90 nop seg002:008A 90 nop seg002:008B 90 nop seg002:008C 90 nop seg002:008D 90 nop seg002:008E 90 nop seg002:008F D1 E8 shr ax, 1 seg002:0091 D1 E8 shr ax, 1 seg002:0093 D1 E8 shr ax, 1 seg002:0095 D1 E8 shr ax, 1 seg002:0097 D1 E8 shr ax, 1 seg002:0099 5A pop dx ; (key[i%12]<<3)|(key[i%12]>>5) seg002:009A 0A C2 or al, dl seg002:009C 03 F8 add di, ax ; j+=(key[i%12]<<3)|(key[i%12]>>5) seg002:009E 81 E7 FF 00 and di, 0FFh seg002:00A2 8A 84 00 00 mov al, [si+0] ; S[i] seg002:00A6 8A 95 00 00 mov dl, [di+0] ; S[j] seg002:00AA 86 C2 xchg al, dl ; S[i], S[j] = S[j], S[i] seg002:00AC 88 95 00 00 mov [di+0], dl seg002:00B0 88 84 00 00 mov [si+0], al seg002:00B4 46 inc si ; i+=1 seg002:00B5 81 FE 00 01 cmp si, 100h seg002:00B9 72 A2 jb short loc_1050D ; rc4_init seg002:00BB 33 C9 xor cx, cx seg002:00BD BB 69 01 mov bx, 169h ; 输入字符串的地址 seg002:00C0 8A 0F mov cl, [bx] seg002:00C2 43 inc bx seg002:00C3 8B F3 mov si, bx seg002:00C5 33 DB xor bx, bx seg002:00C7 33 D2 xor dx, dx seg002:00C9 33 C0 xor ax, ax seg002:00CB 85 C0 test ax, ax seg002:00CD 90 nop seg002:00CE 90 nop seg002:00CF 90 nop seg002:00D0 90 nop seg002:00D1 90 nop seg002:00D2 seg002:00D2 loc_10582: ; CODE XREF: seg002:00FE↓j seg002:00D2 FE C3 inc bl ; i seg002:00D4 8A 87 00 00 mov al, [bx+0] ; S[i] seg002:00D8 02 D0 add dl, al ; j+=S[i] seg002:00DA 52 push dx seg002:00DB 8B FA mov di, dx seg002:00DD 8A 87 00 00 mov al, [bx+0] seg002:00E1 86 85 00 00 xchg al, [di+0] ; S[i], S[j] = S[j], S[i] seg002:00E5 88 87 00 00 mov [bx+0], al seg002:00E9 02 85 00 00 add al, [di+0] ; S[i]+S[j] seg002:00ED 8B F8 mov di, ax seg002:00EF 8A 85 00 00 mov al, [di+0] ; S[S[i]+S[j]] seg002:00F3 90 nop seg002:00F4 90 nop seg002:00F5 90 nop seg002:00F6 90 nop seg002:00F7 90 nop seg002:00F8 90 nop seg002:00F9 40 inc ax ; S[S[i]+S[j]]+1 seg002:00FA 30 04 xor [si], al ; input[i]^=(S[S[i]+S[j]]+1) seg002:00FC 5A pop dx seg002:00FD 46 inc si seg002:00FE E2 D2 loop loc_10582 seg002:0100 EB 0F jmp short loc_105C1 seg002:0102 ; --------------------------------------------------------------------------- seg002:0102 seg002:0102 loc_105B2: ; CODE XREF: seg002:010F↓j seg002:0102 ; seg002:013E↓j seg002:0102 B8 00 4C mov ax, 4C00h seg002:0105 CD 21 int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT) seg002:0105 ; AL = exit code seg002:0107 ; --------------------------------------------------------------------------- seg002:0107 seg002:0107 loc_105B7: ; CODE XREF: seg002:002F↑j seg002:0107 ; seg002:0130↓j seg002:0107 B4 09 mov ah, 9 seg002:0109 8D 16 2A 01 lea dx, unk_1012A seg002:010D CD 21 int 21h ; DOS - PRINT STRING seg002:010D ; DS:DX -> string terminated by "$" seg002:010F EB F1 jmp short loc_105B2 seg002:0111 ; --------------------------------------------------------------------------- seg002:0111 seg002:0111 loc_105C1: ; CODE XREF: seg002:0100↑j seg002:0111 33 C9 xor cx, cx seg002:0113 33 FF xor di, di seg002:0115 33 F6 xor si, si seg002:0117 BF 41 01 mov di, 141h ; 密文地址 seg002:011A BE 68 01 mov si, 168h ; 输入加密完的地址 seg002:011D 84 C9 test cl, cl seg002:011F 90 nop seg002:0120 90 nop seg002:0121 90 nop seg002:0122 90 nop seg002:0123 90 nop seg002:0124 83 C6 02 add si, 2 seg002:0127 8A 0D mov cl, [di] seg002:0129 47 inc di seg002:012A seg002:012A loc_105DA: ; CODE XREF: seg002:0134↓j seg002:012A 8A 04 mov al, [si] seg002:012C 8A 15 mov dl, [di] seg002:012E 38 D0 cmp al, dl seg002:0130 75 D5 jnz short loc_105B7 seg002:0132 46 inc si seg002:0133 47 inc di seg002:0134 E2 F4 loop loc_105DA seg002:0136 B4 09 mov ah, 9 seg002:0138 8D 16 16 01 lea dx, unk_10116 ; "Wrong!" seg002:013C CD 21 int 21h ; DOS - PRINT STRING seg002:013C ; DS:DX -> string terminated by "$" seg002:013E EB C2 jmp short loc_105B2 seg002:0140 ; --------------------------------------------------------------------------- seg002:0140 51 push cx seg002:0141 B1 04 mov cl, 4 seg002:0143 D2 E8 shr al, cl seg002:0145 3C 0A cmp al, 0Ah seg002:0147 7C 04 jl short loc_105FD seg002:0149 04 37 add al, 37h ; '7' seg002:014B EB 02 jmp short loc_105FF seg002:014D ; --------------------------------------------------------------------------- seg002:014D seg002:014D loc_105FD: ; CODE XREF: seg002:0147↑j seg002:014D 04 30 add al, 30h ; '0' seg002:014F seg002:014F loc_105FF: ; CODE XREF: seg002:014B↑j seg002:014F 88 05 mov [di], al seg002:0151 47 inc di seg002:0152 8A C4 mov al, ah seg002:0154 24 0F and al, 0Fh seg002:0156 3C 0A cmp al, 0Ah seg002:0158 7C 04 jl short loc_1060E seg002:015A 04 37 add al, 37h ; '7' seg002:015C EB 02 jmp short loc_10610 seg002:015E ; --------------------------------------------------------------------------- seg002:015E seg002:015E loc_1060E: ; CODE XREF: seg002:0158↑j seg002:015E 04 30 add al, 30h ; '0' seg002:0160 seg002:0160 loc_10610: ; CODE XREF: seg002:015C↑j seg002:0160 88 05 mov [di], al seg002:0162 47 inc di seg002:0163 59 pop cx seg002:0164 C3 retn seg002:0165 ; --------------------------------------------------------------------------- seg002:0165 B4 02 mov ah, 2 seg002:0167 seg002:0167 loc_10617: ; CODE XREF: seg002:0170↓j seg002:0167 AC lodsb seg002:0168 3C 24 cmp al, 24h ; '$' seg002:016A 74 06 jz short locret_10622 seg002:016C 8A D0 mov dl, al seg002:016E CD 21 int 21h ; DOS - DISPLAY OUTPUT seg002:016E ; DL = character to send to standard output seg002:0170 EB F5 jmp short loc_10617 seg002:0172 ; --------------------------------------------------------------------------- seg002:0172 seg002:0172 locret_10622: ; CODE XREF: seg002:016A↑j seg002:0172 C3 retn seg002:0172 ; --------------------------------------------------------------------------- seg002:0173 00 00 00 00 00 align 10h seg002:0173 00 00 00 00 00 seg002 ends ~~~ ok基本读懂了,就是个魔改RC4,魔改点在SBOX初始值和RC4_init对key每个字节做了移位处理 ~~~python def KSA(key): """ Key-Scheduling Algorithm (KSA) 密钥调度算法""" S = list(range(256))[::-1] j = 0 for i in range(256): j = (j + S[i] + ((key[i%12]<<3)&0xff|(key[i%12]>>5))) % 256 S[i], S[j] = S[j], S[i] return S def PRGA(S): """ Pseudo-Random Generation Algorithm (PRGA) 伪随机数生成算法""" i, j = 0, 0 while True: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] K = S[(S[i] + S[j]) % 256] yield K def RC4(key, text): """ RC4 encryption/decryption """ S = KSA(key) keystream = PRGA(S) res = [] for char in text: res.append(char ^ (next(keystream)+1)) return bytes(res) key = b"NCTf2024nctF" text = [0x7C, 0x3E, 0x0D, 0x3C, 0x88, 0x54, 0x83, 0x0E, 0x3B, 0xB8, 0x99, 0x1B, 0x9B, 0xE5, 0x23, 0x43, 0xC5, 0x80, 0x45, 0x5B, 0x9A, 0x29, 0x24, 0x38, 0xA9, 0x5C, 0xCB, 0x7A, 0xE5, 0x93, 0x73, 0x0E, 0x70, 0x6D, 0x7C, 0x31, 0x2B, 0x8C] print(len(text)) print(RC4(key, text)) ~~~ 得到`NCTF{Y0u_4r3_Assemb1y_M4st3r_5d0b497e}` ## x1Login so层看下可知DecStr.get函数是先base64解密再逐字节异或长度,可以把字符串解密 ~~~java public final class MainActivity extends ComponentActivity { private final Class<?> getClass(String classname) { try { return new InMemoryDexClassLoader(ByteBuffer.wrap(Secure.loadDex(getApplicationContext(), DecStr.get("ygvUF2vHFgbPiN9J"))), getClassLoader()).loadClass(classname); // libsimple.so } catch (Exception e) { e.printStackTrace(); return null; } } private final void exit() { finish(); System.exit(-1); throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM."); } private final Object getInstance(Class<?> clazz, Object... args) throws IllegalAccessException, InstantiationException, IllegalArgumentException, InvocationTargetException { Object objNewInstance = clazz.getConstructor(Context.class, String.class, String.class).newInstance(Arrays.copyOf(args, args.length)); Intrinsics.checkNotNullExpressionValue(objNewInstance, "newInstance(...)"); return objNewInstance; } private final void checkSecutity() { if (Secure.checkDebug()) { Toast.makeText(this, "Debugger Detected!", 0).show(); exit(); } if (Secure.checkRoot()) { Toast.makeText(this, "Root Detected!", 0).show(); exit(); } } @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkSecutity(); final EditText editText = (EditText) findViewById(R.id.usernameEditText); final EditText editText2 = (EditText) findViewById(R.id.passwordEditText); Button button = (Button) findViewById(R.id.loginButton); String str = DecStr.get("Exv3nhr5BNW0axn3aNz/DNv9C3q0wxj/Exe=");//com.nctf.simplelogin.Check Intrinsics.checkNotNullExpressionValue(str, "get(...)"); final Class<?> cls = getClass(str); if (cls == null) { Toast.makeText(this, "Error: Program load failure", 0).show(); finish(); System.exit(-1); throw new RuntimeException("System.exit returned normally, while it was supposed to halt JVM."); } button.setOnClickListener(new View.OnClickListener() { // from class: com.nctf.simplelogin.MainActivity$$ExternalSyntheticLambda0 @Override // android.view.View.OnClickListener public final void onClick(View view) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { MainActivity.onCreate$lambda$0(editText, editText2, this, cls, view); } }); } /* JADX INFO: Access modifiers changed from: private */ public static final void onCreate$lambda$0(EditText editText, EditText editText2, MainActivity this$0, Class cls, View view) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Intrinsics.checkNotNullParameter(this$0, "this$0"); try { cls.getMethod(DecStr.get("zM1GzM4="), new Class[0]).invoke(this$0.getInstance(cls, this$0, editText.getText().toString(), editText2.getText().toString()), new Object[0]); // check } catch (Exception e) { e.printStackTrace(); } } } ~~~ 分析代码可知实际上调用了assets里的libsimple.so,提取出class dex中的com.nctf.simplelogin.Check,解压出来拿到so 010修改掉文件开头得到正确dex进行反编译 ~~~java package com.nctf.simplelogin; import android.content.Context; import android.widget.Toast; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /* loaded from: E:\CTF\flag_archives\CTFs\NCTF2025\x1Login\libsimple.so */ public class Check { Context context; String password; String username; public Check(Context context, String username, String password) { this.username = username; this.password = password; this.context = context; } public void check() throws NoSuchAlgorithmException { try { if (check_username()) { MessageDigest digest = MessageDigest.getInstance(DecStr.get("tMC2")); // Md5 digest.update(this.username.getBytes()); byte[] output = digest.digest(); boolean result = check_password(output); if (result) { Toast.makeText(this.context, "Login Successful! Now submit your flag!", 0).show(); return; } } Toast.makeText(this.context, "Login Failed!", 0).show(); } catch (Exception e) { e.printStackTrace(); } } private boolean check_username() { return this.username.equals(DecStr.get("uZPOs29goMu6l38=")); // X1c@dM1n1$t } private boolean check_password(byte[] key) { return Secure.doCheck(this.password, key); } } ~~~ 用户md5后作为密钥`7d53ecd36a43d3d237e7dd633dcf8497`,再来看Secure.doCheck ~~~java package com.nctf.simplelogin; import android.content.Context; import android.os.Debug; import java.io.BufferedReader; import java.io.InputStreamReader; /* loaded from: classes.dex */ public class Secure { public static native boolean doCheck(String str, byte[] bArr); public static native byte[] loadDex(Context context, String str); static { System.loadLibrary(DecStr.get("agDYB3bJ")); // native } private static boolean checkSuExists() { Process processExec = null; try { processExec = Runtime.getRuntime().exec(new String[]{"which", "su"}); boolean z = new BufferedReader(new InputStreamReader(processExec.getInputStream())).readLine() != null; if (processExec != null) { processExec.destroy(); } return z; } catch (Throwable unused) { if (processExec != null) { processExec.destroy(); } return false; } } public static boolean checkRoot() { return checkSuExists(); } public static boolean checkDebug() { return Debug.isDebuggerConnected(); } } ~~~ 拿出来libnative.so,在JNI_OnLoad中找到doCheck,里面算法比较复杂,chatgpt分析后说是3DES 有个小问题就是端序,输入的8字节貌似是按照小端读取的,所以密文、密钥每八字节都要翻转一次 ~~~python from Crypto.Cipher import DES3 import binascii def make_odd_parity(key_bytes: bytes) -> bytes: # DES 要求每个字节为 odd parity(字节中 1 的个数为奇数)。常见实现把最低位作为 parity bit。 out = bytearray() for b in key_bytes: ones = bin(b & 0xFE).count("1") # 不计最低位(假定最低位为 parity) # 如果高 7 位(b & 0xFE)中 1 的个数是偶数,则需要把最低位设置为 1;否则为 0 parity_bit = 1 if (ones % 2 == 0) else 0 out.append((b & 0xFE) | parity_bit) return bytes(out) def triple_des_decrypt_2key_pycryptodome(key16: bytes, ciphertext: bytes) -> bytes: if len(key16) != 16: raise ValueError("key must be 16 bytes for 2-key 3DES") if len(ciphertext) % 8 != 0: raise ValueError("ciphertext must be multiple of 8 bytes") key_adj = make_odd_parity(key16) # DES3 accepts 16- or 24-byte key. For 16 bytes it's treated as K1||K2 (K3 = K1). cipher = DES3.new(key_adj, DES3.MODE_ECB) plaintext = cipher.decrypt(ciphertext) return plaintext if __name__ == "__main__": key_hex = "7d53ecd36a43d3d237e7dd633dcf8497" # 示例 16 bytes (hex) ct_hex = "409EEC86B884A58B7E8A64E21AD3B8BBDF4BFA1246453E52" # 示例(占位) key = binascii.unhexlify(key_hex)[:8][::-1]+binascii.unhexlify(key_hex)[8:][::-1] ciphertext = list(binascii.unhexlify(ct_hex)) for i in range(0, len(ciphertext), 8): ciphertext[i:i+8] = ciphertext[i:i+8][::-1] ciphertext = bytes(ciphertext) pt = triple_des_decrypt_2key_pycryptodome(key, ciphertext) print(pt) ~~~ 得到`~DWPefaS+MY?x$y5=6mG50U5`,emm很怪就是了 ## gogo 长度40,看到很多main开头的VM操作,还想啥直接上条件断点,这波检测到写的插件存在一定不足,碰到`add edx, [rax+rbx*4]`这种内存计算偏移提取的值就不对了,继续改进 好在不需要下很多断点,可以直接手动修改下,稍微看了下trace记录可以发现是类似TEA,修改输入为`0000111122223333444455556666777788889999` 看到了右移2位同时and 3,基本确认是XXTEA  抄一份XXTEA源码对比着看,结合上面图里先取了0x31313131和0x34343434可知,v[n-1]对应0x34343434,所以n-1=4,所以传入v长度实际上是5个dword,也就是输入40字节分开每20字节加密一组XXTEA 但从上图也可以看出来,两组XXTEA都混在了一起,需要区分开,比如第一轮我们只关注0x30303030到0x34343434,我们根据这个原则基本提取了第一组XXTEA的第一轮逻辑 ~~~ shl 0x31313131, 0x2 = 0xc4c4c4c4 shr 0x34343434, 0x5 = 0x1a1a1a1 xor 0xc4c4c4c4, 0x1a1a1a1 = 0xc5656565 shr 0x31313131, 0x3 = 0x6262626 shl 0x34343434, 0x4 = 0x343434340 xor 0x6262626, 0x43434340 = 0x45656566 add 0xc5656565, 0x45656566 = 0xacacacb shr 0x9e3779b9, 0x2 = 0x278dde6e and 0x278dde6e, 0x3 = 0x2 xor 0x2, 0x0 = 0x2 and 0x2, 0x3 = 0x2 xor 0x9e3779b9, 0x31313131 = 0xaf064888 # total+delta初始值0x9e3779b9 imul 0x2, 0x4 = 0x8 xor 0x34343434, 0xa78c0b4f = 0x93b83f7b # k[2]=0xa78c0b4f add 0xaf064888, 0x93b83f7b = 0x42be8803 add 0x8, 0x20 = 0x28 xor 0xacacacb, 0x42be8803 = 0x487442c8 add 0x487442c8, 0x30303030 = 0x78a472f8 ~~~ 基本对应标准XXTEA,但是total初始不为0且密钥还没拿到手,先一点点找到密钥位置 e第一轮值为`(0x9e3779b9>>2)&3=2`,所以key取得是`(j&3)^2`,从0-3依次是2、3、0、1,提取出密钥为`[0x6e637466,0x62ef0ed,0xa78c0b4f,0x32303234]` 在VM的main_RET里我们可以发现密文比较 ~~~c // main.RET __int64 __golang main_RET( __int64 result, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, int a7, int a8, int a9, unsigned __int8 a10) { __int128 v10; // xmm15 __int64 v11; // rdi _BYTE *v12; // r8 __int64 v13; // rcx _BYTE v14[20]; // [rsp+1h] [rbp-15h] BYREF char v15; // [rsp+15h] [rbp-1h] BYREF __int64 v16; // [rsp+2Eh] [rbp+18h] *v14 = v10; *&v14[4] = v10; v11 = *(result + 88); v12 = v14; if ( v14 != (result + 64) ) { *v14 = *(result + 64); *&v14[4] = *(result + 68); } if ( v11 == a10 ) { v16 = result; if ( a10 ) { if ( qword_8C8458 == 20 ) { v13 = runtime_memequal(v14, off_8C8450, 20LL); result = v16; } else { v13 = 0LL; } } else if ( qword_8C8438 == 20 ) { v13 = runtime_memequal(v14, off_8C8430, 20LL); result = v16; } else { v13 = 0LL; } v15 = v13; return runtime_chansend1(*(result + 328), &v15, v13, v11, a10, v12, a7, a8, a9); } return result; } ~~~ 提取两组密文去尝试XXTEA解密,结果发现只有第一组能解密 ~~~python from ctypes import c_uint32 def xxtea_decrypt(n, v, key): # 全部转为c_unit32格式 v = [c_uint32(i) for i in v] r = 6 + 52 // n v0 = v[0].value delta = 0x9e3779b9 total = c_uint32(delta * r) for i in range(r): e = (total.value >> 2) & 3 for j in range(n-1, 0, -1): v1 = v[j-1].value v[j].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(j & 3) ^ e] ^ v1))) v0 = v[j].value v1 = v[n-1].value v[0].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(0 & 3) ^ e] ^ v1))) v0 = v[0].value total.value -= delta return [i.value for i in v] k = [0x6e637466,0x62ef0ed,0xa78c0b4f,0x32303234] v = [0xADD881DE, 0x32A6C4C2, 0x3E61AB1C, 0xF1EFFFCB, 0x167A3027] v = [0xB9D5455D, 0x389C958C, 0x1E3EB13B, 0xBBE8C85F, 0x69483864] # 解密 v = xxtea_decrypt(len(v), v, k) v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))]) print(v) # NCTF{H4rd_VM_with_Go ~~~ 第二组密文解密不对,说明算法或者密钥什么的可能不同,继续像上面那样提取第一轮加密来分析 ~~~ shr 0x36363636, 0x2 = 0xd8d8d8d shl 0x39393939, 0x5 = 0x727272720 xor 0xd8d8d8d, 0x27272720 = 0x2aaaaaad shl 0x36363636, 0x3 = 0x1b1b1b1b0 shr 0x39393939, 0x4 = 0x3939393 xor 0xb1b1b1b0, 0x3939393 = 0xb2222223 add 0x2aaaaaad, 0xb2222223 = 0xdcccccd0 add 0xf72e, 0x9f1c0000 = 0x9f1cf72e xor 0x9e3779b9, 0x36363636 = 0xa8014f8f # delta还是0x9e3779b9 xor 0x39393939, 0x9f1cf72e = 0xa625ce17 # 密钥变了 add 0xa8014f8f, 0xa625ce17 = 0x4e271da6 xor 0xdcccccd0, 0x4e271da6 = 0x92ebd176 add 0x92ebd176, 0x35353535 = 0xc82106ab ~~~ 可以发现第二组移位全部反过来了,同时密钥也变为`[0x32303234,0xd6eb12c3,0x9e3779b9,0x4e435446]` 所以只需修改密钥和移位即可 ~~~python from ctypes import c_uint32 def xxtea_decrypt1(n, v, key): # 全部转为c_unit32格式 v = [c_uint32(i) for i in v] r = 6 + 52 // n v0 = v[0].value delta = 0x9e3779b9 total = c_uint32(delta * r) for i in range(r): e = (total.value >> 2) & 3 for j in range(n-1, 0, -1): v1 = v[j-1].value v[j].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(j & 3) ^ e] ^ v1))) v0 = v[j].value v1 = v[n-1].value v[0].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(0 & 3) ^ e] ^ v1))) v0 = v[0].value total.value -= delta return [i.value for i in v] def xxtea_decrypt2(n, v, key): # 全部转为c_unit32格式 v = [c_uint32(i) for i in v] r = 6 + 52 // n v0 = v[0].value delta = 0x9e3779b9 total = c_uint32(delta * r) for i in range(r): e = (total.value >> 2) & 3 for j in range(n-1, 0, -1): v1 = v[j-1].value v[j].value -= ((((v1 << 5) ^ (v0 >> 2)) + ((v0 << 3) ^ (v1 >> 4))) ^ ((total.value ^ v0) + (key[(j & 3) ^ e] ^ v1))) v0 = v[j].value v1 = v[n-1].value v[0].value -= ((((v1 << 5) ^ (v0 >> 2)) + ((v0 << 3) ^ (v1 >> 4))) ^ ((total.value ^ v0) + (key[(0 & 3) ^ e] ^ v1))) v0 = v[0].value total.value -= delta return [i.value for i in v] k = [0x6e637466,0x62ef0ed,0xa78c0b4f,0x32303234] v = [0xB9D5455D, 0x389C958C, 0x1E3EB13B, 0xBBE8C85F, 0x69483864] # 解密 v = xxtea_decrypt1(len(v), v, k) v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))]) print(v) k = [0x32303234,0xd6eb12c3,0x9f1cf72e,0x4e435446] v = [0xADD881DE, 0x32A6C4C2, 0x3E61AB1C, 0xF1EFFFCB, 0x167A3027] # 解密 v = xxtea_decrypt2(len(v), v, k) v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))]) print(v) ~~~ 得到`NCTF{H4rd_VM_with_Gor0ut1n3_5fc4b0be7ad}` NCTF整体难度确实不大,gogo出的还可以,正好也让我发现了TraceHelper插件改进的空间 最后修改:2025 年 11 月 05 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏