算法分析:Snake
本帖最后由 Panel 于 2022-3-28 19:57 编辑## 算法分析:Snake
### 1.先下载附件,查壳运行得知初步逻辑
##### 查壳得知是UPX壳,那直接使用UPX脱壳工具一键脱壳,得到原程序信息,32位控制台程序
##### 运行一下exe,查看运行逻辑
##### 需要先输入名字
##### 发现就是一个贪吃蛇游戏,死亡以后便得到了关于flag得信息,要求我们分数高才能得到flag,
##### 按照以往经验猜测这种基本上要么要求的分数很高,要么就是一个骗局,那么我们直接采用IDA Pro来进行分析
### 2.静态分析
##### 老规矩,先查看是否存在可以字符串
##### 发现三个可疑的字符串,第一个:通过以往经验可以猜测可能是某一轮Flag加密的结果,第二个:64位长度,每个字符都是Base64编码表中的字符,所以99%就是Base64编码表,那可以先预料到可能是使用了Base64加密,第三个:通常这种提示厉害正确类似的话便可以很大程度确定这串字符所在的位置就是Flag判断位正确的位置
##### 老规矩,顺藤摸瓜,我们直接定位到提示正确的函数取查看验证,因为通常在最后一次验证出结果的地方也能找到最后一次的Flag加密字符,那双击字符便来到了sub_40186F函数,我们先按下N把sub_40186F函数名全局改为Check,接下来我们就称sub_40186F函数为Check
```
int Check()
{
char v1; // @1
char Dst; // @4
int j; // @4
int i; // @1
sub_4021AD(22, 18);
scanf("%s", v1);
for ( i = 0; v1; ++i )
;
sub_4017D2(v1, i);
memset(Dst, 0, 0x800u);
sub_4015F7(v1, Dst, i);
sub_4021AD(22, 20);
for ( j = 0; Dst; ++j )
{
if ( Dst != a7g5d5bayTmdlwl )
return puts("不对哦~下次再来吧~");
}
return puts("哇!你真厉害!");
}
```
##### 阅读代码可以知道只要if条件Dst中的每一个元素的值不等于a7g5d5bayTmdlwl中的对应的每一个元素的值那么便提示不对,那么只要相等便会循环结束且提示正确,此时可以确定a7g5d5bayTmdlwl数组就是最后一轮Flag的加密结果,双击a7g5d5bayTmdlwl便可得到其值为:`7G5d5bAy+TMdLWlu5CdkMTlcJnwkNUgb2AQL3CcmPpVf6DAp72scOSlb`
##### 那我们就找已知和未知的关系了,这里的已知条件是最后一轮Flag加密的结果,未知的条件便是我们的正确Flag
##### 之前的帖子我说过找已知和未知的技巧,就是找左值和右值,我们读Check代码知道v1是我们输入字符串所在地址,i是输入字符串的长度,那从下往上找左右值关系
```
//十四行
sub_4015F7(v1, Dst, i);
//十二行
sub_4017D2(v1, i);
```
##### 那就先分析第十四行的sub_4015F7(v1, Dst, i)
```
int __cdecl sub_4015F7(int a1, int a2, int a3)
{
int v3; // eax@2
int v4; // eax@3
int v5; // ST08_4@3
int v6; // eax@3
int v7; // eax@4
int v8; // eax@5
int v9; // eax@6
unsigned __int8 v11; // @2
unsigned __int8 v12; // @4
int v13; // @1
int v14; // @2
int v15; // @4
int v16; // @1
v16 = 0;
v13 = 0;
while ( v16 < a3 )
{
v3 = v13;
v14 = v13 + 1;
*(_BYTE *)(a2 + v3) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];
v11 = 16 * *(_BYTE *)(v16 + a1) & 0x30;
if ( v16 + 1 >= a3 )
{
v4 = v14;
v5 = v14 + 1;
*(_BYTE *)(a2 + v4) = Str;
*(_BYTE *)(v5 + a2) = 61;
v6 = v5 + 1;
v13 = v5 + 2;
*(_BYTE *)(v6 + a2) = 61;
break;
}
v7 = v14;
v15 = v14 + 1;
*(_BYTE *)(a2 + v7) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF | v11];
v12 = 4 * *(_BYTE *)(v16 + 1 + a1) & 0x3C;
if ( v16 + 2 >= a3 )
{
*(_BYTE *)(a2 + v15) = Str;
v8 = v15 + 1;
v13 = v15 + 2;
*(_BYTE *)(v8 + a2) = 61;
break;
}
*(_BYTE *)(a2 + v15) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];
v9 = v15 + 1;
v13 = v15 + 2;
*(_BYTE *)(a2 + v9) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];
v16 += 3;
}
*(_BYTE *)(v13 + a2) = 0;
return a2;
}
```
##### a1是我们输入字符串所在的地址,a2是最后一轮Flag加密结果,a3是输入字符串的长度
##### 分析代码`*(_BYTE *)(v13 + a2) = 0;`可以知道这里是做最后一轮Flag加密字符串结尾的工作,但是结尾的这个0是不被算入长度的,所以v13是第57位,下角标位56,所以v13在该函数中最后等于56;
##### 阅读代码,得知最后一轮Flag加密字符串a2与输入字符a1无条件做左右值的地方有:
```
*(_BYTE *)(a2 + v3) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];
*(_BYTE *)(a2 + v7) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF
*(_BYTE *)(a2 + v15) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];
*(_BYTE *)(a2 + v9) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];
```
##### 分析v3、v7、v15、v9、v16、v11、v12的关系:
```
//循环外初始值
v16 = 0;
v13 = 0;
//单次循环内变换
v3 = v13;
v14 = v13 + 1;
v11 = 16 * *(_BYTE *)(v16 + a1) & 0x30;
v7 = v14;
v15 = v14 + 1;
v12 = 4 * *(_BYTE *)(v16 + 1 + a1) & 0x3C;
v9 = v15 + 1;
v13 = v15 + 2;
v16 += 3;
```
##### 那么到这里可以化简以上代码为:
```
*(_BYTE *)(a2 + i) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];
*(_BYTE *)(a2 + i+1) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF
*(_BYTE *)(a2 + i+2) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];
*(_BYTE *)(a2 + i+3) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];
```
##### 也就是说在循环内不受任何条件影响每次有序(划重点)操作四个a2字符,那么a2总共有57个字符,循环外操作了最后一个,那循环内只能操作前56个了,也就是循环了56/4=14轮,做多也只能操作14轮,那经过14轮循环以后v16等于42,也就是说while的条件中的a3便是42两个if条件分别是:`if ( v16 + 1 >= a3 )`、`if ( v16 + 2 >= a3 )`,我们可推算在v16在满足循环条件内在两个if前的最大的值为3*13= 39,那v16+1和v16+2的最大值分别为40、41,所以两个if条件是永远不会满足的,那么整个函数的关键逻辑便可化简为
```
while(v16<42)
{
*(_BYTE *)(a2 + i) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];
*(_BYTE *)(a2 + v7) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF | v11];
*(_BYTE *)(a2 + i+2) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];
*(_BYTE *)(a2 + i+3) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];
i += 4;
v16 += 3;
}
```
##### 那此时我们双击Str查看具体内容是啥:
#### 这不就是我们的Base6编码表码?先说一下什么是Base64编码以及如何实现的:
#### Base64是一种以64个可打印字符对二进制数据进行编码的编码算法。Base64在对数据进行编码时以三个8位字符型数据为一组,取这三个字符型数据的ASCII码,然后以6位为一组组成4个新的数据,这4个新的数据有6位,所以它的最大值为2^6=64。我们以4个6位数据的十进制数从Base64表中得到最终编码后的字符。(引用CSDN@Alen.Wang)
#### Base64编码表:
| Value | Char | Value | Char | Value | Char | Value | Char |
| ----- | ---- | ----- | ---- | ----- | ---- | ----- | ---- |
| 0 | A | 16 | Q | 32 | g | 48 | w |
| 1 | B | 17 | R | 33 | h | 49 | x |
| 2 | C | 18 | S | 34 | i | 50 | y |
| 3 | D | 19 | T | 35 | j | 51 | z |
| 4 | E | 20 | U | 36 | k | 52 | 0 |
| 5 | F | 21 | V | 37 | l | 53 | 1 |
| 6 | G | 22 | W | 38 | m | 54 | 2 |
| 7 | H | 23 | X | 39 | n | 55 | 3 |
| 8 | I | 24 | Y | 40 | o | 56 | 4 |
| 9 | J | 25 | Z | 41 | p | 57 | 5 |
| 10 | K | 26 | a | 42 | q | 58 | 6 |
| 11 | L | 27 | b | 43 | r | 59 | 7 |
| 12 | M | 28 | c | 44 | s | 60 | 8 |
| 13 | N | 29 | d | 45 | t | 61 | 9 |
| 14 | O | 30 | e | 46 | u | 62 | + |
| 15 | P | 31 | f | 47 | v | 63 | / |
#### 接下来我将采用举例子的方法来形象地讲解Base64加密:
#### 1.假设待加密的字符串为ABCDEF(先用字符长度为3的倍数的字符串讲解,最后会说字符长度不是3的情况);
#### 2.将ABCDEF六个字符换位二进制,分别为:01000001、01000010、01000011、01000100、01000101、01000110;
#### 3.将上一步每三个字节的二进制分为一组,得到6/3=2组,分别是:010000010100001001000011、010001000100010101000110
#### 4.将上一步得到的两组二进制数的每6位为一小组,4小组为一大组分,得到3x8/6/x2=8小组,两大组,分别是:010000、010100、001001、000011、010001、000100、010101、000110;
#### 5.将这8组二进制数转为十进制数,分别为:16、20、9、3、17、4、21、6;
#### 6.用得到的这六个数去匹配Base64编码表的值便可以得到ABCDEF经过Base64的结果,也就是QUJDREVG;
#### 7.在第4步得知我们的密文长度必须是4的倍数,或者你可以制定运算规则使得明文始终是3的整数倍但是满足Base64编码的规则;
#### 上面我们说的明文是6个字节长度的,也就是3的两倍,根据第七步可以知道密文的字节长度必须要是4的倍数,也就是说只要明文的字节长度不是三的倍数且在运算中不进行特殊处理,我们得到的密文就一定不是4的倍数,那么我们就无法得到密文,所以这里我们需要处理:假设我们需要加密的字符为A,那按照上面的步骤一得到了其二进制数01000001(这一步分到几组算几组,我们接下来会有办法),按照步骤二得到了两组010000、010000,我们说过第二步的原则是每六位为一小组,需要分为四大组,那我们这里只有两小组,怎么办呢?那我们就增加两小组让其小组数达到当前实际组数距离四的最小公倍数,这里的实际组数是2,那其距离四的最小公倍数就是4,那就再增加两小组,那增加的两组我们怎么定义呢?我们规定将所有增加的组数的值都视为N,接下来进行第三步操作的时候我们只需要将实际小组数的值转换为十进制数,增加的直接作为密文,其值字符是’=‘,所以A的两个实际小组的十进制数分别为:16、16,对位的编码表值为QQ,那再加上增加小组的两个=,那么就得到了A的Base64加密结果QQ==;
#### 也就是说在密文的字节长度不满足为3的整数倍的时候,那么我们就在第三步增加小组数量使其小组数达到当前实际小组数距离四的最小公倍数,实际小组按照步骤转换编码,增加小组直接作为=字符作密文;
#### 注意:上面最后说的明文字节长度不是3的倍数的方法只是为了让大家生动理解我想出的一种理解方式,而不是Base64的运算规则!
#### 到这里,再次回来查看我们的代码:
```
while(v16<42)
{
*(_BYTE *)(a2 + i) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];
v11 = 16 * *(_BYTE *)(v16 + a1) & 0x30;
*(_BYTE *)(a2 + v7) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF | v11];
v12 = 4 * *(_BYTE *)(v16 + 1 + a1) & 0x3C;
*(_BYTE *)(a2 + i+2) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];
*(_BYTE *)(a2 + i+3) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];
i += 4;
v16 += 3;
}
```
#### 在这里:a2是密文,a1是明文,Str是Base64编码表,那我将讲解第一轮循环的具体细节,因为接下来的13轮原理完全和这个一样
`*(_BYTE *)(a2 + i) = Str[((signed int)*(_BYTE *)(v16 + a1) >> 2) & 0x3F];`
#### 将输入的字符的ASCLL码值右移2位,相当于取了该ASCLL码值二进制的前6位(总共8位)做新数, 0x3F = 00111111,那将取到的前六位与运算0x3F根据运算规则这里相当于是没有任何意义的,最后的结果就是取到的前六位做新数
```
假设前六位 00101101
0x3F 00111111
假设前六位 & 0x3F = 00101101
```
`v11 = 16 * *(_BYTE *)(v16 + a1) & 0x30;`
#### v11等于a1第一个字符ASCLL码值x16,那这个乘以16相当于是啥呢?二进制左移一位加一个0,左移N位就相当于增加了N个0,也就相当于变为原来数的2^N倍,那变为16倍数是不是相当于左移了4位,也就是相当于是取了一个字符ASCLL码的后四位做新数,那再进行与运算0x30(二进制:00110000),相当于再取左移四位得到的这个数的后两位做新数,合起来就是相当于取了a1第一个字符ASCLL码值的最后两位做新数
`*(_BYTE *)(a2 + v7) = Str[((signed int)*(_BYTE *)(v16 + 1 + a1) >> 4) & 0xF | v11];`
#### 将a1第二个字符ASCLL码值右移四位,相当于取前4位做新数,再与运算0xF(二进制:00001111)相当于取这右移四位的值的后四位做新数,合起来相当于就是取了第二个字符ASCLL码值的前四位做新数,那再与运算v11就相当于是a1第一个字符ASCLL码值的后两位做新数与第二个字符ASCLL码值的前四位做新数连接起来(与运算)得到一个新数,例如:后两位是11,前四位是0101,那新数就是110101(00110101)
`v12 = 4 * *(_BYTE *)(v16 + 1 + a1) & 0x3C;`
#### 将a1第二个字符ASCLL码值乘以4,按照之前的公式来算就是相当于a1第二个字符ASCLL码值左移两位,也就是取后六位做新数,那再与运算0x3C(而进制:00111100),也就相当于再取a1第二个字符ASCLL码值左移两位得到新数的第三到第六位,合起来就相当于取了a1第二个字符ASCLL码值的后四位做新数
`*(_BYTE *)(a2 + i+2) = Str[((signed int)*(_BYTE *)(v16 + 2 + a1) >> 6) & 3 | v12];`
#### 将a1第三个字符ASCLL码值乘以右移6位,相当于取前两位做新数,再与运算3(二进制:011)就相当于取了a1第三个字符ASCLL码值的前两位,再与运算v12,合起来也就是将a1第二个字符ASCLL码值取后四位数做新数与a1第三个字符ASCLL码值的前两位连接得到新数,和上面说的一样
`*(_BYTE *)(a2 + i+3) = Str[*(_BYTE *)(v16 + 2 + a1) & 0x3F];`
#### 将a1第三个字符ASCLL码值与运算0x3F(二进制:00111111),相当于取a1第三个字符ASCLL码值得后六位
### 一次循环结束:一次循环总共产生了3*8位bit位,经过4次操作,每次操作6位bit位,3x8 = 4x6,也就是说每次循环使用3个明文处理产生四个新数做Base64表的下角标来进行编码得到的编码值作为密文赋值给密文数组
#### 到这里了也就是说明a1是明文,a2是密文,Str是Base64编码表,因为a2的值我们知道那直接用网上的解Base64工具便可以得到明文
#### 居然解码失败了,那逻辑我们分析的一清二楚,唯一的问题只有可能是编码表变异了,再次双击Str
#### 细节呀!该编码表的直接引用只有sub_401536函数,而刚才Str出现的地方是sub_4015F7函数,所以Str并不是直接引用Base64原表,那去分析sub_401536函数
```
int sub_401536()
{
int result; // eax@1
char v1; // ST13_1@5
signed int v2; // @2
int j; // @3
int i; // @2
result = dword_406060;
if ( !dword_406060 )
{
v2 = strlen(Str);
for ( i = 0; v2 / 2 > i; ++i )
{
for ( j = 0; v2 - i - 1 > j; ++j )
{
if ( Str > Str )
{
v1 = Str;
Str = Str;
Str = v1;
}
}
}
result = dword_406060++ + 1;
}
return result;
}
```
#### 果真是变了编码表,那我们写脚本看一下变化了的编码表值是多少?
#### 脚本:
```
#include <stdio.h>
int main()
{
char Bs =
{
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54,
0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64,
0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x2B, 0x2F
};
int i = 0;
int j = 0;
int v2 = 64;
int v1 = 0;
for ( i = 0; v2 / 2 > i; ++i )
{
for ( j = 0; v2 - i - 1 > j; ++j )
{
if ( Bs > Bs )
{
v1 = Bs;
Bs = Bs;
Bs = v1;
}
}
}
for(int k=0;k<64;k++){
printf("%c",Bs);
}
}
```
### 那我们现在只需要用这个表手写脚本逆推4x6->3x8就可以得到刚才的明文a1了
#### 脚本:
```
#include <stdio.h>
#include <string.h>
const char * base64char = "ABCDEFGHIJKLMNOPQRST0123456789+/UVWXYZabcdefghijklmnopqrstuvwxyz";
inline int num_strchr(const char *str, char c)
{
const char *pindex = strchr(str, c);
if (NULL == pindex){
return -1;
}
return pindex - str;
}
int base64_decode(const char * base64, unsigned char * dedata)
{
int i = 0, j=0;
int trans = {0,0,0,0};
for (;base64!='\0';i+=4){
// 每四个一组,译码成三个字符
trans = num_strchr(base64char, base64);
trans = num_strchr(base64char, base64);
// 1/3
dedata = ((trans << 2) & 0xfc) | ((trans>>4) & 0x03);
if (base64 == '='){
continue;
}
else{
trans = num_strchr(base64char, base64);
}
// 2/3
dedata = ((trans << 4) & 0xf0) | ((trans >> 2) & 0x0f);
if (base64 == '='){
continue;
}
else{
trans = num_strchr(base64char, base64);
}
// 3/3
dedata = ((trans << 6) & 0xc0) | (trans & 0x3f);
}
dedata = '\0';
printf("长度是:%d\n",strlen((const char*)dedata));
printf("解密后得到:");
for(int m=0;m<42;m++){
printf("%d--",dedata);
}
return 0;
}
int main()
{
char dedata;
base64_decode("7G5d5bAy+TMdLWlu5CdkMTlcJnwkNUgb2AQL3CcmPpVf6DAp72scOSlb", (unsigned char*)dedata);
return 0;
}
```
#### 此时的明文绝对不是flag,再次回到check函数中去找左值
```
int Check()
{
char v1; // @1
char Dst; // @4
int j; // @4
int i; // @1
sub_4021AD(22, 18);
scanf("%s", v1);
for ( i = 0; v1; ++i )
;
sub_4017D2(v1, i);
memset(Dst, 0, 0x800u);
sub_4015F7((int)v1, (int)Dst, i);
sub_4021AD(22, 20);
for ( j = 0; Dst; ++j )
{
if ( Dst != a7g5d5bayTmdlwl )
return puts("不对哦~下次再来吧~");
}
return puts("哇!你真厉害!");
}
```
#### sub_4017D2函数还有一个加密,跟进去
```
int __cdecl sub_4017D2(int a1, signed int a2)
{
int result; // eax@7
signed int j; // @2
signed int i; // @1
for ( i = 1; i <= 10; ++i )
{
for ( j = 0; ; ++j )
{
result = *(_BYTE *)(j + a1);
if ( !(_BYTE)result )
break;
if ( a2 % i )
*(_BYTE *)(j + a1) ^= (_BYTE)i + (_BYTE)j;
else
*(_BYTE *)(j + a1) ^= (unsigned __int8)(j % i) + (_BYTE)j;
}
}
return result;
}
```
#### 该函数的执行完的a1我们已经得到了,那就根据它写脚本逆推就行了,这个加密太简单了,就只有异或而已,那逆运算在异或回去就行了,直接写脚本(i已经在base64变异表加密那里推出来了,i=42(解密那一步也得到了)),这一步解密的函数名叫GetFlag
```
#include <stdio.h>
#include <string.h>
#include <windows.h>
const char * base64char = "ABCDEFGHIJKLMNOPQRST0123456789+/UVWXYZabcdefghijklmnopqrstuvwxyz";
int num_strchr(const char *str, char c)
{
const char *pindex = strchr(str, c);
if (NULL == pindex){
return -1;
}
return pindex - str;
}
int base64_decode(const char * base64, unsigned char * dedata)
{
int i = 0, j=0;
int trans = {0,0,0,0};
for (;base64!='\0';i+=4){
// 每四个一组,译码成三个字符
trans = num_strchr(base64char, base64);
trans = num_strchr(base64char, base64);
// 1/3
dedata = ((trans << 2) & 0xfc) | ((trans>>4) & 0x03);
if (base64 == '='){
continue;
}
else{
trans = num_strchr(base64char, base64);
}
// 2/3
dedata =((trans << 4) & 0xf0) | ((trans >> 2) & 0x0f);
if (base64 == '='){
continue;
}
else{
trans = num_strchr(base64char, base64);
}
// 3/3
dedata = ((trans << 6) & 0xc0) | (trans & 0x3f);
}
dedata = '\0';
printf("长度是:%d\n",strlen((const char*)dedata));
printf("解密后得到:");
for(int m=0;m<42;m++){
printf("%x--",dedata);
}
return 0;
}
void GetFlag(char* Flag)
{
int result; // eax@7
signed int j; // @2
signed int i; // @1
for (int i = 10; i >= 1; --i )
{
for (int j = 0; ; ++j )
{
char result = (unsigned char)Flag;
if ( !(unsigned char)result )
break;
if ( 42 % i )
Flag ^= ((unsigned char)i + (unsigned char)j);
else
Flag ^= ((unsigned char)(j % i) + (unsigned char)j);
}
}
printf("%s",Flag);
printf("\n");
for(int i=0;i<42;i++){
//TODO
printf("\%c",Flag);
}
}
int main()
{
char dedata;
base64_decode("7G5d5bAy+TMdLWlu5CdkMTlcJnwkNUgb2AQL3CcmPpVf6DAp72scOSlb", (unsigned char*)dedata);
char Flag ;
for(int i=0;i<42;i++)
{
Flag=dedata;
}
GetFlag(Flag);
return 0;
}
```
### 得到Flag:
Snake下载地址:https://fbxy.lanzouf.com/iaZF501zso6f Panel 发表于 2022-3-24 11:53
@Hmily h大,为啥我以前发的好像一发就成功了,现在发要审核,是因为什么原因违规导致的吗?
不是,文中涉及了一些关键词过滤,算是误报,但又无法解决,所以先这样,人工审核一下就ok。 Hmily 发表于 2022-3-24 12:04
不是,文中涉及了一些关键词过滤,算是误报,但又无法解决,所以先这样,人工审核一下就ok。
好的,理解,审核幸苦了 @Hmily h大,为啥我以前发的好像一发就成功了,现在发要审核,是因为什么原因违规导致的吗? 学到了,学到了 谢谢,学习了,先收藏慢慢盘{:1_927:} 支持支持 nbnb 冲冲冲{:1_919:} 持续学习中,感谢分享 这个 Snake 程序在哪儿下载呢?