先看运行结果——一个 32KB 的 C++ 程序,运行后在屏幕上打出一句话:

1
嵌入式基地YYDS

这句话在源代码里一个汉字都没有。它藏在哪?藏在一个 int 数组里,而且数组里的每个数字,用的还是不同的进制。完整源码如下,请注意开头的 #define eval 和数组里反复出现的 eval(...)——后面会讲它们是干嘛的:

加密中文demo.cpp
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
#include <stdio.h>
#include <locale.h>

#define eval(everying) everying

int main()
{
// int c = 10;
// int d = 5;

// int rs3 = c++ + ++c - --d - ++d + 1 + c--;
// c++ + ++c = 10 + &c = 10 + 12 = 22
// 22 - --d = 22 - 4 = 18
// 18 - ++d = 18 - 5 = 13
// 13 + 1 = 14
// 14 + c-- = 14 + 12 = 26
// printf("%d\n", rs3);


// int array1[] = {23884, 0x5F0F, 0x5730, eval((int)) - 037777777647, 0123, 0404067, 0b0101011111111010, 0};
// int array1[] = {23884, (0b0100 << 0b0100) | 0b0100, 0x5730, 0404067, 0123, eval((int)) - 0xffffffffa7, 0x5F0F, 1 +~ 0 , eval((int)) - 037777777647, 0b0101011111111010};
int array1[] = {23884, (0b0100 << 0b0100) | 0b0100, 0x5730, 050545, 0123, eval((int)) - 0xffffffffa7, 0x5F0F, 1 +~ 0 , eval((int)) - 037777777647, 0b0101011111111010, 0};
unsigned short magic_shortArray[sizeof(array1)];
int cnt = 0;
for (int i = 0; i != 10; i += 3, i > 10 ? i %= 10 : i)
{
printf("%d | ", array1[i]);
magic_shortArray[cnt++] = (unsigned short)array1[i];
}
setlocale(LC_ALL, "");
wprintf(L"%ls\n", magic_shortArray);
return 0;
}

先处理掉最碍眼的 eval#define eval(everying) everying(连参数名都拼错了,将错就错)是一个恒等宏——eval(什么) 展开后还是什么,什么都不做。它的作用纯粹是当语法烟雾弹:让 eval((int)) - 037777777647 这种句子看起来像在调用什么神秘函数,其实删掉 eval(...) 剩下的就是普通的强制转换和数字。这个 eval 在eval 宏与魔法数字里还有更疯的用法,这里先记住一条:看到 eval,直接脑内删除,剩下的就是真代码

解码:每个数字都是一个字符

array1 里的十个数字,就是输出那句话的十个字符(5 个汉字 + YYDS + 终止符),按输出顺序逐个解码:

输出值 十六进制 字符
23884 0x5D4C
20837 0x5165
24335 0x5F0F
22522 0x57FA
22320 0x5730
89 / 89 0x59 Y、Y
68 / 83 0x44 / 0x53 D、S
0 0x0000 \0

原理一句话:Windows 里 wchar_t 是 16 位,一个 unsigned short 恰好装下一个 UTF-16 编码单元——汉字的 Unicode 码位(比如”嵌”是 U+5D4C)塞进 unsigned short,再用 wprintf%ls 按”宽字符串”打印,中文就凭空出现了。

而源代码里那些数字的书写方式才是真正的行为艺术:68 写成 (0b0100 << 0b0100) | 0b0100(二进制移位或);83 写成八进制 0123;20837 写成 050545;22522 写成 0b0101011111111010(十六位二进制);甚至还有 eval((int)) - 0xffffffffa7 这种看着像内存地址的狠活,以及 1 +~ 0(1 加上按位取反的 0——正好是终止符 \0!)。十进制、十六进制、八进制、二进制、表达式,五种写法混编一个数组——数字还是那些数字,但没人第一眼能看出来它是一句话。

那个诡异的下标循环

取数的循环也值得单独立照:

1
for (int i = 0; i != 10; i += 3, i > 10 ? i %= 10 : i)

每次 i += 3,越过 10 就 i %= 10 折回来——0、3、6、9、(12→2)、5、8、(11→1)、4、7,到 10 恰好退出——在 0 到 9 之间乱中有序地跳,不重不漏走完十个下标。这是把”洗牌”写成了数学。

正经话

这个技巧的正式名字叫码位直存:不经过任何字符串字面量,直接把 Unicode 码位当数字写进数组。它的正经用途是绕开源码编码问题(把字符串做成与源文件编码无关的数据)、做简单的字符混淆,以及——在不知道 %ls 和宽字符规则的时候,用一晚上调出一行”嵌入式基地YYDS”。

顺带一提,这个 demo 还有一个更疯的续作:同样的把戏,但数字的下标、循环步长、甚至用来打印的函数指针,全部用宏和地址运算伪装起来——那是下一篇文章的主角。

系列其他文章见抽象代码宣言