2023SharkCTF-week2-Reverse-wp

2023SharkCTF-week2-Reverse-wp

2023.8.21 by Shen_Fan


before_main

基本阅读ida反汇编代码逻辑后可以发现该程序读入一个字符串,然后使用sub_4015DA对字符串进行处理并返回一个指针作为处理后的结果,再将处理后的结果与Str2比较并输出正误。

sub_4015DA由标志性的base64字母表ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/与Str2的形态可以确定为未换表的base64,考虑直接对Str2解密。

解密后得到:

1
sh@rkctf{something_run_before_main!}

并且输入程序时显示flag错误,鉴定为假flag。

对Str2查找交叉引用发现一个函数对Str2进行了赋值,猜测该函数在main前运行,对该函数中的赋值进行base64解密得到:

1
sharkctf{N3w_Bln7_l3_S0_us3fv1_iN_Pr07r@mlng}

获得flag。

a_cup_of_tea

分析ida返回表代码逻辑后发现程序读入一个字符串,并在判断长度是否为23后将其与一个局部变量数组一起传入一个encrypt函数,然后将Str数组的前16个字节与res数组比较后输出正误

(为啥23字节的数组最后只比对了前16个字节呢,因为有人出题出傻了,给各位师傅磕头了)

对encrypt函数进行简单逆向后得到解密函数(也可以直接照抄ctf-wiki上tea加密的解密函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
void decrypt(unsigned int *a1, unsigned int* a2) {
unsigned int v4=0;
v4 -= 0x20*1640531527;
unsigned int v5 = a1[1];
unsigned int v6 = *a1;
for(int i=0;i<=0x1f;++i) {
v5-=(v6 + v4) ^ (a2[2] + 16 * v6) ^ ((v6 >> 5) + a2[3]);
v6-=(v5 + v4) ^ (*a2 + 16 * v5) ^ ((v5 >> 5) + a2[1]);
v4+=1640531527;
}
*a1=v6;
a1[1]=v5;
}

解密后得到flag

1
i7_IS_JuSt_4_CUP

结合hint得到最终flag

1
sharkctf{i7_IS_JuSt_4_CUP_Of_7e4}

ez_maze

分析逻辑后得知该迷宫由数字0123操作,否则会报出”invalid move”退出,对应的操作下的变量v5猜测为当前位置。0对应+1,向右;1对应-1,向左;2对应-6,猜测为向上;3对应+6,猜测为向下。那么迷宫的列数应该为6,这样+6或-6可以在行之间移动。 当v5=5时循环退出,猜测位置5为终点。迷宫的逻辑为将当前位置与0x2a异或,若异或结果为0,则输出hit the wall并退出。最终退出循环时若位置为5且无多余步骤,就输出flag。

考虑直接拷贝迷宫手动走,墙为0x2a,将进行与0x2a异或的数组byte_403020提取得:

1
2
3
4
5
6
7
8
9
10
unsigned char ida_chars[] =
{
0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x2A, 0x2A, 0x00, 0x2A,
0x00, 0x2A, 0x2A, 0x00, 0x00, 0x2A, 0x00, 0x2A, 0x2A, 0x00,
0x2A, 0x2A, 0x00, 0x2A, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x2A,
0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};

稍微排版一下让列数为6

1
2
3
4
5
6
7
8
9
10
11
12
unsigned char ida_chars[] =
{
0x00, 0x00, 0x00, 0x2A, 0x00, 0x00,
0x2A, 0x2A, 0x00, 0x2A, 0x00, 0x2A,
0x2A, 0x00, 0x00, 0x2A, 0x00, 0x2A,
0x2A, 0x00, 0x2A, 0x2A, 0x00, 0x2A,
0x2A, 0x00, 0x00, 0x00, 0x00, 0x2A,
0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};

可以观察到唯一道路的迷宫,手动走出flag:

1
sharkctf{003313300022220}

Hungry_Snake

本周的压轴题,由于第二周的签到题离奇消失临时整的。

本题的难度主要在于在屎山一般的代码中寻找flag加密的主逻辑并写脚本恢复,

题目给出了结构体信息,先找教程在ida中恢复结构体(你也不想盯着一堆偏移硬看吧)

对于较为复杂的题目,我们一般从flag的输入和输出下手,追踪flag去了哪里并分析其逻辑,这里从输入入手。

1
2
3
printf("flag:");
scanf("%60s", Str);
if ( strlen(Str) == 45 )

首先flag被scanf读入至数组Str,然后对Str进行了长度判断

1
gameInit(Str);

下一次也是最后一次出现时是在游戏初始化过程,由于Str为局部变量一般不会被其他函数访问,可以考虑Str通过游戏初始化被完全转移了。

进入gameInit函数并只关注与他参数有关的代码部分

1
2
memset(&game.in[game.total], 0, 0x30ui64);
strcpy(&game.in[game.total], a1);

结合上文

1
2
3
4
5
6
7
game.width = 16;
game.height = 16;
game.total = game.height * game.width;
//...
game.in = (char *)malloc(game.total + 48i64);
//...
memset(game.in, 46, game.total);

得知game.in为新分配的一块大小为地图大小+48字节的内存,而game.in前game.total字节被设置为’.’,猜测为存储地图,之后的48字节由上面的strcpy(&game.in[game.total], a1);猜测为存储输入的flag。

之后线索断了,但我们知道应该去game.in偏移为game.total的地方找我们的输入

查看游戏主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
scanKey();
v6 = moveSnake();
if ( v6 == -1 )
{
SetConsoleActiveScreenBuffer(game.hOutPut);
puts("You lose!");
system("pause");
return 0;
}
if ( v6 == 1 )
break;
printMap();
delay((unsigned int)game.delay_time);

scanKey经查看发现只获取输入并按一定规则改变蛇的方向。printMap仅仅是将game.in的前game.total字节输出,故排除。

moveSnake函数中没有直接使用game.in中的game.total以后的部分,但是在贪吃蛇所处位置值为’*’后,该函数会调用一个updateMap函数,进入后发现该函数即为对game.in的game.total偏移进行加密的函数。

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
unsigned __int64 updateMap()
{
unsigned __int64 result; // rax
int delay_time; // [rsp+0h] [rbp-30h]
int total; // [rsp+Ch] [rbp-24h]
unsigned int v3; // [rsp+18h] [rbp-18h]
unsigned int v4; // [rsp+1Ch] [rbp-14h]
char *v5; // [rsp+20h] [rbp-10h]
unsigned int i; // [rsp+2Ch] [rbp-4h]

v5 = &game.in[game.total];
delay_time = game.delay_time;
result = (unsigned int)game.total;
total = game.total;
for ( i = 0; i <= 5; ++i )
{
v3 = *((_DWORD *)v5 + 1);
v4 = ((v3 + ((v3 >> 5) ^ (16 * v3))) ^ delay_time) + *(_DWORD *)v5;
*(_DWORD *)v5 = v4;
result = (unsigned __int64)(v5 + 4);
*((_DWORD *)v5 + 1) = ((v4 + ((v4 >> 5) ^ (16 * v4))) ^ (total - 1640531527)) + v3;
v5 += 8;
}
return result;
}

注意到加密过程中的部分密钥来自于game结构体的一些内容,需要注意。

除此以外没有其他部分再使用game.total以后的内容,故猜测flag仅进行了这一次加密。

游戏胜利结束后,main会调用一个checkFlag函数对game.in的game.total偏移后的内容与一个数组aetx进行比较并返回结果,故猜测aetx为flag加密后的结果。

回看加密,该加密每次吃到东西都会调用一次,结合胜利条件game.snake.len=game.total可知总调用次数为255次(初始长度为1),同时加密过程中所使用的delay_time初始值为190,之后递减10至50便不会变化。针对以上特征编写解密脚本:

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
#include <stdio.h>

unsigned char aetx_[] =
{
0x00, 0xDB, 0x3A, 0x03, 0x2A, 0x61, 0x35, 0x8C, 0xBF, 0xA0,
0x49, 0x11, 0xD6, 0x4D, 0xD3, 0x73, 0x37, 0x48, 0xD8, 0x6C,
0xFF, 0x91, 0xB6, 0x0D, 0x88, 0x1B, 0x0E, 0x85, 0x1E, 0xA4,
0xE6, 0x59, 0xF1, 0x64, 0xE8, 0x03, 0xD8, 0xF3, 0x4F, 0xE5,
0x9E, 0x34, 0xFA, 0x44, 0xA6, 0xDF, 0xDC, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};

unsigned int* aetx = (unsigned int*)aetx_;

int main() {
for(int i=0;i<255;i++) {
unsigned int* v5 = aetx + 12;
unsigned int delay_time = i<240?50:50+(i-240)*10;
unsigned int total = 256;
for(int j=0;j<=5;j++) {
v5-=2;
unsigned int v4 = *v5;
v5[1] -= ((v4 + ((v4 >> 5) ^ (16 * v4))) ^ (total - 1640531527));
unsigned int v3 = v5[1];
*v5-=((v3 + ((v3 >> 5) ^ (16 * v3))) ^ delay_time);
}
}
puts((char*)aetx);
}

得到最终flag:

1
sharkctf{WoW_yOU_arE_pl4y1NG_Sn@kE_5o_w3ll!!}