写在前面
一年前在国外看到的 一篇文章 ,也是因为这个脚本,让我喜欢上了 Python。
代码只有一行,有人看了会爱上 Python ,比如像我这么萌萌哒的人。
今天突然想起来了,就靠着记忆重新写了一个。
完成后的代码如下,我已经大概处理了一下,为了好看:
1 | #!/usr/bin/env python |
怎么样?是不是吓到了。当时我感叹:逼还是你会装啊。
好吧来看分析吧。
功能及要求
这个脚本的效果就是在屏幕上输出一个字符串,比如我们最熟悉的I love Medici.Yan!
要求是:
- 不能使用 printf echo 等字符串输出的函数
- 不能出现相关的字符串
实现
Step 1
不能出现 print echo 这些函数,那么我们就可以使用 write 方法,
write(1, 'I love Medici.Yan!\n')
, 1 是什么?1 就是类 Unix 系统下的标准输出,在类 Uninx 系统上,所有的设备都是文件,所以你向这个文件写东西,就会显示到屏幕上。那么在 Python 里面,我们用到了 os 这个模块,
1
2import os
os.write(1, 'I love Medici.Yan\n')但是,又不能直接出现
'I love Medici.Yan\n'
这个字符串,应该怎么办呢?这里我们可以使用每个字母对应的 ASCII 码,然后通过 chr 来将其转换为字符
比如要输出一个
I
1
os.write(1, chr(73))
这种一堆的 chr 显然很不优雅,那么我们考虑用一个大数来存储每一位的字符的 ASCII 码数值.
公式是这样的:
(某字符的 ASCII 码) * 256^(字符的位置)
然后取其和。
1 | words = list("I love Medici.Yan\n") |
Step 2
我们已经把字符串藏在了那个大数里面,那么写一个转换函数,通过递归把字符输出
1 | import os |
Step 3
嗯,我们把这个函数写成 lambda 表达式,lambda 在 Python 里面也可以看作是匿名函数。
1 | import os |
这么调用是没问题的,但是太容易让人看懂了,怎么办?
Python 有个内置方法叫 getattr,用于返回一个对象属性,或者方法
我们知道 import 语句是用来导入外部模块的,当然还有
from...import...
也可以,但是其实 import 实际上是使用 builtin 函数__import__
来工作的
于是上面这段代码,我们可以写成:
1 | comb = lambda f, n: f(f,n) |
写成一行吧
1 | getattr(__import__('os'), 'write')(1, (lambda f,n: f(f,n))(lambda f,n: chr(n % 256)+f(f, n // 256) if n else "", 908683317850257809145804104980513101193289)) |
是不是有点感觉了,来,继续
Step 4
我们知道 __import__('os')
这里的 os 是个字符串,直接写个 os 在那里容易就被人看出是做什么的了,那么我们就可以使用字符串拼接的方法来构造 os 和 write 这两个字符串。
1 | >>> True.__class__.__name__ |
- 我们用 bool 的 o 和 list 的 s 来拼接成一个 os
- 取 wrapper_descriptor 的前两个字符 wr 还有 tupleiterator 的 iter 拼成 writer
- 把 lambda 局部变量的名字改成 _ 和 __ 来降代可读性
- convert 返回的空字符串 “” 用 (lambda:0).func_code.co_lnotab 来代替。
func_code.co_lnotab 是查找函数中代码对象的行号表,我们用的是匿名函数,就没有行号,于是返回的就是空
那么现在我们的代码就写成这个样子了:
1 | getattr(__import__(True.__class__.__name__[1] +[].__class__.__name__[2]),().__class__.__eq__.__class__.__name__[:2] + ().__iter__().__class__.__name__[5:8])(1, (lambda _,__:_(_,__))(lambda _,__: chr(__%256) + _(_, __//256) if __ else (lambda: 0).func_code.co_lnotab, 908683317850257809145804104980513101193289)) |
嗯,看起来已经很不错了。我们下面要做的,就是把数字也藏起来,让读代码的人更加抓狂。
Step 5
现在我们要做的就是在代码里面混淆数字,试着想一下如果每次都重新混淆一次这个数字的话,太痛苦了吧。那么我们就可以把 0-9 这些数字混淆后,再通过局部变量的方式传进来,这样不就可以了?
于是我们的代码就可以写成下面这种形式了:
1 | (lambda n1,n2,n3,n4,n5,n6,n7,n8: |
于是我们现在要做的,就是想办法实现 range(1,9) 就可以了,这 8 个数字,完全可以通过四则运算组成我们的 256, 908683317850257809145804104980513101193289 还有 5,8
我们可以通过函数的代码对象来获取它的参数个数
1 | >>> (lambda a, b, c: 0).func_code.co_argcount |
于是思路就明朗了,我们只要改变参数的个数为 1-8,就能生成我们的 range(1,9) 了。
构造一个元组,元素为 参数个数为 1-8 的函数。
1 | funcs = ( |
然后通过一个递归函数把这个元组输出成 range(1,9):
1 | >>> def convert(L): |
嗯,写成一行:
1 | convert = lambda L: [L[0].func_code.co_argcount] + convert(L[1:]) if L else [] |
再转成匿名递归函数形式:
1 | >>> (lambda f, L: f(f, L))( |
下面就是 怎么隐藏 0 和 1 了。我们可以通过 检查任意函数的局部变量的个数来得到 0 和 1。
1 | >>> (lambda: _).func_code.co_nlocals |
于是我们现在的代码就成了下面的这个样子了。别急,256 和 908683317850257809145804104980513101193289 这两个数字还没有替换呢。
1 | (lambda _,__,___,____,_____,______,_______, ________: |
嗯,我们现在已经有了 1-8 这 8 个数字了,怎么用它来表示成这两个数字呢?
256 比较好办,我们用 4<<6
就可以表示成 256 了,于是 在上面代码中 256 的位置我们就可以表示成 ____<<______
当然,1<<8 的结果也是 256,那么完全可以写成_<<________
没有固定的答案。
对于 908683317850257809145804104980513101193289 这个大数,原文作者提供了一个拆分的方法:
1 | from math import ceil, log |
这个算法不是特别难懂,就是测试在一个确定区间的不同的数字组合,直到我们找到 一个组合使得以一个为基数,一个为移位长度,然后是最接近 num 的(也就是 他们差的绝对值最小)。
然后我们执行
1 | >>> convert(908683317850257809145804104980513101193289) |
得到了一个很理想的分解,于是用这个来替换。
最终结果
再回过头来看这段代码,是不是清晰了很多?逼还是你会装啊。
1 | #!/usr/bin/env python |