查看原文
其他

脚本代码混淆-Python篇-pyminifier(2)

七夜安全 七夜安全博客 2022-12-06

微信公众号:七夜安全博客 

关注信息安全技术、关注 系统底层原理。问题或建议,请公众号留言。

前言

上文中,我们讲解了pyminifier中简化和压缩代码的功能。本篇作为第二篇,也是最终篇,讲解一下最重要的功能:代码混淆,学习一下这个项目的混淆策略。大家如果觉得不错的话,一定要分享到朋友圈哈,写了快5000字,基本上每一个细节都给大家拆分出来了,贴了一部分关键代码,会长一些,一定要有耐心哟。

一.混淆效果

在讲解混淆策略之前,先看一下混淆的效果,恶不恶心,哈哈。对比着混淆的结果,再结合我的讲解,会理解地更加深入。

原始代码

专门设计了一段代码,基本上涵盖了经常出现的语法内容。

  1. import io

  2. import tokenize


  3. abvcddfdf = int("10")

  4. def enumerate_keyword_args(tokens=None):

  5. keyword_args = {}

  6. inside_function = False

  7. dsfdsf,flag = inside_function,1

  8. a = str(flag)

  9. for index, tok in enumerate(tokens):

  10. token_type = tok[0]

  11. token_string = tok[1]

  12. a = str(token_string)

  13. b=a

  14. if token_type == tokenize.NAME:

  15. if token_string == "def":

  16. function_name = tokens[index+1][1]

  17. keyword_args.update({function_name: []})

  18. elif inside_function:

  19. if tokens[index+1][1] == '=': # keyword argument

  20. print(api(text=token_string))

  21. keyword_args[function_name].append(token_string)

  22. def api(text):

  23. print(text)


  24. def listified_tokenizer(source):

  25. io_obj = io.StringIO(source)

  26. return [list(a) for a in tokenize.generate_tokens(io_obj.readline)]


  27. code = u'''

  28. def api(text):

  29. print(text)

  30. '''

  31. abcd=1212

  32. bcde=abcd

  33. cdef=(abcd,bcde)

  34. defg=[abcd,bcde,cdef]

  35. efgh = {abcd:"cvcv","b":"12121"}

  36. f12212="hhah"

  37. f112122="hheeereah"

  38. tokens_list = listified_tokenizer(code)

  39. print(tokens_list)

  40. enumerate_keyword_args(tokens_list)

混淆后的效果


  1. #!/usr/bin/env python

  2. #-*- coding:utf-8 -*-

  3. 흞=int

  4. ݽ=None

  5. ﮄ=False

  6. ﻟ=str

  7. 嬯=enumerate

  8. 눅=list

  9. import io

  10. ﭢ=io.StringIO

  11. import tokenize

  12. ﰅ=tokenize.generate_tokens

  13. ނ=tokenize.NAME


  14. 嘢 = 흞("10")

  15. def ܪ(tokens=ݽ):

  16. 蘩 = {}

  17. ﭷ = ﮄ

  18. dsfdsf,flag = ﭷ,1

  19. a = ﻟ(flag)

  20. for ﶨ, tok in 嬯(tokens):

  21. ﶗ = tok[0]

  22. ﯢ = tok[1]

  23. a = ﻟ(ﯢ)

  24. b=a

  25. if ﶗ == ނ:

  26. if ﯢ == "def":

  27. 齬 = tokens[ﶨ+1][1]

  28. 蘩.update({齬: []})

  29. elif ﭷ:

  30. if tokens[ﶨ+1][1] == '=': # keyword argument

  31. print(ݖ(text=ﯢ))

  32. 蘩[齬].append(ﯢ)

  33. def ݖ(ﲖ):

  34. print(ﲖ)


  35. def ﰭ(source):

  36. د = ﭢ(source)

  37. return [눅(a) for a in ﰅ(د.readline)]


  38. ﳵ = u'''

  39. def api(text):

  40. print(text)

  41. '''

  42. 횗=1212

  43. ﮪ=횗

  44. ﲊ=(횗,ﮪ)

  45. 딲=[횗,ﮪ,ﲊ]

  46. ࢹ = {횗:"cvcv","b":"12121"}

  47. ﮤ="hhah"

  48. ﱄ="hheeereah"

  49. 狾 = ﰭ(ﳵ)

  50. print(狾)

  51. ܪ(狾)

二.混淆策略

pyminifier的混淆策略分成五大部分,主要是针对变量名,函数名,类名,内置模块名和外部模块进行混淆。每种混淆又分成两步,第一步是确定要混淆的内容,第二步进行内容替换,替换成随机字符。

1.变量名混淆

针对变量名的混淆,并不是所有变量名都能混淆的,因为要保证安全性,混淆过头了,程序就无法运行了。在 函数obfuscatable_variable对变量名进行了过滤,保留着可以混淆的变量名。

  1. def obfuscatable_variable(tokens, index, ignore_length=False):


  2. tok = tokens[index]

  3. token_type = tok[0]

  4. token_string = tok[1]

  5. line = tok[4]

  6. if index > 0:

  7. prev_tok = tokens[index-1]#获取上一个Token

  8. else: # Pretend it's a newline (for simplicity)

  9. prev_tok = (54, '\n', (1, 1), (1, 2), '#\n')

  10. prev_tok_type = prev_tok[0]

  11. prev_tok_string = prev_tok[1]

  12. try:

  13. next_tok = tokens[index+1]#获取下一个Token

  14. except IndexError: # Pretend it's a newline

  15. next_tok = (54, '\n', (1, 1), (1, 2), '#\n')

  16. next_tok_string = next_tok[1]

  17. if token_string == "=":# 跳过赋值 = 后面的token

  18. return '__skipline__'

  19. if token_type != tokenize.NAME:#不是变量名称忽略

  20. return None # Skip this token

  21. if token_string.startswith('__'):## __ 开头的不管,比如__init__

  22. return None

  23. if next_tok_string == ".":# 导入的模块名(已经导入的)忽略

  24. if token_string in imported_modules:

  25. return None

  26. if prev_tok_string == 'import':#导入的包名忽略

  27. return '__skipline__'

  28. if prev_tok_string == ".":#导入模块中的变量/函数忽略

  29. return '__skipnext__'

  30. if prev_tok_string == "for":#for循环中的变量如果长度大于2则进行混淆

  31. if len(token_string) > 2:

  32. return token_string

  33. if token_string == "for":# for 关键字忽略

  34. return None

  35. if token_string in keyword_args.keys():#函数名忽略

  36. return None

  37. if token_string in ["def", "class", 'if', 'elif', 'import']:#关键字忽略

  38. return '__skipline__'

  39. if prev_tok_type != tokenize.INDENT and next_tok_string != '=':

  40. return '__skipline__'

  41. if not ignore_length:

  42. if len(token_string) < 3:#长度小于3个则忽略

  43. return None

  44. if token_string in RESERVED_WORDS:#在保留字中也忽略

  45. return None

  46. return token_string

从函数中可以看到,有以下几类变量名不能混淆:

  1. token属性不是tokenize.NAME的过滤掉,例如数字token,字符串token,符号token。

  2. 以 __ 开头的名称过滤掉,例如 init

  3. 导入的第三方的模块名和变量过滤掉,例如 import os,os不能修改。

  4. for循环中的变量名长度小于等于2的过滤掉。

  5. 函数名过滤掉(接下来会有专门针对函数的处理方式)。

  6. 关键字和保留字过滤掉,长度小于3的名称也过滤掉。


确定了要混淆的内容,接下来进行替换,主要涉及replace_obfuscatablesobfuscate_variable函数,核心代码如下:

  1. if token_string == replace and prev_tok_string != '.':# 不是导入的变量

  2. if inside_function:#在函数里面

  3. if token_string not in keyword_args[inside_function]:#判断是否在参数列表中

  4. if not right_of_equal: #token所在的这一行没有 = 或者token在 = 的左边

  5. if not inside_parens: # token不在( )之间

  6. return return_replacement(replacement) # 例如 a=123 ,str.length() 中的str

  7. else:

  8. if next_tok[1] != '=':# token在( )之间 api(text) 中的 text,

  9. return return_replacement(replacement)

  10. elif not inside_parens:#token在 = 的右边,token不在( )之间 例如 a = b 中的b

  11. return return_replacement(replacement)

  12. else:#token在 = 的右边,token在( )之间

  13. if next_tok[1] != '=': #例如a=api(text) text需要改变

  14. return return_replacement(replacement)

  15. elif not right_of_equal:#token所在的这一行没有 = 或者token在 = 的左边

  16. if not inside_parens:

  17. return return_replacement(replacement)

  18. else:

  19. if next_tok[1] != '=':

  20. return return_replacement(replacement)

  21. elif right_of_equal and not inside_parens:# 例如 a = b 中的b

  22. return return_replacement(replacement)

在上述代码中可以看出,混淆变量名称需要区分作用域,即模块中的变量和函数中的变量,即使名称是一样的,但不是一回事,所以需要区分对待。通过如下三个变量进行划分:

  • inside_function 代表变量是在函数中

  • right_of_equal 代表着变量是在 = 的右侧

  • inside_parens 代表变量是在()中

大家可能奇怪,right_of_equal 和 inside_parens 是用来干什么的?其实是为了区分函数调用使用参数名的情况。例如:

  1. def api(text):

  2. print(text)


  3. api(text="123")

在函数调用的时候, api(text="123")中的text是不能混淆的,不然会报错的。

2.函数名混淆

通过obfuscatable_function函数确定要混淆的函数名称,原理上很简单,排除类似_init_的函数,然后前一个token是def,那当前的token就是函数名称。

  1. def obfuscatable_function(tokens, index, **kwargs):

  2. ......

  3. prev_tok_string = prev_tok[1]

  4. if token_type != tokenize.NAME:

  5. return None # Skip this token

  6. if token_string.startswith('__'): # Don't mess with specials

  7. return None

  8. if prev_tok_string == "def": #获取函数名称

  9. return token_string

对于函数名称的替换主要是在两个部位,一个是函数定义的时候,另一个是在函数调用的时候。函数定义的时候容易确定,函数调用的时候大体分成两种情况,一种是静态函数,另一种是动态函数,主要是要确认一下是否需要替换。具体代码位于obfuscate_function函数中:

  1. def obfuscate_function(tokens, index, replace, replacement, *args):


  2. def return_replacement(replacement):

  3. FUNC_REPLACEMENTS[replacement] = replace

  4. return replacement

  5. ......

  6. if token_string.startswith('__'):

  7. return None

  8. if token_string == replace:

  9. if prev_tok_string != '.':

  10. if token_string == replace: #函数定义

  11. return return_replacement(replacement)

  12. else:#函数调用

  13. parent_name = tokens[index-2][1]

  14. if parent_name in CLASS_REPLACEMENTS:#classmethod

  15. # This should work for @classmethod methods

  16. return return_replacement(replacement)

  17. elif parent_name in VAR_REPLACEMENTS:#实例函数

  18. # This covers regular ol' instance methods

  19. return return_replacement(replacement)

在代码的末尾 通过prev_tok_string来判断是定义函数还是调用,如果prev_tok_string!=“.”,代表着定义。

通过parent_name是否在CLASS_REPLACEMENTS VAR_REPLACEMENTS中,判断是静态函数还是动态函数,但是写的有点冗余,最后的处理方式都是一样的。

3.类名混淆

通过obfuscatable_class函数来确认要混淆的类名称,只要判断 prev_tok_string=="class" 即可。

  1. def obfuscatable_class(tokens, index, **kwargs):

  2. ......

  3. prev_tok_string = prev_tok[1]

  4. if token_type != tokenize.NAME:

  5. return None # Skip this token

  6. if token_string.startswith('__'): # Don't mess with specials

  7. return None

  8. #通过判断前一个token是class,就可以知道当前的是类名称

  9. if prev_tok_string == "class":

  10. return token_string

对于类名称的替换,这个项目进行了简化处理,无法跨模块跨文件进行混淆,这样的设定就简单了很多,关键代码在obfuscate_class函数中,其实直接就替换了,没啥复杂的。

  1. def obfuscate_class(tokens, index, replace, replacement, *args):


  2. def return_replacement(replacement):

  3. CLASS_REPLACEMENTS[replacement] = replace

  4. return replacement

  5. ......

  6. if prev_tok_string != '.': ##无法跨模块混淆

  7. if token_string == replace:

  8. return return_replacement(replacement)


4.builtin模块混淆

首先遍历token发现内置模块中的函数和类,代码中内置了 builtins表,enumerate_builtins函数通过比对里面的值来确定token是否是内置的。

  1. builtins = [

  2. 'ArithmeticError',

  3. 'AssertionError',

  4. 'AttributeError',

  5. ......

  6. 'str',

  7. 'sum',

  8. 'super',

  9. 'tuple',

  10. 'type',

  11. 'unichr',

  12. 'unicode',

  13. 'vars',

  14. 'xrange',

  15. 'zip'

  16. ]

内置模块的混淆通过赋值的方式来实现,举个例子,在Python 中有个str的内置函数,正常代码如下:

  1. sum = str(10)

混淆后:

  1. xxxx= str

  2. sum = xxxx(19)

原理如上所示,具体是通过obfuscate_builtins函数来实现的,将所有符合的内置函数/类,都转化成赋值等式,插入到token链的前面,但是有一点需要注意:新的token必须要放到解释器路径(#!/usr/bin/env python)和编码('# -- coding: utf-8 --')之后,这样才不会报错。代码如下:

  1. for tok in tokens[0:4]: # Will always be in the first four tokens

  2. line = tok[4]

  3. if analyze.shebang.match(line): # (e.g. '#!/usr/bin/env python')

  4. if not matched_shebang:

  5. matched_shebang = True

  6. skip_tokens += 1

  7. elif analyze.encoding.match(line): # (e.g. '# -*- coding: utf-8 -*-')

  8. if not matched_encoding:

  9. matched_encoding = True

  10. skip_tokens += 1

  11. insert_in_next_line(tokens, skip_tokens, obfuscated_assignments)


5.第三方模块与函数的混淆

针对第三方模块与函数的混淆,pyminifier进行了简化处理,具体逻辑在obfuscate_global_import_methods中,通过以下两种方式导入的模块忽略:

  1. import xxx as ppp

  2. from xxx import ppp

只处理 importpackage类型的导入。

枚举模块

首先通过 enumerate_global_imports 函数枚举所有通过import导入的模块,忽略了类里面和函数中导入的模块,只接受全局导入,核心代码如下:

  1. elif token_type == tokenize.NAME:

  2. if token_string in ["def", "class"]:

  3. function_count += 1

  4. if indentation == function_count - 1: #出了函数之后才会相等

  5. function_count -= 1

  6. elif function_count >= indentation: #排除了在函数内部和类内部的import导入

  7. if token_string == "import":

  8. import_line = True

  9. elif token_string == "from":

  10. from_import = True

  11. elif import_line:

  12. if token_type == tokenize.NAME and tokens[index+1][1] != 'as':# 排除 import as

  13. if not from_import and token_string not in reserved_words:#排除from import

  14. if token_string not in imported_modules:

  15. if tokens[index+1][1] == '.': # module.module

  16. parent_module = token_string + '.'

  17. else:

  18. if parent_module:

  19. module_string = (

  20. parent_module + token_string)

  21. imported_modules.append(module_string)

  22. parent_module = ''

  23. else:

  24. imported_modules.append(token_string)

遍历函数并混淆

获取导入的模块后,接着遍历token,获取源文件中模块调用的函数,和之前的方法一样通过赋值的方式进行替换,举个例子:原代码:

  1. import os

  2. os.path.exists("text")

混淆后的代码:

  1. import os

  2. ﳀ=os.path

  3. ﳀ.exists("text")

具体函数调用的替换代码很简短,module_method形如os.path,即ﳀ.exists("text")这部分:

  1. if token_string == module_method.split('.')[0]:

  2. if tokens[index+1][1] == '.':

  3. if tokens[index+2][1] == module_method.split('.')[1]:

  4. tokens[index][1] = replacement_dict[module_method]

  5. tokens[index+1][1] = ""

  6. tokens[index+2][1] = ""

接下来将替换变量进行定义,形如ﳀ=os.path,并通过insert_in_next_line函数插入到import模块的下方。有一点需要注意的是token索引index + 6,原因很简单, ﳀ=os.path\n转化为token的长度就是6。

  1. elif import_line:

  2. if token_string == module_method.split('.')[0]:

  3. # Insert the obfuscation assignment after the import

  4. ......

  5. else:

  6. line = "%s=%s\n" % ( # This ends up being 6 tokens

  7. replacement_dict[module_method], module_method)

  8. for indent in indents: # Fix indentation

  9. line = "%s%s" % (indent[1], line)

  10. index += 1

  11. insert_in_next_line(tokens, index, line)

  12. index += 6 # To make up for the six tokens we inserted

  13. index += 1



混淆源生成

从上面讲解的混淆策略中,我们大体了解了pyminifier的工作方式,但是还有一点没有讲解,那就是混淆源的生成,什么意思呢?如下所示, os.path为啥会被替换成 

  1. ﳀ=os.path

混淆源生成位于obfuscation_machine函数中,分成了两种情况。

在Py3中,支持unicode字符作为变量名称,所以基本上是使用unicode字符作为数据源,混淆后会出现各个国家的语言符号,看着着实恶心,而Py2则是使用的ASCII码的大小写作为数据源。数据源有了,然后进行随机化,让其变得更混乱一些。

代码如下:

  1. # This generates a list of the letters a-z:

  2. lowercase = list(map(chr, range(97, 123)))

  3. # Same thing but ALL CAPS:

  4. uppercase = list(map(chr, range(65, 90)))

  5. if use_unicode:

  6. # Python 3 lets us have some *real* fun:

  7. allowed_categories = ('LC', 'Ll', 'Lu', 'Lo', 'Lu')

  8. # All the fun characters start at 1580 (hehe):

  9. big_list = list(map(chr, range(1580, HIGHEST_UNICODE)))

  10. max_chars = 1000 # Ought to be enough for anybody :)

  11. combined = []

  12. rtl_categories = ('AL', 'R') # AL == Arabic, R == Any right-to-left

  13. last_orientation = 'L' # L = Any left-to-right

  14. # Find a good mix of left-to-right and right-to-left characters

  15. while len(combined) < max_chars:

  16. char = choice(big_list)

  17. if unicodedata.category(char) in allowed_categories:

  18. orientation = unicodedata.bidirectional(char)

  19. if last_orientation in rtl_categories:

  20. if orientation not in rtl_categories:

  21. combined.append(char)

  22. else:

  23. if orientation in rtl_categories:

  24. combined.append(char)

  25. last_orientation = orientation

  26. else:

  27. combined = lowercase + uppercase

  28. shuffle(combined) # Randomize it all to keep things interesting

数据源有了,那按照什么顺序输出呢?

这就用到了permutations 函数,生成迭代器,对数据进行排列组合然后输出。

  1. for perm in permutations(combined, identifier_length):

  2. perm = "".join(perm)

  3. if perm not in RESERVED_WORDS: # Can't replace reserved words

  4. yield perm


总结

pyminifier 算是一个不错的入门项目,帮助大家学习脚本混淆,但是不要用在生产环境中,bug挺多,而且混淆能力并不是很强。接下来我会接着讲解脚本混淆的技术手段,不限于python。


推荐阅读:

APT组织武器:MuddyC3泄露代码分析

Python RASP 工程化:一次入侵的思考

教你学木马攻防 | 隧道木马 | 第一课

一个Python开源项目-哈勃沙箱源码剖析(下)


如果大家喜欢这篇文章的话,请不要吝啬分享到朋友圈,并置顶公众号。

关注公众号:七夜安全博客

回复【8】:领取 python神经网络 教程 

  • 回复【1】:领取 Python数据分析 教程大礼包

  • 回复【2】:领取 Python Flask 全套教程

  • 回复【3】:领取 某学院 机器学习 教程

  • 回复【4】:领取 爬虫 教程

  • 回复【5】:领取编译原理 教程

  • 回复【6】:领取渗透测试教程

  • 回复【7】:领取人工智能数学基础


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存