CUMT-RE专项

CUMT-2021RE专项赛

1、签到

循环左移,逆序即可

1
2
*((_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]
#ff=input
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);

//明文相邻两四个字节进行加密,并转成大端的int型,key是固定的
unsigned int *__fastcall encrypt(unsigned int *result, _DWORD *a2)
{
unsigned int v2; // [sp+Ch] [bp+Ch]
unsigned int v3; // [sp+10h] [bp+10h]
int v4; // [sp+14h] [bp+14h]
unsigned int i; // [sp+18h] [bp+18h]

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;
}
/* python
a=[1668640116,1668572795,1382381157,1920165215,829644646,1970167677,]
for i in a:
print(int.to_bytes(i,4,'big').decode(),end='')
*/

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; // eax
int v5; // eax
int v6; // [rsp+4h] [rbp-7Ch]
unsigned __int64 i; // [rsp+8h] [rbp-78h]
unsigned __int64 j; // [rsp+10h] [rbp-70h]
unsigned __int64 k; // [rsp+18h] [rbp-68h]
unsigned __int64 m; // [rsp+20h] [rbp-60h]
unsigned __int64 n; // [rsp+28h] [rbp-58h]
unsigned __int64 ii; // [rsp+30h] [rbp-50h]
unsigned __int64 jj; // [rsp+38h] [rbp-48h]
__int64 v14; // [rsp+40h] [rbp-40h]
__int64 v15; // [rsp+48h] [rbp-38h]
__int64 v16; // [rsp+50h] [rbp-30h]
__int64 v17[4]; // [rsp+60h] [rbp-20h] BYREF

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);//要求v3 0-5 即数组每行为1-6
}
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; //要求每列1-6
++*((_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 { // __C_specific_handler
.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;
}
}
//而IDA直接识别为
for ( i = 0; i < 0x200; ++i )
//xxxxxxx

整体加密流程就是先秘钥初始化,此部分与输入无关,之后进行加密,加密流程类似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) { // 网上一般采用的解密方法 感觉有点别扭
//
// for (short r = 17; r > 1; r--) {
// *L = *L ^ P[r];
// *R = f(*L) ^ *R;
// swap(L, R);
// }
// swap(L, R);
// *R = *R ^ P[1];
// *L = *L ^ P[0];
//}

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) {
/* initialize P box w/ key*/
uint32_t k;
for (short i = 0, p = 0; i < 18; i++)
{
k = key[i % key_len];
P[i] ^= k;
}
/* blowfish key expansion (521 iterations) */
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++) //512次
{
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专项来纪念一下,也是最近鸽了老久的比赛,现在终于完结撒花咯,还有一堆比赛有待复现,路途尚远,还要继续前行!


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!