CUMT-2021RE专项赛
1、签到
循环左移,逆序即可
| *((_BYTE *)Buf2 + v3) = __ROL1__(*((_BYTE *)Buf2 + v3), 1); ++v3;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from Crypto.Util.number import * Buf1=[0]*12 Buf1[0] = 0xE8DAEAC6; Buf1[1] = 0xF6CCE8C6; Buf1[2] = 0xCA9680DA; Buf1[3] = 0xECCAA4BE; Buf1[4] = 0x66E6A4CA; Buf1[5] = 0x72DCCABE; Buf1[6] = 0x8ACA9C62; Buf1[7] = 0xCEDC62A4; Buf1[8] = 0x66A48EBE; Buf1[9] = 0x80BE6EC2; Buf1[10] = 0xDC6282CE; Buf1[11] = 0xFA; for j in range(len(Buf1)): a=int.to_bytes(Buf1[j],4,'little') for i in a: print(chr((i>>1)|((i&1)<<7)),end='')
|
2、来自字节码的鼓励
python字节码,比较短,对照官方文档很容易便能翻译出源码。
dis — Disassembler for Python bytecode — Python 3.10.0 documentation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from Crypto.Util.number import * n=[-83,-96,-78,-21,-3,-17,58,31,58]
c='' f=open('1','r') s=f.read() f.close() f=open('2','rb') b=f.read(9) f.close() for i in range(len(s)): tmp=ord(s[i])^b[i] tmp=tmp+n[i] c+=chr(tmp) if c==ff: print('Right! Please add cumtctf{}') else: print('try again')
|
ff是输入,只在最后用到,故翻译出的字节码前部分便是flag的生成代码。
3、my cloth
经典upx壳,后加三段魔改default和轮数的Tea加密。
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
| v23[0] = 222; v23[1] = 173; v23[2] = 190; v23[3] = 239; v5 = 0; for ( i = 0; i <= 3; ++i ) v5 = (v5 << 8) + (unsigned __int8)v24[i]; v7 = 0; for ( j = 4; j <= 7; ++j ) v7 = (v7 << 8) + (unsigned __int8)v24[j]; v17 = v5; v18 = v7; encrypt(&v17, v23); unsigned int *__fastcall encrypt(unsigned int *result, _DWORD *a2) { unsigned int v2; unsigned int v3; int v4; unsigned int i;
v2 = *result; v3 = result[1]; v4 = 0; for ( i = 0; i <= 0x3F; ++i ) { v4 -= 559038737; v2 += (a2[1] + (v3 >> 5)) ^ (16 * v3 + *a2) ^ (v4 + v3); v3 += (a2[3] + (v2 >> 5)) ^ (16 * v2 + a2[2]) ^ (v4 + v2); } *result = v2; result[1] = v3; return result; }
|
exp:
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
| #include<iostream> using namespace std;
void tea_decode(unsigned int* s, unsigned int* key) { unsigned int v0 = s[0]; unsigned int v1 = s[1]; unsigned int defalt = 0x21524111; int sum = 0; for (int i = 0; i <= 0x3F; i++) sum -= defalt;
unsigned int k0 = key[0], k1 = key[1], k2 = key[2], k3 = key[3]; for (int i = 0; i <= 0x3f; i++) { v1 -= (k3 + (v0 >> 5)) ^ ((v0 << 4) + k2) ^ (sum + v0); v0 -= (k1 + (v1 >> 5)) ^ ((v1 << 4) + k0) ^ (sum + v1); sum += defalt; } s[0] = v0; s[1] = v1; cout << s[0] << "," << s[1] << ","; } int main() { unsigned int key[4] = { 222,173,190,239 }; unsigned int enc[6] = { 0xD5AF0608,0x361EF340,0xB55D7042,0xB460532B,0xC53FB95B,0xCC5F1002 }; tea_decode(enc, key); tea_decode(enc + 2, key); tea_decode(enc + 4, key); return 0; }
|
4、weak
花指令+数独
花指令单独写了一个函数,通过修改EIP来越过垃圾代码。
1 2 3 4 5 6
| .text:00000000000013EA junk proc near ; CODE XREF: .text:000000000000123A↑p .text:00000000000013EA pop rax .text:00000000000013EB add rax, 5 .text:00000000000013EF push rax .text:00000000000013F0 retn .text:00000000000013F0 junk endp
|
call 会push下一条指令的IP,之后对IP+5,并retn,对IP的值完成+5的操作。
nop掉call junk下一条指令开始的5个字节即可。
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| int __cdecl main(int argc, const char **argv, const char **envp) { int v3; int v5; int v6; unsigned __int64 i; unsigned __int64 j; unsigned __int64 k; unsigned __int64 m; unsigned __int64 n; unsigned __int64 ii; unsigned __int64 jj; __int64 v14; __int64 v15; __int64 v16; __int64 v17[4];
v17[3] = __readfsqword(0x28u); v17[0] = 0LL; v17[1] = 0LL; puts("your flag?"); __isoc99_scanf("%s", v17); v6 = 0; for ( i = 0LL; i <= 0x23; ++i ) { if ( !numbers[i] ) numbers[i] = *((_BYTE *)v17 + v6++) - 48; } junk(); for ( j = 0LL; j <= 5; ++j ) { v14 = 0LL; v15 = 0LL; v16 = 0LL; for ( k = 0LL; k <= 5; ++k ) { v3 = numbers[6 * j + k] - 1; ++*((_DWORD *)&v14 + v3); } for ( m = 0LL; m <= 5; ++m ) { if ( *((_DWORD *)&v14 + m) != 1 ) { LABEL_12: fail(); return 0; } } } junk(); for ( n = 0LL; n <= 5; ++n ) { v14 = 0LL; v15 = 0LL; v16 = 0LL; for ( ii = 0LL; ii <= 5; ++ii ) { v5 = numbers[6 * ii + n] - 1; ++*((_DWORD *)&v14 + v5); } for ( jj = 0LL; jj <= 5; ++jj ) { if ( *((_DWORD *)&v14 + jj) != 1 ) goto LABEL_12; } } succ(); return 0; }
|
6阶数独,数独并不难,手撸即可。
5、gogogo
go 语言+迷宫问题 10x10阶
不过IDA7.6也存在一些反编译的问题
1 2 3 4 5 6 7 8 9 10 11 12 13
| v12 = fmt_Fscanln(v5, v8, v10); if ( !v2 ) { for ( i = 0LL; v15[1] > i; i = v14 + 1 ) { v14 = i; if ( !(unsigned __int8)main___ptr_Maze__advance() ) { v16[0] = &off_4D6200; v14 = fmt_Fprintln(v6, v9, v11, v12, v13); } } if ( a11111111111010[11] == '#' )
|
比如将最后的check终点的位置,识别成了一个常量位置,查看汇编和动调能解决这个错误。
迷宫路径,从下标11(0xB)开始,扫到’#',眼过即可。但还是准备好一套熟悉的自动化迷宫脚本,一旦量大起来,手撸就不现实了。
6、SimpleSMC
花指令+反调试+SMC+Blowfish
首先观察main函数,第一句汇编便是iretq的返回指令,显然经过了修改,对main交叉引用可以定位到一句lea rcx, main,并发现上方有花指令。
1 2 3 4 5 6 7 8
| .text:0000000140003001 ; __unwind { .text:0000000140003001 jz short near ptr loc_140003005+1 .text:0000000140003003 jnz short near ptr loc_140003005+1 .text:0000000140003005 .text:0000000140003005 loc_140003005: ; CODE XREF: sub_140002FF0:loc_140003001↑j .text:0000000140003005 ; sub_140002FF0+13↑j .text:0000000140003005 call near ptr 0C9493074h .text:000000014000300A loope near ptr loc_140003053+1
|
通过jz 和 jnz 连用实现跳转,nop掉loc_140003005处的第一个字节即可。

通过与NT有关的API调用,类似一种反调试手段,不过此处也没发现对main函数有关的处理,跟进140003110函数。
1 2 3 4 5 6 7 8 9 10
| .text:0000000140003110 sub_140003110 proc near ; CODE XREF: sub_140002FF0:loc_140003073↑p .text:0000000140003110 ; DATA XREF: .pdata:000000014000D054↓o .text:0000000140003110 .text:0000000140003110 var_8 = qword ptr -8 .text:0000000140003110 .text:0000000140003110 pushfq .text:0000000140003111 or [rsp+8+var_8], 100h .text:0000000140003119 popfq .text:000000014000311A retn .text:000000014000311A sub_140003110 endp
|
pushfq是将标志寄存器的值入栈,之后or 0x100进行修改,将TF标志位置1,涉及到一个反调试。
通过将陷阱标志位TF置1导致触发单步执行异常(触发后会置0),而我们事先设置好的异常处理函数会修改eip跳到正确的代码处,调试时则不会。
参考:反调试技术–WIndows篇 - 深海之炎 - 博客园 (cnblogs.com)
在汇编窗口能看到try 和 except,并且在except 的代码中看到了对main函数处的内存进行了smc自修改。

汇编看出,修改代码为单字节循环左移三异或0x3F恢复,idapy脚本恢复即可。
1 2 3 4 5 6 7 8
| import idautils target=0x0000000140001000 for i in range(0x1F50): a=Byte(target+i) result=((a<<3)|(a>>5))^0x3f PatchByte(target+i,result&0xff) print('yes')
|
修复后可见ida在反编译时定义了4个256的int数组,并且内存中有连续的18个int。

hint提示为blowfish加密,不过优化让代码面目全非,一步一步分析是个体力活。
blowfish参考:https://cloud.tencent.com/developer/article/1836650
不过是魔改掉了p盒和s盒,初始化的代码是没有变的,可能经过优化有些难读。
例如:
1 2 3 4 5 6 7 8 9 10 11 12
| for (int i = 0; i < 4; ++i) { for (int j = 0; j < 256; j+=2) { BlowfishEncryption(ptr, &leftSide, &rightSide); ptr->s[i][j] = leftSide; ptr->s[i][j+1] = rightSide; } }
for ( i = 0; i < 0x200; ++i )
|
整体加密流程就是先秘钥初始化,此部分与输入无关,之后进行加密,加密流程类似festil轮,左边等于左边异或p[i],右边等与F(左)^右,最后再左右交换。一共循环16轮,最后一轮取消交换,左右与p[17]和p[16]异或即可。解密即逆过程。
1 2 3 4 5 6 7 8 9 10
| void blowfish_decrypt(uint32_t* L, uint32_t* R) { *L ^= P[17]; *R ^= P[16]; swap(L, R); for (short r = 15; r >=0; r--) { *L = *L ^ f(*R); *R^=P[r]; swap(L, R); } }
|
按照内存中的数据修改头文件中的pbox和sbox,解密的主体代码如下。
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 44 45 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 83
| #include<iostream> #include"blowfish.h" using namespace std; static uint32_t f(uint32_t x) { uint32_t h = S[0][x >> 24] + S[1][x >> 16 & 0xff]; return (h ^ S[2][x >> 8 & 0xff]) + S[3][x & 0xff]; }
void blowfish_encrypt(uint32_t* L, uint32_t* R) { for (short r = 0; r < 16; r++) {
*L = *L ^ P[r]; *R = f(*L) ^ *R; swap(L, R); } swap(L, R); *R = *R ^ P[16]; *L = *L ^ P[17]; }
void blowfish_decrypt(uint32_t* L, uint32_t* R) { *L ^= P[17]; *R ^= P[16]; swap(L, R); for (short r = 15; r >= 0; r--) { *L = *L ^ f(*R); *R ^= P[r]; swap(L, R); } }
void blowfish_init(const unsigned int* key, int key_len) { uint32_t k; for (short i = 0, p = 0; i < 18; i++) { k = key[i % key_len]; P[i] ^= k; } uint32_t l = 0x00, r = 0x00; for (short i = 0; i < 18; i += 2) { blowfish_encrypt(&l, &r); P[i] = l; P[i + 1] = r; } for (short i = 0; i < 4; i++) { for (short j = 0; j < 256; j += 2) { blowfish_encrypt(&l, &r); S[i][j] = l; S[i][j + 1] = r; } } } int main() {
unsigned int key[4] = { 0x12233445,0xDEADBEEF,0x90223344,0x88112243 }; blowfish_init(key, 4);
uint32_t enc[8] = { 0x7187C938, 0xCDE138C1, 0x3DBA6F8C, 0x4E68D12A, 0xA7FB22EE, 0x52E73F49, 0x81E16485, 0x753D87D7 }; for (int i = 0; i < 4; i += 1) blowfish_decrypt(enc + 7 - i, enc + i);
printf("%s", (char*)enc); return 0; }
|
当然秘钥初始化是与输入无关的,通过动调来拿到初始化后的扩展盒和秘钥解密可能会更快。
最后,某些人现在终于有自己的博客啦,用re专项来纪念一下,也是最近鸽了老久的比赛,现在终于完结撒花咯,还有一堆比赛有待复现,路途尚远,还要继续前行!