Loading... # SCTF 2024 复现 ## BBox java+so层加密,so层固定seed和crc ~~~c __int64 __fastcall Java_com_example_bbandroid_MainActivity_checkFlag(JNIEnv *a1, __int64 a2, __int64 a3) { __int64 v4; // rbx int v5; // r13d const char *v6; // rax const char *v7; // r12 __int64 v8; // r14 signed int v9; // ecx int v10; // eax signed int v11; // esi signed int v12; // edx signed int v13; // esi unsigned __int64 v14; // rax int v15; // edx int v16; // esi char v18[264]; // [rsp+0h] [rbp-138h] BYREF unsigned __int64 v19; // [rsp+108h] [rbp-30h] v19 = __readfsqword(0x28u); LODWORD(v4) = 0; v5 = time(0LL); v6 = (const char *)((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD))(*a1)->GetStringUTFChars)(a1, a3, 0LL); if ( v6 ) { v7 = v6; strncpy(v18, v6, 0xFFuLL); v18[255] = 0; LODWORD(v4) = __strlen_chk(v18, 256LL); ((void (__fastcall *)(JNIEnv *, __int64, const char *))(*a1)->ReleaseStringUTFChars)(a1, a3, v7); srand(v5 / 1000000 / 100); if ( (int)v4 >= 4 ) { v4 = (unsigned int)v4 >> 2; v8 = 0LL; do { v18[4 * v8] ^= rand(); v18[4 * v8 + 1] ^= rand(); v18[4 * v8 + 2] ^= rand(); v18[4 * v8 + 3] ^= rand(); v9 = *(_DWORD *)&v18[4 * v8]; v10 = 32; do { v11 = (2 * v9) ^ 0x85B6874F; if ( v9 >= 0 ) v11 = 2 * v9; v12 = (2 * v11) ^ 0x85B6874F; if ( v11 >= 0 ) v12 = 2 * v11; v13 = (2 * v12) ^ 0x85B6874F; if ( v12 >= 0 ) v13 = 2 * v12; v9 = (2 * v13) ^ 0x85B6874F; if ( v13 >= 0 ) v9 = 2 * v13; v10 -= 4; } while ( v10 ); *(_DWORD *)&v18[4 * v8++] = v9; } while ( v8 != v4 ); } if ( v18[0] == 51 ) { v14 = -1LL; while ( __PAIR64__(v18[v14 + 3], v18[v14 + 2]) == __PAIR64__(byte_B60[v14 + 3], byte_B60[v14 + 2]) && v18[v14 + 4] == byte_B60[v14 + 4] ) { if ( v14 == 35 ) { v14 = 39LL; LABEL_25: LOBYTE(v4) = v14 >= 0x27; return (unsigned int)v4; } v15 = v18[v14 + 5]; v16 = byte_B60[v14 + 5]; v14 += 4LL; if ( v15 != v16 ) goto LABEL_25; } } LODWORD(v4) = 0; } return (unsigned int)v4; } ~~~ 提取17时候的rand,然后python解密 ~~~python s = [0xA3C8C033, 0x1A1DBFF3, 0xC6B7413B, 0x52865EF1, 0x1E6BCF52, 0xBFCBF9C5, 0xF1627BED, 0x544843F7, 0xD94C85FB, 0x6EF23035] new_s = [] for v in s: for i in range(32): bit = v & 1 if bit: v ^= 0x85B6874F v >>= 1 if bit: v |= 0x80000000 new_s += v.to_bytes(4, "little") rand = [185, 173, 127, 3, 159, 15, 241, 103, 121, 183, 57, 221, 147, 136, 174, 234, 176, 61, 122, 7, 242, 137, 229, 52, 35, 85, 216, 78, 183, 218, 236, 113] for i in range(32): print(chr(rand[i]^new_s[i]), end="") # HuqdOgqiMKPiWHFxFmPiW/M}I\rfO.}f ~~~ 接着找java层,发现被NP管理器混淆了,混淆的比较厉害,很多名称没法轻松hook,甚至jadx反编译报错 ### 法一 我想用一种构造方法,把base64表还原出来,输入不太行,麻烦,pass ### 法二 直接分析base64 table的生成,硬分析,非常笨的方法,但做出来绝对只是时间的问题,事实上我[去年复现](https://github.com/SH-Op-X/CTF-Writeups/tree/main/SCTF_2024#bbox)的时候就是这样的 ### 法三 hook生成base64表的地方,也就是下面这里 ~~~java strange.ALPHABET = (char[])ۥۣۢۢ.n(0x9259, ((String)ۥۣۢۢ.n(0xFA8C, null, new Object[]{((short[])ۥۣۢۢ.n(70719)), ((int)0), ((int)(ۤۧۦ۠.۠۠ۦۥ ^ -750)), ((int)3052)})), new Object[0]); ~~~ hook拿得到`nopqrstDEFGHIJKLhijklUVQRST/WXYZabABCcdefgmuv6789+wxyz012345MNOP` ~~~js Java.perform(function () { var Strange = Java.use("com.example.bbandroid.strange"); console.log(Strange.ALPHABET.value); }); ~~~ 但是字符集合不太对,分析encode传入发现取了输入的长度(v18),然后和输入字符串(v34后面那个是"charAt")做了异或  因此前面脚本可以加上个异或30(长度30的输入base64完才是40),解密出来就是正确的base64密文 ~~~python s = [0xA3C8C033, 0x1A1DBFF3, 0xC6B7413B, 0x52865EF1, 0x1E6BCF52, 0xBFCBF9C5, 0xF1627BED, 0x544843F7, 0xD94C85FB, 0x6EF23035] print(len(s)) new_s = [] for v in s: for i in range(32): bit = v & 1 if bit: v ^= 0x85B6874F v >>= 1 if bit: v |= 0x80000000 new_s += v.to_bytes(4, "little") rand = [185, 173, 127, 3, 159, 15, 241, 103, 121, 183, 57, 221, 147, 136, 174, 234, 176, 61, 122, 7, 242, 137, 229, 52, 35, 85, 216, 78, 183, 218, 236, 113, 136, 108, 116, 39, 123, 101, 142, 245] for i in range(40): print(chr(rand[i]^new_s[i]^30), end="") # VkozQyowSUNwIVXfXsNwI1ScWBlxQ0cxQ0UbW1Cb ~~~ cyberchef解密得到flag为`Y0u_@re_r1ght_r3ver53_is_easy!` ## ez_cython 这题可以看到sub14514传入的是个列表形式 ~~~python # Decompiled with PyLingual (https://pylingual.io) # Internal filename: ez_cython.py # Bytecode version: 3.8.0rc1+ (3413) # Source timestamp: 1970-01-01 00:00:00 UTC (0) import cy def str_hex(input_str): return [ord(char) for char in input_str] def main(): print('欢迎来到猜谜游戏!') print("逐个输入字符进行猜测,直到 'end' 结束。") while True: guess_chars = [] while True: char = input("请输入一个字符(输入 'end' 结束):") if char == 'end': break if len(char) == 1: guess_chars.append(char) else: print('请输入一个单独的字符。') guess_hex = str_hex(''.join(guess_chars)) if cy.sub14514(guess_hex): print('真的好厉害!flag非你莫属') break print('不好意思,错了哦。') retry = input('是否重新输入?(y/n):') if retry.lower() != 'y': break print('游戏结束') if __name__ == '__main__': main() ~~~ 对于这种题目,最佳做法就是类似于hook,实现一个自定义列表和Int ~~~python import cy class fakeint(int): def __init__(self, dat: int, name=None): self.dat = int(dat) if name is None: self.name = hex(id(self))[-6:] else: self.name = name def debugprint(self, dat): print(f'[{self.name}]{dat}') def __add__(self, other): r = self.dat + other self.debugprint(f'__add__({other}) -> {self.dat} + {other} = {r}') return fakeint(r) def __sub__(self, other): r = self.dat - other self.debugprint(f'__sub__({other}) -> {self.dat} - {other} = {r}') return fakeint(r) def __mul__(self, other): r = self.dat - other self.debugprint(f'__mul__({other}) -> {self.dat} * {other} = {r}') return fakeint(r) def __xor__(self, other): r = self.dat ^ other self.debugprint(f'__xor__({other}) -> {self.dat} ^ {other} = {r}') return fakeint(r) def __and__(self, other): r = self.dat & other self.debugprint(f'__and__({other}) -> {self.dat} & {other} = {r}') return fakeint(r) def __lshift__(self, other): r = self.dat << other self.debugprint(f'__lshift__({other}) -> {self.dat} << {other} = {r}') return fakeint(r) def __rshift__(self, other): r = self.dat >> other self.debugprint(f'__rshift__({other}) -> {self.dat} >> {other} = {r}') return fakeint(r) def __eq__(self, other): r = self.dat == other self.debugprint(f'__eq__({other}) -> {self.dat} == {other} = {r}') return fakeint(r) def __ne__(self, other): r = self.dat != other self.debugprint(f'__ne__({other}) -> {self.dat} != {other} = {r}') return fakeint(r) def __repr__(self): return f'fakeint({self.dat})' key_idxs = [] class fakelist(list): def __init__(self, dat, name=None): super().__init__(dat) if name is None: self.name = hex(id(self))[-6:] else: self.name = name def debugprint(self, dat): print(f'[{self.name}] {dat}') def __getitem__(self, index): global key_idxs self.debugprint(f'__getitem__({index}), {super().__getitem__(index)}') if self.name == 'key': key_idxs.append(index) return super().__getitem__(index) def __setitem__(self, index, value): self.debugprint(f'__setitem__({index}, {value})') return super().__setitem__(index, value) def copy(self): return fakelist(super().copy(), self.name) def __eq__(self, other): global out r = super().__eq__(other) self.debugprint(f'__eq__({other}) -> {super().__repr__()} == {other} = {r}') out = self return r key = b'SyC10VeRf0RVer' def ret_key(arg): print(f'ret_key({arg})') return fakelist([fakeint(x) for x in key], 'key') cy.QOOQOOQOOQOOOQ.get_key = ret_key test = b'1'*32 dat = fakelist([fakeint(x) for x in test], 'data') print(cy.sub14514(dat)) print(key_idxs) ~~~ 通过log可以找到加密规律以及密文,同构出来 ~~~python key = list(b'SyC10') xor = [2654435769, 1013904242, 3668340011, 2027808484, 387276957] idx = [3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1] for j in range(5): for i in range(32): a=((s[(i-1)%32]>>3)^(s[(i+1)%32]<<3))+((s[(i-1)%32]<<2)^(s[(i+1)%32]>>4)) b=(xor[j]^s[(i+1)%32])+(key[idx[j*32+i]]^s[(i-1)%32]) s[i%32]=((a^b)+s[i%32])&0xffffffff ~~~ 逆向very easy(其实是XXTEA类型,但没必要按照模板了) ~~~python s = [4108944556, 3404732701, 1466956825, 788072761, 1482427973, 782926647, 1635740553, 4115935911, 2820454423, 3206473923, 1700989382, 2460803532, 2399057278, 968884411, 1298467094, 1786305447, 3953508515, 2466099443, 4105559714, 779131097, 288224004, 3322844775, 4122289132, 2089726849, 656452727, 3096682206, 2217255962, 680183044, 3394288893, 697481839, 1109578150, 2272036063] key = list(b'SyC10') xor = [2654435769, 1013904242, 3668340011, 2027808484, 387276957] idx = [3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1] for j in range(4, -1, -1): for i in range(31, -1, -1): a = ((s[(i - 1) % 32] >> 3) ^ (s[(i + 1) % 32] << 3)) + ((s[(i - 1) % 32] << 2) ^ (s[(i + 1) % 32] >> 4)) b = (xor[j] ^ s[(i + 1) % 32]) + (key[idx[j * 32 + i]] ^ s[(i - 1) % 32]) s[i % 32] = (s[i % 32] - (a ^ b)) & 0xffffffff print(bytes(s).decode()) ~~~ ## uds 稍微搜一下就可以找到hex文件相关说明:https://blog.csdn.net/qq_42965739/article/details/105163617 直接ida就可以识别文件格式进行反编译,注意STM32要选择ARM小端处理器 观察到最左边函数列表最后一个里调用了TEA函数,往上交叉引用来到一个多个case的函数sub_80043E8,在tea下方还有一个标准rc4 ~~~c int __fastcall sub_80043E8(int a1, int a2, unsigned __int8 *a3, int a4) { int v6; // r0 int result; // r0 int v8; // r0 int v9; // [sp+0h] [bp-20h] BYREF int v10; // [sp+4h] [bp-1Ch] v9 = (int)a3; v10 = a4; switch ( a2 ) { case 0: v6 = *a3; switch ( v6 ) { case 1: return 0; case 2: case 3: if ( *(_BYTE *)(a1 + 8245) ) return 0; else return 51; case 4: return 0; default: return 18; } case 1: sub_8003A3C("got ECUReset request of type %x\n", *a3); v8 = *a3; if ( v8 == 1 || v8 == 3 || v8 == 4 ) return 0; else return 18; case 2: return sub_80024B8(a1, (int (__fastcall **)(int, int, int))a3); case 3: return sub_80024A0(a1, (int)a3); case 4: return 0; case 5: v9 = 0x44332211; v10 = 0x88776655; return (*((int (__fastcall **)(int, int *, int))a3 + 3))(a1, &v9, 8); case 6: v9 = 0x44332211; v10 = 0x88776655; if ( !sub_8104D10(*((_DWORD *)a3 + 1), (int)&v9, *((unsigned __int16 *)a3 + 4)) ) return 53; rc4(*((_DWORD *)a3 + 1), 0x200000A8); return 0; case 7: return sub_81030F4(a1, (int)a3); case 8: if ( *((_WORD *)a3 + 1) != 0x1234 ) return 49; MEMORY[0x20000000] = 1; return 0; case 9: sub_8003A3C("UDS_SRV_EVT_RequestDownload\r\n"); MEMORY[0x20000008] = *(_DWORD *)a3; sub_8000380(MEMORY[0x20000008], 200); result = 0; MEMORY[0x20000004] = 0; return result; case 11: sub_8000332(MEMORY[0x20000008] + MEMORY[0x20000004], *(_DWORD *)a3, *((unsigned __int16 *)a3 + 2)); MEMORY[0x20000004] += *((unsigned __int16 *)a3 + 2); sub_8003A3C("UDS_SRV_EVT_TransferData = %d\r\n", MEMORY[0x20000004]); return 0; case 12: sub_8003A3C("UDS_SRV_EVT_RequestTransferExit\r\n"); return 0; case 13: sub_8003A3C("server session timed out!\n"); nullsub_29(a1); sub_800335C(a1, &off_8104DEC); return 114; case 14: sub_8003A3C("powering down!\n"); sub_8004BCC(*a3); return 114; default: sub_8003A3C("Unhandled event: %d\n", a2); return 17; } } ~~~ 比较关键的是如何定位密文,翻了多篇wp,我总结最佳做法就是翻data数据,本身这种固件就不是很多,可以找一下哪里数据不是很有规律  首先看off_8004EA8交叉引用找到下方函数 ~~~c void __noreturn sub_8000398() { _UNKNOWN **i; // r4 for ( i = &off_8004EA8; i < (_UNKNOWN **)&dword_8004EC8; i += 4 ) ((void (__fastcall *)(_DWORD, void *, void *))((unsigned int)i[3] | 1))(*i, i[1], i[2]); sub_80002A0(); } ~~~ 可以发现i=&off_8004EA8,那么i[3]指向0x810004C(正好是一个函数),*i是dword_8004EC8,i[1]是0x20000000,i[2]是0x194 ~~~c int __fastcall sub_810004C(char *a1, _BYTE *a2, int a3) { _BYTE *v3; // r4 unsigned int v4; // r2 unsigned int v5; // t1 int v6; // r3 int v7; // t1 unsigned int v8; // r2 unsigned int v9; // t1 char v10; // t1 v3 = &a2[a3]; do { v5 = (unsigned __int8)*a1++; v4 = v5; v6 = v5 & 0xF; if ( (v5 & 0xF) == 0 ) { v7 = (unsigned __int8)*a1++; v6 = v7; } v8 = v4 >> 4; if ( !v8 ) { v9 = (unsigned __int8)*a1++; v8 = v9; } while ( --v6 ) { v10 = *a1++; *a2++ = v10; } while ( --v8 ) *a2++ = 0; } while ( a2 < v3 ); return 0; } ~~~ 这个函数是我们熟悉的RLE,第一个参数是要解压的字节,第二个是解压出来的位置,第三个是解压出来的大小 ~~~python def rle_decode_byte(encoded: bytes) -> bytes: ip = 0 decoded = [] while ip < len(encoded): v = encoded[ip] ip += 1 a = v & 0xf if a == 0: a = encoded[ip] if ip < len(encoded): ip += 1 b = v >> 4 if b == 0: b = encoded[ip] if ip < len(encoded): ip += 1 while a-1 != 0: decoded.append(encoded[ip]) ip += 1 a -= 1 while b-1 != 0: decoded.append(0) b -= 1 return bytes(decoded) # 示例 if __name__ == "__main__": sample = bytes.fromhex("01130296880012B014A691FEB9D741AF82CC4EE94747284FD1421052015890D0030090D003021801") db = rle_decode_byte(sample) print(db.hex()) ~~~ 解压得到 ~~~ 00000000000000000000000000000000000088000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014a691feb9d741af82cc4ee94747284fd10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000010000000090d0030090d00300000000010000000000000000000000000000000000000000000000 ~~~ attention!这个开头指向了0x20000000,那么前面`rc4(*((_DWORD *)a3 + 1), 0x200000A8);`中0x200000A8指向的正是上面0xa8开始的数据 结合rc4里检查0字节结尾,我们可以提取出rc4密文 ~~~c unsigned int __fastcall rc4(int a1, int a2) { int v3; // r7 int v4; // r4 unsigned int i; // r6 int v6; // r9 unsigned int result; // r0 rc4_init(a1); LOBYTE(v3) = 0; v4 = 0; for ( i = 0; ; ++i ) { result = sub_810001E(a2); if ( result <= i ) break; v4 = (v4 + 1) % 256; v3 = (unsigned __int8)(*(_BYTE *)(4 * v4 + 0x200035A8) + v3); v6 = *(_DWORD *)(4 * v4 + 0x200035A8); *(_DWORD *)(4 * v4 + 0x200035A8) = *(_DWORD *)(4 * v3 + 0x200035A8); *(_DWORD *)(4 * v3 + 0x200035A8) = v6; *(_BYTE *)(a2 + i) ^= *(_BYTE *)(4 * (unsigned __int8)(*(_BYTE *)(4 * v4 + 0x200035A8) + *(_BYTE *)(4 * v3 + 0x200035A8)) + 0x200035A8); } return result; } unsigned __int8 *__fastcall sub_810001E(unsigned __int8 *a1) { unsigned __int8 *v1; // r2 v1 = a1 + 1; while ( *a1++ ) ; return (unsigned __int8 *)(a1 - v1); } ~~~ 提取密文 ~~~python for i in range(0xa8, len(db)): if db[i] == 0: break print(db[i], end=", ") ~~~ 解密 ~~~python from ctypes import c_uint32 def tea_decrypt(r, v, key, delta): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) total = c_uint32(0xC6EF3720+delta*r) for i in range(r): total.value -= delta v0.value += ((v1.value << 4) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 5) + key[1]) v1.value += ((v0.value << 4) + key[2]) ^ (v0.value + total.value) ^ ((v0.value >> 5) + key[3]) return v0.value, v1.value k = [0x123, 0x4567, 0x89ab, 0xcdef] v = [0x11223344, 0x55667788] delta = 0x61C88647 for i in range(0, len(v), 2): v[i:i+2] = tea_decrypt(32, v[i:i+2], k, delta) # print(list(map(hex, v))) v = b"".join([int.to_bytes(v[i], byteorder='big', length=4) for i in range(len(v))]) print(v) def KSA(key): """ Key-Scheduling Algorithm (KSA) 密钥调度算法""" S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) % 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)) return bytes(res) text = [20, 166, 145, 254, 185, 215, 65, 175, 130, 204, 78, 233, 71, 71, 40, 79, 209] print(len(text)) print(RC4(v, text)) ~~~ 得到`W0L000043MB541337` ## logindemo java层存在大量字符串混淆,解密回填下 ~~~python def dec(s, key): s = list(bytes.fromhex(s)) s[0] ^= 0x7a tmp = s[0] for i in range(1, len(s)): s[i] ^= tmp ^ (key ^ 17) tmp = s[i] return bytes(s) def dec1(s, key): s = list(bytes.fromhex(s)) s[0] ^= 73 tmp = s[0] for i in range(1, len(s)): s[i] ^= tmp ^ (key ^ 15) tmp = s[i] return bytes(s) def dec2(s, key): s = list(bytes.fromhex(s)) s[0] ^= 0x76 tmp = s[0] for i in range(1, len(s)): s[i] ^= tmp ^ (key ^ 0x77) tmp = s[i] return bytes(s) print(dec("19525c1d15434752434257151d4a464f5e425b545c4b43585f1e3d56485e4c585c", 0x4f).decode()) print(dec("1e5f43796251534c5e03145f43", 0x4f).decode()) print(dec("19525c1d154347524342571515565e1d145f43796251534c5e", 0x4f).decode()) print(dec("094c46786953575e5d", 0x4f).decode()) print(dec1("1a050207", 26).decode()) print(dec1("7115151515151515", 26).decode()) print(dec1("210915115f00150e160c0a141c020a14130d0d111d161c1c1c", 26).decode()) print(dec1("324c591a191d520d0d", 26).decode()) print(dec1("6b1b1b47040715110d0803530d0d", 26).decode()) print(dec1("6b4a", 26).decode()) print(dec1("28041509101f170008131454500c0914", 26).decode()) print(dec1("6656161d1b12", 26).decode()) print(dec1("1a2f1b1c1a00141202", 26).decode()) print(dec1("ae66295b7b2a474710425d20686c21524628727b0b665f3c745234714034604b21adc74c257c4b29757b0b665f3c7452347d6818645215", 26).decode()) print(dec2("9019552134421b0b4821394f153f44190d7d1319720b0c48271f522e0355391d59ef6aa3247b331f4c310b48231a572e3f6935095c350a7c173d7d21286d27256c2a1e7d00105f2d0d5c31167ec5ad294a022442cb", 20).decode()) ~~~ ~~~ com.example.myapplication.MESSAGE dex_class.dex com.example.emm.dex_class say_hello SCTF 88888888 http://47.109.106.62:9090 {"name":" ","password":" "} application/json /login Signature 用户已存在!或密码少于6位了或密码错误 未连接网络或服务器关闭!(请直接在平台提交正确的用户名+密码) ~~~ 回填得到 ~~~java package com.example.sctf1; import android.content.Intent; import android.os.Build.VERSION; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import dalvik.system.DexClassLoader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.OpenOption; import java.util.Objects; import okhttp3.Call; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request.Builder; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class LoginActivity extends AppCompatActivity { static final boolean $assertionsDisabled; public static final String EXTRA_MESSAGE; public Button btn_login; public EditText edit_password; public EditText edit_username; public String phone_number; public String secretKey; public String string1; static { LoginActivity.EXTRA_MESSAGE = "com.example.myapplication.MESSAGE"; } public void login(View view) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { InputStream inputStream0 = this.getAssets().open("dex_class.dex"); File dexfile = new File(this.getCacheDir(), "dex_class.dex"); OutputStream outputStream = Build.VERSION.SDK_INT < 26 ? null : Files.newOutputStream(dexfile.toPath(), new OpenOption[0]); byte[] buffer = new byte[0x400]; while(true) { int v = inputStream0.read(buffer); if(v <= 0) { goto label_10; } if(outputStream == null) { break; } outputStream.write(buffer, 0, v); } throw new AssertionError(); label_10: inputStream0.close(); if(outputStream != null) { outputStream.close(); Class class0 = new DexClassLoader(dexfile.getAbsolutePath(), this.getCacheDir().getAbsolutePath(), null, this.getClassLoader()).loadClass("com.example.emm.dex_class"); Object object0 = class0.newInstance(); Method method0 = class0.getDeclaredMethod("say_hello", String.class); method0.setAccessible(true); this.btn_login = (Button)this.findViewById(id.btn_login); this.btn_login.setOnClickListener((View v) -> new Thread(new com.example.sctf1.LoginActivity.1(this, method0, object0)).start()); return; } throw new AssertionError(); class com.example.sctf1.LoginActivity.1 implements Runnable { com.example.sctf1.LoginActivity.1(Method method0, Object object0) { } @Override public void run() { LoginActivity.this.edit_username = (EditText)LoginActivity.this.findViewById(id.edit_username); LoginActivity.this.edit_password = (EditText)LoginActivity.this.findViewById(id.edit_password); String s = LoginActivity.this.edit_username.getText().toString(); String s1 = "SCTF"; String s2 = LoginActivity.this.edit_password.getText().toString(); String s3 = "88888888"; LoginActivity.this.phone_number = Getstr.getNothing(LoginActivity.transform(GoodCard.anything((s + s2)))); try { LoginActivity.this.secretKey = Objects.requireNonNull(this.val$method.invoke(this.val$dexlib_obj, LoginActivity.this.phone_number)).toString(); } catch(IllegalAccessException | InvocationTargetException illegalAccessException0) { throw new RuntimeException(illegalAccessException0); } String s4 = "http://47.109.106.62:9090"; String s5 = "{\"name\":\"" + "\",\"password\":\"" + s3 + "\"}"; String s6 = SignatureGenerator.generateSignature((LoginActivity.this.secretKey + (s4 + s5))); RequestBody requestBody0 = RequestBody.create(MediaType.parse("application/json"), s5); Request request0 = new Builder().url(s4 + "/login").post(requestBody0).addHeader("Signature", s6).build(); Call call0 = new OkHttpClient().newCall(request0); Response response = null; try { try { response = call0.execute(); LoginActivity.this.string1 = response.body().string(); if(response.isSuccessful()) { Intent intent = new Intent(LoginActivity.this, SecondActivity.class); LoginActivity.this.startActivity(intent); } else { String s7 = "用户已存在!或密码少于6位了或密码错误"; Toast.makeText(LoginActivity.this, s7, 0).show(); goto label_37; label_28: exception0.printStackTrace(); com.example.sctf1.LoginActivity.1.1 loginActivity$1$10 = new Runnable() { @Override public void run() { if(LoginActivity.this.string1 == null) { LoginActivity.this.string1 = "未连接网络或服务器关闭!(请直接在平台提交正确的用户名+密码)"; } Toast.makeText(LoginActivity.this, LoginActivity.this.string1, 0).show(); } }; LoginActivity.this.runOnUiThread(loginActivity$1$10); if(response != null) { goto label_38; } return; } goto label_37; } catch(Throwable throwable0) { } } catch(Exception exception0) { goto label_28; } if(response != null) { response.close(); } throw throwable0; label_37: if(response != null) { label_38: response.close(); } } } } @Override // androidx.fragment.app.FragmentActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(layout.activity_login); } public static String transform(String input) { StringBuilder result = new StringBuilder(); char[] arr_c = input.toCharArray(); for(int v = 0; v < arr_c.length; ++v) { result.append(arr_c[v] * 100); } return result.toString(); } } ~~~ 加密逻辑为`GoodCard.anything` -> `LoginActivity.transform` -> `Getstr.getNothing` -> `com.example.emm.dex_class.say_hello` 逆向开始,第一个很明显做了个循环异或 ~~~java public String say_hello(String original) { return dex_class.xorEncryptDecrypt(original, "S0C0Z0Y0W"); } public static String xorEncryptDecrypt(String input, String key) { StringBuilder output = new StringBuilder(); for(int i = 0; i < input.length(); ++i) { output.append(((char)(input.charAt(i) ^ key.charAt(i % key.length())))); } return output.toString(); } ~~~ 第二个Getstr.getNothing在so里,有点复杂直接hook看下 ~~~ Getstr.getNothing is called: str=9800970098009700980097009800970098009700980097009800970098009700 Getstr.getNothing result=5045094413585467707853882780759531095633013117065857519178684771275364807413082472406228120229563489108632252785636645670872698819224972890047487581811332254680241130547827260310260156274356362590066709127261486610962436177824392026615913358088949515401041129200535045603908296850986535932167342047836952081593168351978891009244883210600480512457372948532264723914384241781541613968653412830343762347260625142972051715030907876003790185293489419779271045262824394366532040730447855614903756780018616574288028378761167000741451345026574342587808243860484045368480622683879039748064603874478923277460189909112074820466 Getstr.getNothing is called: str=9800970098009700980097009800970098009700980097009800970098009700 Getstr.getNothing result=5045094413585467707853882780759531095633013117065857519178684771275364807413082472406228120229563489108632252785636645670872698819224972890047487581811332254680241130547827260310260156274356362590066709127261486610962436177824392026615913358088949515401041129200535045603908296850986535932167342047836952081593168351978891009244883210600480512457372948532264723914384241781541613968653412830343762347260625142972051715030907876003790185293489419779271045262824394366532040730447855614903756780018616574288028378761167000741451345026574342587808243860484045368480622683879039748064603874478923277460189909112074820466 Getstr.getNothing is called: str=9800970098009700980097009800970098009700980097009800970098009700 Getstr.getNothing result=5045094413585467707853882780759531095633013117065857519178684771275364807413082472406228120229563489108632252785636645670872698819224972890047487581811332254680241130547827260310260156274356362590066709127261486610962436177824392026615913358088949515401041129200535045603908296850986535932167342047836952081593168351978891009244883210600480512457372948532264723914384241781541613968653412830343762347260625142972051715030907876003790185293489419779271045262824394366532040730447855614903756780018616574288028378761167000741451345026574342587808243860484045368480622683879039748064603874478923277460189909112074820466 Getstr.getNothing is called: str=98009700 Getstr.getNothing result=2233501535873032438823121598204299505434529114931768813605420613081008238848241351384976360380040680898472734338437713527212987244461988210103109572904067199730297662538130740444958693897705543982767205853162434482670047472191376691771728478745450434840193243358407260246576449832102844937592858139016490868660365496774265956046594638227737863254485349509678643684737220238028223704498869369480140313776541597313319284504066160649959820155991542479147585714027746619950684145658878413672779565168489187038976612610030868825988176731507778054179818006517603863182124465810106800361331579499382340133899622159252658860 Getstr.getNothing is called: str=9700 Getstr.getNothing result=13904806999322946098738724391470420861218239226803900906903126921611226597677267491411725201453899947887238728859042520601315993658447283397644650078927695405554954717254946789808519906028094494520846898353464681798834920500140475141818409571951479007870285426630324534583299906745841755480715970390702130537479135132978815683732301317073599115508986658255646557196880732715768178790334868529001059725119336388131629576390739424170336637855522394851298127307107888633456592938110815639203198941093690346266870396632427968296171952101897929685083237976646419550108479566471911042499480157215212085802929831244889653802 ~~~ 测试了几轮发现输出长度相同,而且非常大是个数字字符串,猜测类似RSA 在so里搜索找到了三处数字 ~~~ 106697219132480173106064317148705638676529121742557567770857687729397446898790451577487723991083173010242416863238099716044775658681981821407922722052778958942891831033512463262741053961681512908218003840408526915629689432111480588966800949428079015682624591636010678691927285321708935076221951173426894836169 144819424465842307806353672547344125290716753535239658417883828941232509622838692761917211806963011168822281666033695157426515864265527046213326145174398018859056439431422867957079149967592078894410082695714160599647180947207504108618794637872261572262805565517756922288320779308895819726074229154002310375209 114514114514114514114514114514114514114514114514 ~~~ 前两个都是质数,第三个不清楚,尝试rsa解密发现解出来的对应 ~~~python import gmpy2 # 已知参数 p = 106697219132480173106064317148705638676529121742557567770857687729397446898790451577487723991083173010242416863238099716044775658681981821407922722052778958942891831033512463262741053961681512908218003840408526915629689432111480588966800949428079015682624591636010678691927285321708935076221951173426894836169 q = 144819424465842307806353672547344125290716753535239658417883828941232509622838692761917211806963011168822281666033695157426515864265527046213326145174398018859056439431422867957079149967592078894410082695714160599647180947207504108618794637872261572262805565517756922288320779308895819726074229154002310375209 e = 65537 c = 13904806999322946098738724391470420861218239226803900906903126921611226597677267491411725201453899947887238728859042520601315993658447283397644650078927695405554954717254946789808519906028094494520846898353464681798834920500140475141818409571951479007870285426630324534583299906745841755480715970390702130537479135132978815683732301317073599115508986658255646557196880732715768178790334868529001059725119336388131629576390739424170336637855522394851298127307107888633456592938110815639203198941093690346266870396632427968296171952101897929685083237976646419550108479566471911042499480157215212085802929831244889653802 # 计算模数 n 和 φ(n) n = p * q phi = (p - 1) * (q - 1) # 计算私钥 d d = gmpy2.invert(e, phi) # 解密密文得到明文 m = pow(c, d, n) print(m) # 9700 ~~~ 第三处直接按照00划分字符串即可,肉眼就能分开了 ~~~java public static String transform(String input) { StringBuilder result = new StringBuilder(); char[] arr_c = input.toCharArray(); for(int v = 0; v < arr_c.length; ++v) { result.append(arr_c[v] * 100); } return result.toString(); } ~~~ 最后一个ai分析就是把后一半字符串和前一半字符串交叉拼接 ~~~java package com.example.sctf1; public class GoodCard { public static String anything(String str) { char[] arr_c = str.toCharArray(); int to = arr_c.length - 1; GoodCard.perfect(arr_c, 0, to, (to + 1) / 2); return String.valueOf(arr_c); } public static void circle(char[] a, int from, int i, int n2) { for(int k = i * 2 % n2; k != i; k = k * 2 % n2) { char temp = a[i + from]; a[i + from] = a[k + from]; a[k + from] = temp; } } public static void perfect(char[] a, int from, int to, int n) { if(from >= to) { return; } if(from == to - 1) { MyMath.swap(a, from, to); return; } int k = 0; int p = n * 2 + 1; int k_3; for(k_3 = 1; k <= p / 3; k_3 *= 3) { ++k; p /= 3; } int m = (k_3 - 1) / 2; GoodCard.rightCircle(a, from + m, from + n + m - 1, m); int i = 0; for(int t = 1; i < k; t *= 3) { GoodCard.circle(a, from - 1, t, m * 2 + 1); ++i; } GoodCard.perfect(a, m * 2 + from, to, (to - (m * 2 + from) + 1) / 2); } public static void reverse(char[] a, int from, int to) { while(from < to) { MyMath.swap(a, from, to); ++from; --to; } } public static void rightCircle(char[] a, int from, int to, int n) { int m = n % (to - from + 1); GoodCard.reverse(a, to - m + 1, to); GoodCard.reverse(a, from, to - m); GoodCard.reverse(a, from, to); } } ~~~ 解密脚本如下 ~~~python from base64 import b64decode import gmpy2 secretkey = list(b64decode(b"YgNxAGMDawJjZQR6B2IGYANiYQVxAG8JYQhkawZ3AW8JagluYAN2Am4GbQRmZAR6AWwBbQNlZANxCWsCaAlhZQRxAm4CbgRkZgl1AWIIawhmYAh3B2MBaAJmaghwAWoEbwliYgBwCWkIbQNuawlyAW0FYAhuYgB1Am8CawRuYAJxCG8CbgRgZAF3BWgJYQlhZwFzAGsJbwFlagV0CW0FawhgawNyCGkAbQNjYQh3Bm4FbwNkYAJ6A2IDaANmYwl2A2sEaQRlaglyAm0HaARmYQZ7AWsIbwZhYwB0B2sIYAlvYgN2AWsHYQJiZAByAGgDYANmYQB0BmwJYABhZwl1CGkEaQlmZQJzBm0IYQdjYwh6A20BbwVhZQl3CGoIaABmYAN1AW0IbQFvYAN0B2gFYAVhZAF1BW8HYQZvagZ6A20CaAhjZAl6BWkCYQhvZAN1AGgDaQJhYQZzCW8BYABjZAN0AW8BbQViZwVyA28JagJiagd1CGgDawhvYQJwB28GagNvYwd6AG0BYQFuawVyBGoBaQFkZAV1A24HbgdlYgd1AWwIYQNkagV6BW0DbglvZAl0A2kFaAhjawB3AmwAbwZgZwV3BGMEaQllYAhzAGsEYAFhYAJ6Bm4GaQdkagl6A20DawdkZgd3BW0JbgRgYQZyB2kJbANnZgdwA2gFaQFnYAh7BW4GbAdhYgd7Bm4DawJmawN0CWMDbgJgYwh0AmwGYQRiYQN2BmIGaAVkYwh2BmsDbAFhaglzB2kAbQBjZwJ1BGkCaghiZgByAWkJaAhhagZ3AG0JaghiYQVwAmMIaAFjYgVzBGh0dHA6Ly80Ny4xMDkuMTA2LjYyOjkwOTB7Im5hbWUiOiJTQ1RGIiwicGFzc3dvcmQiOiI4ODg4ODg4OCJ9").split(b"http")[0]) xor = b"S0C0Z0Y0W" for i in range(len(secretkey)): secretkey[i] ^= xor[i%len(xor)] secretkey = int(bytes(secretkey).decode()) # 已知参数 p = 106697219132480173106064317148705638676529121742557567770857687729397446898790451577487723991083173010242416863238099716044775658681981821407922722052778958942891831033512463262741053961681512908218003840408526915629689432111480588966800949428079015682624591636010678691927285321708935076221951173426894836169 q = 144819424465842307806353672547344125290716753535239658417883828941232509622838692761917211806963011168822281666033695157426515864265527046213326145174398018859056439431422867957079149967592078894410082695714160599647180947207504108618794637872261572262805565517756922288320779308895819726074229154002310375209 e = 65537 # 计算模数 n 和 φ(n) n = p * q phi = (p - 1) * (q - 1) # 计算私钥 d d = gmpy2.invert(e, phi) # 解密密文得到明文 m = str(pow(secretkey, d, n)) m = list(map(int, m.split("00")[:-1])) print(len(m), bytes(m).decode()) for i in range(len(m)//2): print(chr(m[i*2+1])+chr(m[i*2]), end="") print(chr(m[-1])) ~~~ 得到`wysth7mu5j6hg` ## SGAME 字符串里发现了Lua5.4,十之八九是常见的魔改lua 主函数存在大量字符串解密,可以结合动调找逻辑,找到了open game文件、ptrace反调试(检测到调试就反转key)、对game内容rc4解密操作(解密完可以看出来大概是luac文件,但是头部魔改成了ELF,同时还有其他地方可能是opcode做了修改,因为没法反编译) 全网没啥wp,烧卖写的很简略、sekai写的又看不懂 但是基本能确定魔改,那么编译一份lua5.4(注意要去src里makefile把优化关掉,不然优化过的函数对不上),然后用xia0ji神的luminia push了一份符号表,再回到SGAME pull可以发现基本全部还原(可以在上一个bindiff就基本全还原,0.9 sim+0.9 con),没还原的可以确定是魔改了 魔改的点一个个来看 1. 删除了临时文件的创建,估计是为了防止非预期 ~~~c __int64 __fastcall sub_5555555837F9(__int64 a1) { int v1; // ecx int v2; // r8d int v3; // r9d char s[24]; // [rsp+20h] [rbp-20h] BYREF unsigned __int64 v6; // [rsp+38h] [rbp-8h] v6 = __readfsqword(0x28u); if ( !pclose(s) ) luaL_error( a1, (unsigned int)"unable to generate a unique filename", (unsigned int)"unable to generate a unique filename", v1, v2, v3); lua_pushstring(a1, (__int64)s); return 1LL; } ~~~ ~~~c __int64 __fastcall os_tmpname(__int64 a1) { int v1; // ecx int v2; // r8d int v3; // r9d int fd; // [rsp+1Ch] [rbp-34h] char templatea[40]; // [rsp+20h] [rbp-30h] BYREF unsigned __int64 v7; // [rsp+48h] [rbp-8h] v7 = __readfsqword(0x28u); strcpy(templatea, "/tmp/lua_XXXXXX"); fd = mkstemp64(templatea); if ( fd == -1 ) luaL_error( a1, (unsigned int)"unable to generate a unique filename", (unsigned int)"unable to generate a unique filename", v1, v2, v3); close(fd); lua_pushstring(a1, templatea); return 1LL; } ~~~ 2. luaK_storevar下面的数字魔改  3. loadString ~~~c __int64 __fastcall loadString(__int64 a1, __int64 a2) { __int64 StringN; // [rsp+18h] [rbp-8h] StringN = loadStringN(a1, a2); if ( !StringN ) error(a1, "bad format for constant string"); return StringN; } ~~~ ~~~c __int64 __fastcall sub_555555594965(__int64 *a1, __int64 a2) { __int64 v2; // rcx int v3; // r8d int v4; // r9d unsigned __int64 v5; // rax unsigned __int64 i; // [rsp+10h] [rbp-20h] __int64 v8; // [rsp+18h] [rbp-18h] v8 = loadStringN(a1, a2); if ( !v8 ) error((__int64)a1, (int)"bad format for constant string", (__int64)"bad format for constant string", v2, v3, v4); if ( *(_BYTE *)(v8 + 11) == 0xFF ) v5 = *(_QWORD *)(v8 + 16); else v5 = *(unsigned __int8 *)(v8 + 11); for ( i = 0LL; i < v5; ++i ) *(_BYTE *)(v8 + 24 + i) = *(_BYTE *)(v8 + 24 + i); return v8; } ~~~ 4. io_popen,这里应该是不再像原来lua那样读取参数文件,而是直接打开硬编码的game文件 5. dumpString,貌似多加了一处复制 ~~~c void __fastcall sub_555555575E39(__int64 a1, __int64 a2) { size_t v2; // rax size_t i; // [rsp+10h] [rbp-20h] size_t size; // [rsp+18h] [rbp-18h] _BYTE *ptr; // [rsp+28h] [rbp-8h] if ( a2 ) { if ( *(_BYTE *)(a2 + 11) == 0xFF ) v2 = *(_QWORD *)(a2 + 16); else v2 = *(unsigned __int8 *)(a2 + 11); size = v2; ptr = malloc(v2); for ( i = 0LL; i < size; ++i ) ptr[i] = *(_BYTE *)(a2 + 24 + i); dumpSize(a1, size + 1); dumpBlock(a1, ptr, size); free(ptr); } else { dumpSize(a1, 0LL); } } ~~~ 6. luaG_traceexec里的luaP_opmodes不一样 7. funcnamefromcode没太看懂 8. checkHeader(开头从Lua变成了LF\x7F) 9. luaU_undump和f_parser(第一字节从0x1b变为E) 10. luaV_execute魔改了字节case 其中最后一个魔改是最关键的,Lua5.4的源码相比前几版做了改进,通过计算偏移来实现case跳转而非以前的直接switch-case,这也导致很难区分case 但是经过观察发现可以借助它的偏移来计算opcode位置,具体如下:off_5555555B3520是地址集合,而rax是偏移值  正好对应源码如下disptab_0  我们直接猜测出题人没有魔改代码(也就是说排完序后的地址魔改的和源码都是对应的),只是修改了原本的case值,那么可以借助跳转地址在所有地址(已排好序)中的位置来确定opcode映射 ~~~python game_case = [0x0000555555599387, 0x000055555559959F, 0x0000555555599867, 0x0000555555599AC1, 0x0000555555599CD6, 0x0000555555599F8C, 0x000055555559A2F2, 0x000055555559A5EC, 0x000055555559A89F, 0x000055555559AAB3, 0x000055555559AD3E, 0x000055555559AEF8, 0x000055555559B178, 0x000055555559B3F8, 0x000055555559B679, 0x000055555559B94C, 0x000055555559BB94, 0x000055555559BD90, 0x000055555559C05B, 0x000055555559C1F2, 0x000055555559C389, 0x000055555559C520, 0x000055555559C6A4, 0x000055555559C825, 0x000055555559CAA5, 0x000055555559CD25, 0x000055555559CFA6, 0x000055555559D279, 0x000055555559D4C1, 0x000055555559D6BD, 0x000055555559D988, 0x000055555559DB69, 0x000055555559DD4A, 0x000055555559E114, 0x000055555559DF2B, 0x000055555559E2F7, 0x000055555559E474, 0x000055555559E5FE, 0x000055555559E797, 0x000055555559E9BE, 0x000055555559EB7C, 0x000055555559EC8B, 0x000055555559EDAB, 0x000055555559EF24, 0x000055555559F029, 0x000055555559F117, 0x000055555559F1CE, 0x000055555559F35D, 0x000055555559F598, 0x000055555559F7D3, 0x000055555559F925, 0x000055555559FAC5, 0x000055555559FCEB, 0x000055555559FF11, 0x00005555555A0137, 0x0000555555598908, 0x00005555555989FB, 0x0000555555598AD4, 0x0000555555598BB6, 0x0000555555598CAD, 0x0000555555598DCF, 0x0000555555598E87, 0x0000555555598F47, 0x0000555555598FFF, 0x00005555555990E9, 0x0000555555599204, 0x00005555555A035D, 0x00005555555A049E, 0x00005555555A064D, 0x00005555555A07B4, 0x00005555555A0945, 0x00005555555A0B28, 0x00005555555A0C36, 0x00005555555A0E00, 0x00005555555A0FF3, 0x00005555555A10FF, 0x00005555555A119D, 0x00005555555A12AC, 0x00005555555A13D8, 0x00005555555A1663, 0x00005555555A17F6, 0x00005555555A1910, 0x00005555555A1A37] sorted_game_case = sorted(game_case) game_case_id = [sorted_game_case.index(i) for i in game_case] real_case = [0x0000000000026576, 0x0000000000026669, 0x0000000000026742, 0x0000000000026824, 0x000000000002691B, 0x0000000000026A3B, 0x0000000000026AF3, 0x0000000000026BB3, 0x0000000000026C6B, 0x0000000000026D55, 0x0000000000026E70, 0x0000000000026FF3, 0x000000000002720B, 0x00000000000274D3, 0x000000000002772E, 0x0000000000027943, 0x0000000000027BF9, 0x0000000000027F5F, 0x000000000002825A, 0x000000000002850D, 0x000000000002871F, 0x00000000000289AA, 0x0000000000028B64, 0x0000000000028DE4, 0x0000000000029061, 0x00000000000292E2, 0x00000000000295B5, 0x00000000000297FD, 0x00000000000299F9, 0x0000000000029CC4, 0x0000000000029E5B, 0x0000000000029FF2, 0x000000000002A189, 0x000000000002A30D, 0x000000000002A48E, 0x000000000002A70E, 0x000000000002A98B, 0x000000000002AC0C, 0x000000000002AEDF, 0x000000000002B127, 0x000000000002B323, 0x000000000002B5EE, 0x000000000002B7CF, 0x000000000002B9B0, 0x000000000002BD7A, 0x000000000002BB91, 0x000000000002BF5D, 0x000000000002C0DD, 0x000000000002C26A, 0x000000000002C406, 0x000000000002C62D, 0x000000000002C7EB, 0x000000000002C8FA, 0x000000000002CA1A, 0x000000000002CB93, 0x000000000002CC98, 0x000000000002CD86, 0x000000000002CE38, 0x000000000002CFC5, 0x000000000002D1FE, 0x000000000002D437, 0x000000000002D587, 0x000000000002D725, 0x000000000002D949, 0x000000000002DB6D, 0x000000000002DD91, 0x000000000002DFB5, 0x000000000002E0F4, 0x000000000002E2A1, 0x000000000002E408, 0x000000000002E59C, 0x000000000002E785, 0x000000000002E896, 0x000000000002EA66, 0x000000000002EC59, 0x000000000002ED65, 0x000000000002EE06, 0x000000000002EF1B, 0x000000000002F047, 0x000000000002F2D0, 0x000000000002F464, 0x000000000002F57E, 0x000000000002F6A5] sorted_real_case = sorted(real_case) real_case_id = [sorted_real_case.index(i) for i in real_case] print(game_case_id) print(real_case_id) ~~~ 值如下,很明显SGAME把0-10对应的opcode往后移动了 ~~~ [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82] ~~~ 可以直接修改unluac的,但我不知道怎么改了,官方wp也没了,网上的又没学会呜呜 ## ezgo 拿到题目后不管是调试还是跑都会报错,先去看主函数逻辑 去符号的go一般拉到最底下找主函数 ~~~c __int64 __golang sub_4B1680(__int64 a1, char **a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) ) LABEL_8: sub_46AC20(a1, (int)a2, a3, a4, a5, a6, a7, a8, a9, v35, v38); v10 = qword_594118; if ( !qword_594118 ) { a2 = &off_4FEA28; LODWORD(a1) = runtime_gopanic((unsigned int)&RTYPE_string, (unsigned int)&off_4FEA28, a3, a4, a5, a6, a7, a8, a9); goto LABEL_8; } v11 = sub_4A5C60(qword_594110, qword_594118, (unsigned int)&unk_4FE8D0, 1, 0, -1, a7, a8, a9); v17 = strings_Join(v11, v10, v12, 0, 0, v13, v14, v15, v16); v46 = sub_4B09E0(v17, v10, v18, 0, 0, v19, v20, v21, v22); v45 = v23; runtime_chanrecv1(qword_593F08, 0LL); v24 = (int)off_58F1C0; // 密文 if ( cmp((unsigned int)&off_4FF360, v46, v10, v45, (_DWORD)off_58F1C0, qword_58F1C8, qword_58F1D0, v25, v26, v35, v38) ) { v47[0] = &RTYPE_string; v47[1] = &off_4FEA38; return fmt_Fprintln((unsigned int)off_4FF328, qword_593F38, (unsigned int)v47, 1, 1, v28, v29, v30, v31, v36, v39); } else { v48[0] = &RTYPE_string; v48[1] = runtime_convTstring(qword_594110, qword_594118, v27, v45, v24, v28, v29, v30, v31, v36, v39); return fmt_Fprintf( (unsigned int)off_4FF328, qword_593F38, (unsigned int)"congratulations, the flag is syclover{%s}\n", 42, (unsigned int)v48, 1, 1, v32, v33, v37, v40, v41, v42, v43, v44); } } ~~~ > 这个题目学到了个招数,出现`call analysis failed`时候在出问题的call函数调用上按下Y键,删除函数类型定义,此时当前函数就可以正确反编译,参考来自https://hex-rays.com/blog/igors-tip-of-the-week-148-fixing-call-analysis-failed > > 这个问题主要原因是ida没有正确识别传参导致的 > > 如果删除定义还不对,那就根据实际传参数目减少定义里的数目(对这道题go来说) 关注到sub_4B1520函数里出现了flag字符串,调用了flag__ptr_FlagSet_Var函数,查看可知flag是个参数 ~~~ (base) xxx:/mnt/d/ida9/dbgsrv$ ./sycgogogo -flag flag needs an argument: -flag Usage of ./sycgogogo: -flag string your flag ~~~ 随便输一些字符发现有长度要求 ~~~ (base) xxx:/mnt/d/ida9/dbgsrv$ ./sycgogogo -flag aaaa panic: runtime error: slice bounds out of range [:16] with length 4 ~~~ 16?再结合密文64字节猜测是16倍数,输入64字节此时不在报错,而是返回GG(flag不对) ~~~ (base) xxx:/mnt/d/ida9/dbgsrv$ ./sycgogogo -flag aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa GG... ~~~ 在ida debugger选项中添加参数`-flag aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`进行调试,然后发现跑不起来,还没搞懂原因反正接收了一堆异常,调试非常艰难,中间报错被杀死很多次 sub_4B1520中发现注册了6个goroutine ~~~c v18[0] = off_4E00B0; v18[1] = off_4E00B8; v18[2] = off_4E00C0; v18[3] = off_4E00A8; v18[4] = &off_4E00C8; v18[5] = &off_4E00D0; for ( i = 0; i < 6; i = v17 + 1 ) { v17 = i; sub_43EF20(v18[i], a2, v18[i], a4, a5, a6, a7, a8, (_DWORD)a9); } ~~~ ~~~ .rodata:00000000004E00A8 off_4E00A8 dq offset sub_4AEA40 ; DATA XREF: sub_4B1520+36↑o .rodata:00000000004E00B0 off_4E00B0 dq offset sub_4AECE0 ; DATA XREF: sub_4B1520+12↑o .rodata:00000000004E00B8 off_4E00B8 dq offset sub_4AEE40 ; DATA XREF: sub_4B1520+1E↑o .rodata:00000000004E00C0 off_4E00C0 dq offset sub_4AEFC0 ; DATA XREF: sub_4B1520+2A↑o .rodata:00000000004E00C8 off_4E00C8 dq offset sub_4AF220 ; DATA XREF: sub_4B1520+42↑o .rodata:00000000004E00D0 off_4E00D0 dq offset sub_4AF2C0 ; DATA XREF: sub_4B1520+4E↑o ~~~ 第一个敏感的话能发现sub_4892C0调用的正是sys_ptrace(101),根据结果如果没有反调试就调用sub_4AE9C0,里面先对RC4密钥做移位处理(先全部逆序、再后12位逆序、最后前四位逆序),然后RC4加密了密文(sub_4AE9C0) ~~~c __int64 __golang sub_4AECE0( __int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9) { __int64 v9; // r14 __int64 v10; // rax __int64 v11; // rcx __int64 v12; // r8 __int64 v13; // r9 __int64 v14; // r10 __int64 v15; // r11 _BYTE v17[8]; // [rsp+0h] [rbp-8h] BYREF _UNKNOWN *retaddr; // [rsp+8h] [rbp+0h] BYREF if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) ) sub_46AC20(a1, a2); runtime_chanrecv1(qword_593EF8, 0); v10 = sub_4892C0(101, 0, 0, 0); if ( v11 != 1 ) sub_4AE9C0(v10, 0, v11, 0, a5, v12, v13, v14, v15); runtime_chansend1(qword_593F08, v17); return runtime_chansend1(qword_593F00, v17); } void __golang sub_4AE9C0( __int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9) { __int64 v9; // r14 int v10; // r8d int v11; // r9d int v12; // r10d int v13; // r11d rc4_Cipher *v14; // rax int v15; // ecx int v16; // r8d int v17; // r9d int v18; // r10d int v19; // r11d int v20; // eax __int64 v21; // [rsp-40h] [rbp-40h] char v22; // [rsp-38h] [rbp-38h] _UNKNOWN *retaddr; // [rsp+0h] [rbp+0h] BYREF if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) ) LABEL_5: sub_46AC20(a1, a2, a3, a4, a5, a6, a7, a8, a9, v21, v22); sub_4AE860(a1, a2, a3, a4, a5, a6, a7, a8, a9); a2 = qword_58F1A8; v14 = (rc4_Cipher *)crypto_rc4_NewCipher((_DWORD)rc4_key, qword_58F1A8, qword_58F1B0, a4, a5, v10, v11, v12, v13); if ( a2 ) { v20 = *(_QWORD *)(a2 + 8); LODWORD(a2) = v15; LODWORD(a1) = runtime_gopanic(v20, v15, v15, a4, a5, v16, v17, v18, v19); goto LABEL_5; } crypto_rc4__ptr_Cipher_XORKeyStream(v14, *(_slice_uint8 *)cipher, *(_slice_uint8 *)cipher); } unsigned __int64 __golang sub_4AE860( __int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8) { unsigned __int64 v8; // rcx char *v9; // rbx __int64 v10; // rsi unsigned __int64 i; // rax char *v12; // rbx unsigned __int64 v13; // rsi __int64 v14; // rdi unsigned __int64 j; // rax char v16; // r8 char *v17; // rdx unsigned __int64 result; // rax unsigned __int64 k; // rcx char v20; // bl v8 = qword_58F1A8; v9 = rc4_key; v10 = qword_58F1A8 - 1; for ( i = 0; v10 > (__int64)i; ++i ) { if ( i >= v8 ) sub_46CD40(i); a4 = (unsigned __int8)v9[i]; if ( v10 >= v8 ) sub_46CD40(v10); a6 = (unsigned __int8)v9[v10]; v9[i] = a6; v9[v10--] = a4; } if ( (unsigned __int64)qword_58F1A8 < 4 ) sub_46CE00(4, v9, qword_58F1A8, a4, v10, a6, a7, a8); v12 = &rc4_key[((4 - qword_58F1B0) >> 63) & 4]; v13 = qword_58F1A8 - 4; v14 = qword_58F1A8 - 5; for ( j = 0; v14 > (__int64)j; ++j ) { if ( j >= v13 ) sub_46CD40(j); v16 = v12[j]; if ( v14 >= v13 ) sub_46CD40(v14); v12[j] = v12[v14]; v12[v14--] = v16; } if ( (unsigned __int64)qword_58F1B0 < 4 ) sub_46CDC0(j, v12, 4); v17 = rc4_key; result = 0; for ( k = 3; (__int64)k > (__int64)result; --k ) { if ( result >= 4 ) sub_46CD40(result); v20 = v17[result]; if ( k >= 4 ) sub_46CD40(k); v17[result] = v17[k]; v17[k] = v20; ++result; } return result; } ~~~ 第二个反编译不完整,可以nop掉sub_472F80后面的三行汇编得到完整代码,循环扫描/proc下的所有条目获取pid,然后进而获取对应的进程名,检查是否等于`linux_server`或者`linux_server64`,检测到立即退出,反之做一次RC4加密密文(sub_4AE9C0) ~~~c __int64 __golang sub_4AEE40(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v5 + 16) ) sub_46AC20(a1, a2); runtime_chanrecv1(qword_593EE8, 0); v6 = 5; v11 = sub_4AC520((__int64)"/proc", 5, (unsigned int)off_4E0148, a4, a5, v7, v8, v9, v10); if ( a4 ) return sub_4AF3E0(v11, v6); while ( v6 > 0 ) { v28 = v11; v18 = (char *)(*(__int64 (__golang **)(_QWORD))(*(_QWORD *)v11 + 48LL))(*(_QWORD *)(v11 + 8)); v24 = sub_472F80(v18, v6, v19, a4, a5, v20, v21, v22, v23); v11 = sub_4AED60(v24, v6, v25, a4, a5); if ( v26 ) return sub_4AF3E0(v11, v6); if ( v6 == 12 ) { v12 = 'es_xunil'; if ( *(_QWORD *)v11 == 'es_xunil' && *(_DWORD *)(v11 + 8) == 'revr' ) return sub_4AF3E0(v11, v6); } else { v12 = 'es_xunil'; } if ( v6 == 14 && *(_QWORD *)v11 == 'es_xunil' && *(_DWORD *)(v11 + 8) == 'revr' && *(_WORD *)(v11 + 12) == '46' ) return sub_4AF3E0(v11, v6); v11 = v28 + 16; --v6; } sub_4AE9C0(v11, v6, v12, a4, a5, v13, v14, v15, v16); return sub_4AF3E0(v27, v6); } __int64 __golang sub_472F80(char *a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, int a6, int a7, int a8, int a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) ) sub_46AC20((__int64)a1, a2); if ( a2 && a2 < 19 ) { v10 = *a1; if ( *a1 == 45 || v10 == 43 ) { v11 = a2 - 1; v12 = (__int64)&a1[((1 - a2) >> 63) & 1]; if ( a2 == 1 ) { v44 = sub_461660(1, 0, 0, v11, v12, a6, a7, a8, a9); v13 = sub_46D6C0(v44, a1, 1); if ( !v44 ) unknown_libname_23(v13); p_strconv_NumError = (strconv_NumError *)runtime_newobject(&RTYPE_strconv_NumError); p_strconv_NumError->Func.len = 4; p_strconv_NumError->Func.ptr = "Atoi"; v18 = a2; p_strconv_NumError->Num.len = a2; if ( dword_5B41E0 ) { p_strconv_NumError = (strconv_NumError *)sub_46C980( (__int64)p_strconv_NumError, (__int64)a1, a2, a2, v12, v15, v16, v17); v19 = (char *)v44; *v20 = v44; } else { v19 = (char *)v44; } p_strconv_NumError->Num.ptr = v19; v21 = off_58EDB8[0]; p_strconv_NumError->Err.tab = off_58EDB0; if ( dword_5B41E0 ) { p_strconv_NumError = (strconv_NumError *)sub_46C980( (__int64)p_strconv_NumError, (__int64)a1, v18, a2, (__int64)v21, v15, v16, v17); *v22 = v21; } p_strconv_NumError->Err.data = v21; return 0; } } else { v11 = a2; v12 = (__int64)a1; } v24 = 0; v25 = 0; while ( v11 > v24 ) { v32 = *(unsigned __int8 *)(v12 + v24) - 48; if ( (unsigned __int8)(*(_BYTE *)(v12 + v24) - 48) > 9u ) { v45 = sub_461660(a2, 0, 0, v11, v12, v25, v32, a8, a9); v33 = sub_46D6C0(v45, a1, a2); if ( a2 > (unsigned __int64)-v45 ) unknown_libname_23(v33); v34 = (strconv_NumError *)runtime_newobject(&RTYPE_strconv_NumError); v34->Func.len = 4; v34->Func.ptr = "Atoi"; v38 = a2; v34->Num.len = a2; if ( dword_5B41E0 ) { v34 = (strconv_NumError *)sub_46C980((__int64)v34, (__int64)a1, a2, a2, v12, v35, v36, v37); v39 = (char *)v45; *v40 = v45; } else { v39 = (char *)v45; } v34->Num.ptr = v39; v41 = off_58EDB8[0]; v34->Err.tab = off_58EDB0; if ( dword_5B41E0 ) { v34 = (strconv_NumError *)sub_46C980((__int64)v34, (__int64)a1, v38, a2, (__int64)v41, v35, v36, v37); *v42 = v41; } v34->Err.data = v41; return 0; } a8 = 5 * v25; ++v24; v25 = (unsigned __int8)v32 + 10 * v25; } result = v25; v43 = -v25; if ( v10 == 45 ) return v43; } else { result = sub_472AE0((_DWORD)a1, a2, 10, 0, a5, a6, a7, a8, a9); if ( (RTYPE **)a2 == off_4FF380 ) { v26[1] = 4; if ( dword_5B41E0 ) { result = sub_46C980(result, a2, (__int64)v26, 0, a5, v27, v28, v29); *v31 = v30; } *v26 = "Atoi"; } } return result; } __int64 __golang sub_4AED60(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v5 + 16) ) sub_46AC20(a1, a2); v25[1] = 5; v25[0] = "/proc"; v25[3] = 10; v25[2] = strconv_FormatInt(a1, 10, a3, a4); v25[5] = 4; v25[4] = "comm"; v10 = path_filepath_join((unsigned int)v25, 3, 3, a4, a5, v6, v7, v8, v9); v16 = sub_498FE0(v10, 3, v11, a4, a5, v12, v13, v14, v15); if ( a4 ) return 0; if ( v17 < 2 ) sub_46CDC0(v16, 3, 2); return runtime_slicebytetostring(0, v16, 2, 0, 2, v18, v19, v20, v21, v23, v24); } ~~~ 第三个出现了"/proc/net/tcp",可以检查tcp连接状态,反编译不完整,可以稍微patch下得到完整代码,发现用冒号分隔,然后有个a8d5逆序转为int正好是23946,也就是ida调试常用的端口;如果不等于说明没有ida调试,做一次RC4加密密文(sub_4AE9C0) ~~~c __int64 __golang sub_4AEFC0(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&v45 <= *(_QWORD *)(v5 + 16) ) sub_46AC20(a1, a2); v46 = v6; runtime_chanrecv1(qword_593EF0, 0); *(_QWORD *)&v46 = &off_4E0150; v7 = 13; v8 = 0; v13 = os_OpenFile((__int64)"/proc/net/tcp", 13, 0, 0, a5, v9, v10, v11, v12); sub_4AF3A0(v13, 13); while ( 1 ) { v19 = bufio__ptr_Scanner_Scan((unsigned int)&v42, v7, v14, v8, a5, v15, v16, v17, v18, v36); if ( !(_BYTE)v19 ) break; v7 = v43; v25 = runtime_slicebytetostring(0, v43, v44, v8, a5, v21, v22, v23, v24, v37, v39, v41); v31 = strings_Fields(v25, v7, v26, v8, a5, v27, v28, v29, v30, v38, v40); if ( v7 >= 4 ) { v7 = *(_QWORD *)(v31 + 24); v8 = 1; a5 = 0; v32 = strings_SplitN(*(_QWORD *)(v31 + 16), v7, (int)&unk_4FDCC8, 1, 0, -1, v16, v17, v18); if ( (unsigned __int64)v7 <= 1 ) sub_46CD40(1); v14 = *(_QWORD *)(v32 + 24); if ( v14 == 4 ) { v7 = *(_QWORD *)(v32 + 16); if ( *(_DWORD *)v7 == 'a8d5' || *(_DWORD *)v7 == 'A8D5' ) { v33 = (**((__int64 (***)(void))&v46 + 1))(); return sub_4AF3A0(v33, v7); } } } } sub_4AE9C0(v19, v7, v20, v8, a5, v21, v22, v23, v24); v35 = (**((__int64 (***)(void))&v46 + 1))(); return sub_4AF3A0(v35, v7); } ~~~ 第四个同上,patch下拿到完整代码,发现读取了`/proc/self/status`并检测文件中TracerPid字段,然后解析冒号后的数字(v11是长度,\*v30是第一个字符),其实就是检查是否不等于0,不等于0说明有调试,无调试的时候做一次RC4加密密文(sub_4AE9C0) ~~~c __int64 __golang sub_4AEA40(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, int a6, int a7, int a8, int a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)v46 <= *(_QWORD *)(v9 + 16) ) sub_46AC20(a1, a2); v47 = (void (__golang **)(__int64))*((_QWORD *)&v10 + 1); v46[13] = off_4E0140; v11 = 17; v12 = 0; os_OpenFile((__int64)"/proc/self/statuspidfd_sen", 17, 0, 0, a5, a6, a7, a8, a9); sub_4AF420(); while ( 1 ) { v18 = bufio__ptr_Scanner_Scan((unsigned int)&v44, v11, v13, v12, a5, v14, v15, v16, v17, v38); if ( !(_BYTE)v18 ) break; v11 = v45; v24 = runtime_slicebytetostring(0, v45, v46[0], v12, a5, v20, v21, v22, v23, v39, v40, v41); if ( v11 >= 9 ) { v42 = v24; v13 = sub_403280(v24, (__int64)"TracerPid"); v24 = v42; } else { v13 = 0; } if ( (_BYTE)v13 ) { v12 = 1; a5 = 0; v25 = strings_SplitN(v24, v11, (int)&unk_4FDCC8, 1, 0, -1, v15, v16, v17); if ( (unsigned __int64)v11 <= 1 ) sub_46CD40(1); v11 = *(_QWORD *)(v25 + 24); v30 = (_BYTE *)strings_TrimSpace( *(_QWORD *)(v25 + 16), v11, *(_QWORD *)(v25 + 16), 1, 0, v26, v27, v28, v29, v38, v40); if ( v11 != 1 || *v30 != 48 ) { v43 = v10; v31 = runtime_convTstring((_DWORD)v30, v11, v13, 1, 0, v14, v15, v16, v17); *(_QWORD *)&v43 = &RTYPE_string; *((_QWORD *)&v43 + 1) = v31; v36 = fmt_Fprintln( (unsigned int)off_4FF328, qword_593F38, (unsigned int)&v43, 1, 1, v32, v33, v34, v35, v38, v40); (*v47)(v36); return sub_4AF420(); } } } sub_4AE9C0(v18, v11, v19, v12, a5, v20, v21, v22, v23); ((void (*)(void))*v47)(); return sub_4AF420(); } ~~~ 第五个里面sub_489520调用了syscall,传入110(sys_getppid)获取了父进程pid,然后根据pid去获取父进程名称(`/proc/<PID>/comm`),接着检查父进程名称里是否存在sh,没有找到就会直接调用runtime_gopanic使程序崩溃,也就是说出题人希望题目能够直接在shell(bash、zsh、sh等)中直接执行 ~~~c __int64 __golang sub_4AF220(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) ) goto LABEL_8; sub_489520(a1, a2, a3, a4, a5, a6, a7, a8, a9, 110, v10, *((__int64 *)&v10 + 1), 0); v16 = sub_4AED60(v33, a2, v11, a4, a5, v12, v13, v14, v15); v22 = v17 == 0; if ( v17 ) { LABEL_5: if ( !v22 ) v17 = *(_QWORD *)(v17 + 8); a2 = a4; a1 = runtime_gopanic(v17, a4, v17, a4, a5, v18, v19, v20, v21); LABEL_8: sub_46AC20(a1, a2, a3, a4, a5, a6, a7, a8, a9, v29, v30); } a4 = 2; result = strings_Index(v16, a2, (unsigned int)"sh", 2, a5, v18, v19, v20, v21, v29, v30, v31, v32); if ( result < 0 ) { runtime_gopanic((unsigned int)&RTYPE_string, (unsigned int)&off_4FEA28, v24, 2, a5, v25, v26, v27, v28); goto LABEL_5; } return result; } __int64 __golang sub_489520( _DWORD a1, _DWORD a2, _DWORD a3, _DWORD a4, _DWORD a5, _DWORD a6, _DWORD a7, _DWORD a8, _DWORD a9, __int64 a10) { __int64 result; // rax result = a10; __asm { syscall; LINUX - } return result; } __int64 __golang sub_4AED60(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v5 + 16) ) sub_46AC20(a1, a2); v25[1] = 5; v25[0] = "/proc"; v25[3] = 10; v25[2] = strconv_FormatInt(a1, 10, a3, a4); v25[5] = 4; v25[4] = "comm"; v10 = path_filepath_join((unsigned int)v25, 3, 3, a4, a5, v6, v7, v8, v9); v16 = sub_498FE0(v10, 3, v11, a4, a5, v12, v13, v14, v15); if ( a4 ) return 0; if ( v17 < 2 ) sub_46CDC0(v16, 3, 2); return runtime_slicebytetostring(0, v16, 2, 0, 2, v18, v19, v20, v21, v23, v24); } ~~~ 最后一个函数,注册了一个无限循环(goroutine),反复执行sleep 2s,检测实际花了多少秒,如果大于5s就会立即修改密文(循环异或0x66),实际测试好像正常也会做异或? ~~~c void __golang __noreturn sub_4AF2C0(__int64 a1, __int64 a2, __int64 a3, int a4, int a5) { __int64 v5; // r14 signed __int64 v6; // rbx __int64 i; // rax __int64 v8; // rax int v9; // ecx int v10; // r8d int v11; // r9d int v12; // r10d int v13; // r11d __int64 v14; // rax __int64 v15; // rax __int64 v16; // [rsp-10h] [rbp-20h] signed __int64 v17; // [rsp+0h] [rbp-10h] _UNKNOWN *retaddr; // [rsp+10h] [rbp+0h] BYREF if ( (unsigned __int64)&retaddr > *(_QWORD *)(v5 + 16) ) { v6 = 0; i = runtime_chanrecv1(qword_593F00, 0); while ( 1 ) { v8 = ((__int64 (__golang *)(__int64))time_Now)(i); if ( v8 < 0 ) { v9 = 0xD7B17F80; v6 = ((unsigned __int64)(2 * v8) >> 31) + 0xDD7B17F80LL; } v17 = v6; v14 = sub_468B00(2000000000, v6, v9, a4, a5, v10, v11, v12, v13, v16); v15 = ((__int64 (__golang *)(__int64))time_Now)(v14); if ( v15 < 0 ) v6 = ((unsigned __int64)(2 * v15) >> 31) + 0xDD7B17F80LL; i = v17; v6 -= v17; if ( v6 > 5 ) { for ( i = 0; qword_58F1C8 > i; ++i ) // 40h { v6 = *((unsigned __int8 *)cipher + i) ^ 0x66u; *((_BYTE *)cipher + i) ^= 0x66u; } } } } ((void (__noreturn *)(void))sub_46AC20)(); } ~~~ 通过分析go注册的6个goroutine(并发线程)我们发现了6种反调试,其中前四个没有调试会对密文做四次RC4加密,且加密完都会调用runtime_chansend1通知主进程,第五次没有调试就不会退出,第六个没有调试就不会修改密文 开始分析主函数,里面第一个函数sub_4A5C60经过ai分析发现是strings.SplitN函数,将输入按照下划线分割 第二个函数sub_4B09E0里发现进入一个循环,里面注册了一个sub_4B0DA0(主函数的goroutine),重点分析sub_4B0DA0,里面调用了sub_4B1040,根据a3值做不同处理 ~~~c __int64 __golang sub_4B1040(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5, int a6, int a7, int a8, int a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( (unsigned __int64)&v101 <= *(_QWORD *)(v9 + 16) ) goto LABEL_26; if ( a3 > 1 ) { if ( a3 == 2 ) { runtime_chanrecv1(qword_593F18, 0); v71 = aes_key_len; v76 = crypto_aes_NewCipher((_DWORD)aes_key, aes_key_len, qword_58F1F0, a4, a5, v72, v73, v74, v75); if ( !v77 ) { v97 = v71; v94 = v76; v100 = runtime_makeslice((unsigned int)&RTYPE_uint8, a2, a2, a4, a5, v78, v79, v80, v81); v86 = runtime_stringtoslicebyte(0, a1, a2, a4, a5, v82, v83, v84, v85); (*(void (__golang **)(__int64, __int64, __int64, __int64, __int64, __int64, __int64))(v94 + 40))( v71, v100, a2, a2, v86, a1, v87); v101 = v100; v102 = a2; v103 = a2; v104 = 2; v88 = runtime_chansend1(a4, &v101); sub_4B0EE0(v88, (__int64)&v101, v89, a2, v86, v90, v91, v92); return runtime_chansend1(qword_593F20, &v93); } runtime_gopanic(*(_QWORD *)(v77 + 8), a4, *(_QWORD *)(v77 + 8), a4, a5, v78, v79, v80, v81); } else { if ( a3 != 3 ) { LABEL_19: runtime_gopanic((unsigned int)&RTYPE_string, (unsigned int)&off_4FEA28, a3, a4, a5, a6, a7, a8, a9); goto LABEL_20; } runtime_chanrecv1(qword_593F20, 0); v53 = aes_key_len; v58 = crypto_aes_NewCipher((_DWORD)aes_key, aes_key_len, qword_58F1F0, a4, a5, v54, v55, v56, v57); v64 = v59 == 0; if ( !v59 ) { v93 = v58; v100 = runtime_makeslice((unsigned int)&RTYPE_uint8, a2, a2, a4, a5, v60, v61, v62, v63); v69 = runtime_stringtoslicebyte(0, a1, a2, a4, a5, v65, v66, v67, v68); (*(void (__golang **)(__int64, __int64, __int64, __int64, __int64, __int64, __int64))(v93 + 40))( v53, v100, a2, a2, v69, a1, v70); v101 = v100; v102 = a2; v103 = a2; v104 = 3; return runtime_chansend1(a4, &v101); } } if ( !v64 ) v59 = *(_QWORD *)(v59 + 8); runtime_gopanic(v59, a4, v59, a4, a5, v60, v61, v62, v63); goto LABEL_19; } if ( a3 ) { if ( a3 == 1 ) { runtime_chanrecv1(qword_593F10, 0); v10 = aes_key_len; v15 = crypto_aes_NewCipher((_DWORD)aes_key, aes_key_len, qword_58F1F0, a4, a5, v11, v12, v13, v14); v21 = v16 == 0; if ( !v16 ) { v95 = v15; v98 = v10; v100 = runtime_makeslice((unsigned int)&RTYPE_uint8, a2, a2, a4, a5, v17, v18, v19, v20); v26 = runtime_stringtoslicebyte(0, a1, a2, a4, a5, v22, v23, v24, v25); (*(void (__golang **)(__int64, __int64, __int64, __int64, __int64, __int64, __int64))(v95 + 40))( v10, v100, a2, a2, v26, a1, v27); v101 = v100; v102 = a2; v103 = a2; v104 = 1; v28 = runtime_chansend1(a4, &v101); sub_4B0EE0(v28, (__int64)&v101, v29, a2, v26, v30, v31, v32); return runtime_chansend1(qword_593F18, &v93); } goto LABEL_23; } goto LABEL_19; } v34 = aes_key_len; v35 = crypto_aes_NewCipher((_DWORD)aes_key, aes_key_len, qword_58F1F0, a4, a5, a6, a7, a8, a9); v41 = v36 == 0; if ( v36 ) { LABEL_20: if ( !v41 ) v36 = *(_QWORD *)(v36 + 8); runtime_gopanic(v36, a4, v36, a4, a5, v37, v38, v39, v40); LABEL_23: if ( !v21 ) v16 = *(_QWORD *)(v16 + 8); runtime_gopanic(v16, a4, v16, a4, a5, v17, v18, v19, v20); LABEL_26: sub_46AC20(); } v99 = v34; v96 = v35; v100 = runtime_makeslice((unsigned int)&RTYPE_uint8, a2, a2, a4, a5, v37, v38, v39, v40); v46 = runtime_stringtoslicebyte(0, a1, a2, a4, a5, v42, v43, v44, v45); (*(void (__golang **)(__int64, __int64, __int64, __int64, __int64, __int64, __int64))(v96 + 40))( v34, v100, a2, a2, v46, a1, v47); v101 = v100; v102 = a2; v103 = a2; v104 = 0; v48 = runtime_chansend1(a4, &v101); sub_4B0EE0(v48, (__int64)&v101, v49, a2, v46, v50, v51, v52); return runtime_chansend1(qword_593F10, &v93); } ~~~ 里面有AES,密钥为`hey_syclover2024`,里面还有个对密钥做移位处理的函数sub_4B0EE0:先全部逆序、再前四位逆序、最后后12位逆序 ~~~c signed __int64 __golang sub_4B0EE0( __int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, __int64 a8) { unsigned __int64 v8; // rcx unsigned __int64 v9; // rbx __int64 v10; // rsi unsigned __int64 i; // rax char *v12; // rdx unsigned __int64 v13; // rax unsigned __int64 j; // rcx char *v15; // rdx unsigned __int64 v16; // rbx __int64 v17; // rsi signed __int64 result; // rax char v19; // di v8 = aes_key_len; v9 = (unsigned __int64)aes_key; v10 = aes_key_len - 1; for ( i = 0; v10 > (__int64)i; ++i ) { if ( i >= v8 ) sub_46CD40(i); a4 = *(unsigned __int8 *)(i + v9); if ( v10 >= v8 ) sub_46CD40(v10); a6 = *(unsigned __int8 *)(v10 + v9); *(_BYTE *)(v9 + i) = a6; *(_BYTE *)(v9 + v10--) = a4; } if ( (unsigned __int64)qword_58F1F0 < 4 ) sub_46CDC0(i, v9, 4); v12 = aes_key; v13 = 0; for ( j = 3; (__int64)j > (__int64)v13; --j ) { if ( v13 >= 4 ) sub_46CD40(v13); v9 = (unsigned __int8)v12[v13]; if ( j >= 4 ) sub_46CD40(j); v10 = (unsigned __int8)v12[j]; v12[v13] = v10; v12[j] = v9; ++v13; } if ( (unsigned __int64)aes_key_len < 4 ) sub_46CE00(4, v9, aes_key_len, a4, v10, a6, a7, a8); v15 = &aes_key[((4 - qword_58F1F0) >> 63) & 4]; v16 = aes_key_len - 4; v17 = aes_key_len - 5; for ( result = 0; v17 > result; ++result ) { if ( result >= v16 ) sub_46CD40(result); v19 = v15[result]; if ( v17 >= v16 ) sub_46CD40(v17); v15[result] = v15[v17]; v15[v17--] = v19; } return result; } ~~~ 总结为a3等于0、1、2时AES加密一次明文然后对密钥移位一次;a3等于3时只做一次AES加密(猜测a3是第几个分组的意思) 到这里可以做逆向了 ~~~python from Crypto.Cipher import AES, ARC4 from Crypto.Cipher.AES import MODE_ECB key = b"hey_syclover2024" keys = [key] for i in range(4): key = key[::-1] key = key[:4][::-1] + key[4:][::-1] keys.append(key) cipher = bytes([0xF0, 0x5B, 0x29, 0x5F, 0xC3, 0x5C, 0x2A, 0xBC, 0x8A, 0x42, 0x8F, 0xE7, 0x63, 0x5C, 0xFD, 0xAC, 0x74, 0x7E, 0x6D, 0xD3, 0x67, 0x13, 0x84, 0x1B, 0xDA, 0x60, 0x7C, 0x36, 0x96, 0xA8, 0x80, 0xDA, 0x51, 0xA7, 0xEC, 0xE5, 0x62, 0xFE, 0xC9, 0xB5, 0xE1, 0xF9, 0x07, 0x12, 0xB3, 0x53, 0xB3, 0xC0, 0x31, 0x14, 0x86, 0xD0, 0xC3, 0xD0, 0x92, 0xDE, 0x5A, 0x0D, 0xD1, 0xFF, 0x5B, 0x00, 0x1D, 0x2E]) for i in range(4): cipher = ARC4.new(keys[i+1]).decrypt(cipher) for i in range(4): tmp = AES.new(keys[i], mode=MODE_ECB).decrypt(cipher[16 * i:16 * i + 16]) print(bytes([j^0x66 for j in tmp]).decode(), end="") ~~~ 最后修改:2025 年 11 月 04 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏