破解pyfuck代码的背后:如何掌握Python的命名空间与作用域

引言

昨天偶然间看到一个代码混淆技巧,研究了一下,挺有意思的,分享给大家。

jsfuck

说到 jsfuck,做爬虫的小伙伴应该都听说过,还是挺有意思的一种混淆方案,只是表面效果拉满,实际效果不太行,解密比较方便,不适合作为日常混淆方案。

不过还是可以用来耍耍帅哈哈,对 js 不了解的话,短时间内是无法解密的。

来看下效果:image-20241226211054568

上面代码可以执行的原理是因为 js 是一种弱类型的语言,所以可以通过下面的类型方式进行替换,从而通过各种拼接实现执行任意代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
false       =>  ![]
true => !![]
undefined => [][[]]
NaN => +[![]]
0 => +[]
1 => +!+[]
2 => !+[]+!+[]
10 => [+!+[]]+[+[]]
Array => []
Number => +[]
String => []+[]
Boolean => ![]
Function => []["filter"]
eval => []["filter"]["constructor"]( CODE )()
window => []["filter"]["constructor"]("return this")()

jsfuck 已经出来很久了,有很多的资料可以研究,感兴趣的小伙伴可以自行尝试,放一个 jsfuck 的网站,网站上面有对应的原理和源代码。

jsfuck 解密方案

将代码放到浏览器中运行,运行结束后,结果后面会显示一个 VMxxx 的链接,点击这个链接,就会跳到真正解密后的代码了,因为这个代码才是真正发送给 js 引擎执行的代码,会在这里显示出来。

image-20241226211456690

image-20241226211505663

pyfuck

说完了 jsfuck,该进入正题了,本篇文章我们来说一下 pyfuck,没想到吧,python 也可以这么玩,一种强类型的语言,也可以通过八个基本的字符来执行任意代码,属实是比较神奇的。

先准备一段示例代码:

1
2
3
4
import requests

r = requests.get('https://www.baidu.com')
print(r.text[:130])

将代码通过 pyfuck 网站进行转换,得到以下代码,执行看看:

image-20241226212035159

可以看到,代码被成功执行,并且整个过程没有报错,而且混淆后的代码还支持换行等格式化操作。

当然了,python 的 pyfuck 混淆其实和 jsfuck 差不多,也是表面效果拉满,但是实际混淆效果一般,因为 pyfuck 也是很好解密的,下面我们来尝试解密它。

pyfuck 解密方案 1

要想解密 pyfuck,先观察代码结构,可以看到所有的代码都是由 == 连起来的,并且所有的代码都是放到 exec 中来执行的,将所有的等号换成回车看看:

image-20241226213206619

现在看起来比较明显了,其实就是通过 exec 来一行一行的执行一个字符串,并且使用等号将所有的 exec 连接起来。

那么通过对 exec 的了解和上面的结论,我们可以提出一个大胆的猜想,既然最后能成功执行我们的代码,那必然会在前面一步一步的解密并且拼接成我们输入的代码,给到最后一个 exec 执行才可以。exec 中的字符串,其实就是通过 %c%x 来格式化字符串,并且定义多个变量,将多个变量进行各种拼接,最终生成我们输入的代码。

有了想法,开始验证,在所有代码的最后,添加一个 print,输出最后的 exec 执行的变量,看结果:

image-20241226213656706

大功告成!代码成功的被输出出来了,我们成功的解密了 pyfuck!

pyfuck 解密方案 2

刚才是解密方案 1,方案 1 的话需要自己修改代码并且将等号去掉才可以,稍微有点麻烦,我们可以使用第二个方案,hook 大法。

先准备我们自己的 exec 函数,替换掉系统的 exec,输出需要执行的code 之后再执行原来的 exec

添加以下代码:

1
2
3
def exec(code):
__builtins__.exec(code)
print(code)

运行查看结果:

image-20241226223627147

发现报错了,只执行了两条 code,为什么?

命名空间和作用域

观察报错代码,发现是第三个 exec 代码报错了,e 这个变量未定义,但是我们看第二行的 code,e 已经定义过并且已经被赋值了,那么为什么还会报错 e 未定义呢?

这里就涉及到一个命名空间和作用域的问题了。

全局命名空间

因为 exec 每次执行的代码是一个字符串,那么这个时候就涉及到一个执行环境或者上下文的问题,代码中如果定义了一个变量,那么去哪里去找这个变量呢?

肯定不能是当前执行 exec 的上下文,因为这样会污染当前的环境,如果执行了外部代码可能会导致安全问题或者改变当前环境中的某个值导致权限失效或者其他问题,所以 exec 提供了两个参数:

1
exec(source, /, globals=None, locals=None, *, closure=None)

分别是 globalslocals,分别是全局命名空间和本地命名空间,关于这两个参数大家看官方文档自己尝试一下就很容易理解了,这里就不多解释了。

现在来解答上面的问题,为什么会报错 e 变量找不到呢,因为在执行 exec 之前,先要对字符串进行格式化,在格式化的时候,需要在当前模块的全局命名空间中查找 e 这个变量,但是 e 这个变量是在 exec 中定义的,因为安全保护等原因,我们无法在 exec 之外访问到 exec 内部定义的变量,所以这就会导致在外面格式化变量的时候报错找不到变量。

那如何解决这个问题呢?很简单,将 exec 内部定义的变量保存起来,更新到当前模块的全局命名空间就可以了,应该如何操作呢?来看代码:

1
2
3
def exec(code):
__builtins__.exec(code, globals())
print(code)

解释一下,globals 函数会输出全局命名空间的所有变量,在 exec 执行的时候,会将定义的变量更新到 globals 函数的结果(结果是一个字典)中,函数的结果就是当前模块的全局命名空间,这样的话,在后面进行字符串格式化的时候就可以正常找到 exec 中定义的变量了,代码也不会报错了。

如果不好理解的话,看下面的代码:

1
2
3
g = globals()
g['aaa'] = 111
print(aaa)

上面的代码是可以正常执行的,为什么呢,因为我们手动在当前模块的全局命名空间内设置了 aaa 的值,即使没有明确定义 aaa 变量,在后续访问 aaa 的时候仍然可以找到 aaa 变量,可以正常输出它的值。上面的解决方案也是一样的道理,在 exec 执行的时候,会将变量赋值的结果保存到全局命名空间中,这样就不会让后面的字符串格式化操作报错了。

来看最后的执行效果:

image-20241226232134498

总结

jsfuck 和 pyfuck 还是很有趣的,感兴趣的小伙伴可以自行转换一下玩一玩,还是可以学到不少东西的。

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
更多文章请关注:qrcode
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接
https://blog.d77.xyz/archives/1623ec35.html