python-obfuscation-framework 1.9.1__py3-none-any.whl → 1.9.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pof/obfuscator/__init__.py +2 -0
- pof/obfuscator/cipher/deep_encryption.py +77 -45
- pof/obfuscator/cipher/shift.py +2 -3
- pof/obfuscator/constants.py +2 -0
- pof/obfuscator/controlflow/__init__.py +19 -0
- pof/obfuscator/controlflow/control_flow_flatten.py +208 -0
- pof/obfuscator/esoteric/doc.py +2 -0
- pof/obfuscator/extract_variables.py +31 -67
- pof/obfuscator/remove/newline.py +3 -4
- pof/obfuscator/remove/print.py +18 -2
- pof/obfuscator/strings.py +2 -0
- pof/utils/tokens.py +58 -0
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/METADATA +191 -55
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/RECORD +18 -16
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/WHEEL +0 -0
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/entry_points.txt +0 -0
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/licenses/LICENSE +0 -0
- {python_obfuscation_framework-1.9.1.dist-info → python_obfuscation_framework-1.9.3.dist-info}/top_level.txt +0 -0
pof/obfuscator/__init__.py
CHANGED
|
@@ -25,6 +25,7 @@ from .compression.gzip import GzipObfuscator
|
|
|
25
25
|
from .compression.lzma import LzmaObfuscator
|
|
26
26
|
from .compression.zlib import ZlibObfuscator
|
|
27
27
|
from .constants import ConstantsObfuscator
|
|
28
|
+
from .controlflow.control_flow_flatten import ControlFlowFlattenObfuscator
|
|
28
29
|
from .definitions import DefinitionsObfuscator
|
|
29
30
|
from .encoding.a85 import ASCII85Obfuscator
|
|
30
31
|
from .encoding.b16 import Base16Obfuscator
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"CharFromDocObfuscator",
|
|
77
78
|
"CommentsObfuscator",
|
|
78
79
|
"ConstantsObfuscator",
|
|
80
|
+
"ControlFlowFlattenObfuscator",
|
|
79
81
|
"DeepEncryptionObfuscator",
|
|
80
82
|
"DefinitionsObfuscator",
|
|
81
83
|
"DocstringObfuscator",
|
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
# FIXME (deoktr): work in progress !
|
|
18
17
|
from base64 import b64encode
|
|
19
|
-
from tokenize import
|
|
18
|
+
from tokenize import (
|
|
19
|
+
DEDENT,
|
|
20
|
+
INDENT,
|
|
21
|
+
LPAR,
|
|
22
|
+
NAME,
|
|
23
|
+
NEWLINE,
|
|
24
|
+
NL,
|
|
25
|
+
OP,
|
|
26
|
+
RPAR,
|
|
27
|
+
STRING,
|
|
28
|
+
untokenize,
|
|
29
|
+
)
|
|
20
30
|
|
|
21
31
|
from pof.logger import logger
|
|
22
32
|
|
|
@@ -25,14 +35,56 @@ class DeepEncryptionObfuscator:
|
|
|
25
35
|
def __init__(self, encryption_depth=0) -> None:
|
|
26
36
|
self.encryption_depth = encryption_depth
|
|
27
37
|
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _nested_depth_at(tokens):
|
|
40
|
+
nested = 0
|
|
41
|
+
awaiting = False
|
|
42
|
+
for i, (toknum, tokval) in enumerate(tokens):
|
|
43
|
+
if toknum == NAME and tokval in ("def", "class") and nested == 0:
|
|
44
|
+
awaiting = True
|
|
45
|
+
if awaiting and toknum == INDENT:
|
|
46
|
+
awaiting = False
|
|
47
|
+
nested += 1
|
|
48
|
+
elif nested > 0 and toknum == INDENT:
|
|
49
|
+
nested += 1
|
|
50
|
+
elif nested > 0 and toknum == DEDENT:
|
|
51
|
+
nested -= 1
|
|
52
|
+
yield i, nested
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _is_empty_return(tokens, pos):
|
|
56
|
+
"""Check if the return at pos has no value (bare return)."""
|
|
57
|
+
for j in range(pos + 1, len(tokens)):
|
|
58
|
+
nt = tokens[j][0]
|
|
59
|
+
if nt in (NEWLINE, NL):
|
|
60
|
+
return True
|
|
61
|
+
if nt != DEDENT:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _replace_returns(cls, tokens):
|
|
67
|
+
"""Replace return with r= at the function body level.
|
|
68
|
+
|
|
69
|
+
Returns inside nested def/class are left intact.
|
|
70
|
+
"""
|
|
71
|
+
depths = dict(cls._nested_depth_at(tokens))
|
|
72
|
+
result = []
|
|
73
|
+
for i, (toknum, tokval) in enumerate(tokens):
|
|
74
|
+
if toknum == NAME and tokval == "return" and depths.get(i, 0) == 0:
|
|
75
|
+
result.extend([(NAME, "r"), (OP, "=")])
|
|
76
|
+
if cls._is_empty_return(tokens, i):
|
|
77
|
+
result.append((NAME, "None"))
|
|
78
|
+
else:
|
|
79
|
+
result.append((toknum, tokval))
|
|
80
|
+
return result
|
|
81
|
+
|
|
28
82
|
def obfuscate_tokens(self, tokens): # noqa: C901 PLR0912
|
|
29
83
|
"""Encrypt every function's source code.
|
|
30
84
|
|
|
31
|
-
Encrypt every function's source code
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
memory, of course the draw back is the speed will be reduced.
|
|
35
|
-
Also verify integrity dynamically, maybe also sign encrypted code.
|
|
85
|
+
Encrypt every function's source code and decrypt only when needed
|
|
86
|
+
(just-in-time) via exec(). This prevents the entire source code being
|
|
87
|
+
accessible at once in memory.
|
|
36
88
|
|
|
37
89
|
Convert functions into the following:
|
|
38
90
|
|
|
@@ -47,12 +99,8 @@ class DeepEncryptionObfuscator:
|
|
|
47
99
|
del r_dict
|
|
48
100
|
return r_val
|
|
49
101
|
```
|
|
50
|
-
|
|
51
|
-
Todo:
|
|
52
|
-
- create a function 'exec_return' and call it with en encrypted source
|
|
53
102
|
"""
|
|
54
103
|
result = [] # obfuscated tokens
|
|
55
|
-
# just for testing
|
|
56
104
|
result.extend(
|
|
57
105
|
[
|
|
58
106
|
(NAME, "from"),
|
|
@@ -91,11 +139,9 @@ class DeepEncryptionObfuscator:
|
|
|
91
139
|
inside_function and depth <= self.encryption_depth and toknum == DEDENT
|
|
92
140
|
):
|
|
93
141
|
inside_function = False
|
|
94
|
-
# [2:-1] is to remove indent/dedent
|
|
95
142
|
|
|
96
143
|
fixed_function_tokens = []
|
|
97
|
-
|
|
98
|
-
fixed_depth = -1 # should it be - (self.encryption_depth) ??
|
|
144
|
+
fixed_depth = -1
|
|
99
145
|
for ftnum, ftval in function_tokens:
|
|
100
146
|
ftval_d = ftval
|
|
101
147
|
if ftnum == INDENT:
|
|
@@ -105,59 +151,43 @@ class DeepEncryptionObfuscator:
|
|
|
105
151
|
fixed_depth -= 1
|
|
106
152
|
fixed_function_tokens.append((ftnum, ftval_d))
|
|
107
153
|
|
|
108
|
-
#
|
|
154
|
+
# [2:-1] removes the outer indent/dedent wrapper
|
|
109
155
|
source = untokenize(fixed_function_tokens[2:-1])
|
|
110
156
|
|
|
111
|
-
# obviously doesn't work with yield
|
|
112
157
|
if not any(i in source for i in ["yield", "super"]):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
source =
|
|
116
|
-
source = source.replace("return", "r=")
|
|
158
|
+
body_tokens = fixed_function_tokens[2:-1]
|
|
159
|
+
replaced_tokens = self._replace_returns(body_tokens)
|
|
160
|
+
source = untokenize(replaced_tokens)
|
|
117
161
|
|
|
118
162
|
encoded = b64encode(source.encode())
|
|
119
163
|
globals_dict_name = "r_dict"
|
|
120
164
|
new_tokens = [
|
|
121
165
|
(NEWLINE, "\n"),
|
|
122
|
-
(
|
|
123
|
-
|
|
124
|
-
" " * (self.encryption_depth + 1),
|
|
125
|
-
), # TODO (deoktr): change me
|
|
166
|
+
(INDENT, " " * (self.encryption_depth + 1)),
|
|
167
|
+
# r_dict = globals().copy()
|
|
126
168
|
(NAME, globals_dict_name),
|
|
127
169
|
(OP, "="),
|
|
128
170
|
(NAME, "globals"),
|
|
129
171
|
(LPAR, "("),
|
|
130
|
-
(
|
|
172
|
+
(RPAR, ")"),
|
|
131
173
|
(OP, "."),
|
|
132
|
-
(
|
|
174
|
+
(NAME, "copy"),
|
|
133
175
|
(LPAR, "("),
|
|
134
|
-
(
|
|
176
|
+
(RPAR, ")"),
|
|
135
177
|
(NEWLINE, "\n"),
|
|
178
|
+
# r_dict.update(locals())
|
|
136
179
|
(NAME, globals_dict_name),
|
|
137
180
|
(OP, "."),
|
|
138
181
|
(NAME, "update"),
|
|
139
182
|
(LPAR, "("),
|
|
140
183
|
(NAME, "locals"),
|
|
141
184
|
(LPAR, "("),
|
|
142
|
-
(LPAR, ")"),
|
|
143
|
-
(LPAR, ")"),
|
|
144
|
-
(NEWLINE, "\n"),
|
|
145
|
-
# print the code before executing it, for testing
|
|
146
|
-
(NAME, "print"),
|
|
147
|
-
(LPAR, "("),
|
|
148
|
-
(NAME, "b64decode"),
|
|
149
|
-
(LPAR, "("),
|
|
150
|
-
(STRING, repr(encoded)),
|
|
151
|
-
(RPAR, ")"),
|
|
152
|
-
(OP, "."),
|
|
153
|
-
(NAME, "decode"),
|
|
154
|
-
(LPAR, "("),
|
|
155
185
|
(RPAR, ")"),
|
|
156
186
|
(RPAR, ")"),
|
|
157
187
|
(NEWLINE, "\n"),
|
|
188
|
+
# exec(b64decode(b'...'), r_dict)
|
|
158
189
|
(NAME, "exec"),
|
|
159
190
|
(LPAR, "("),
|
|
160
|
-
# just for testing
|
|
161
191
|
(NAME, "b64decode"),
|
|
162
192
|
(LPAR, "("),
|
|
163
193
|
(STRING, repr(encoded)),
|
|
@@ -166,6 +196,7 @@ class DeepEncryptionObfuscator:
|
|
|
166
196
|
(NAME, globals_dict_name),
|
|
167
197
|
(RPAR, ")"),
|
|
168
198
|
(NEWLINE, "\n"),
|
|
199
|
+
# if 'r' not in r_dict:
|
|
169
200
|
(NAME, "if"),
|
|
170
201
|
(STRING, "'r'"),
|
|
171
202
|
(NAME, "not"),
|
|
@@ -173,14 +204,13 @@ class DeepEncryptionObfuscator:
|
|
|
173
204
|
(NAME, globals_dict_name),
|
|
174
205
|
(OP, ":"),
|
|
175
206
|
(NEWLINE, "\n"),
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
" " * (self.encryption_depth + 2),
|
|
179
|
-
), # TODO (deoktr): change me
|
|
207
|
+
# return None
|
|
208
|
+
(INDENT, " " * (self.encryption_depth + 2)),
|
|
180
209
|
(NAME, "return"),
|
|
181
210
|
(NAME, "None"),
|
|
182
211
|
(DEDENT, ""),
|
|
183
212
|
(NEWLINE, "\n"),
|
|
213
|
+
# r_val = r_dict['r']
|
|
184
214
|
(NAME, "r_val"),
|
|
185
215
|
(OP, "="),
|
|
186
216
|
(NAME, globals_dict_name),
|
|
@@ -188,9 +218,11 @@ class DeepEncryptionObfuscator:
|
|
|
188
218
|
(STRING, "'r'"),
|
|
189
219
|
(OP, "]"),
|
|
190
220
|
(NEWLINE, "\n"),
|
|
221
|
+
# del r_dict
|
|
191
222
|
(NAME, "del"),
|
|
192
223
|
(NAME, globals_dict_name),
|
|
193
224
|
(NEWLINE, "\n"),
|
|
225
|
+
# return r_val
|
|
194
226
|
(NAME, "return"),
|
|
195
227
|
(NAME, "r_val"),
|
|
196
228
|
(NEWLINE, "\n"),
|
pof/obfuscator/cipher/shift.py
CHANGED
|
@@ -23,13 +23,12 @@ from pof.utils.tokens import untokenize
|
|
|
23
23
|
class ShiftObfuscator(ShiftCipher):
|
|
24
24
|
"""Shift cipher obfuscator."""
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
def obfuscate_tokens(cls, tokens):
|
|
26
|
+
def obfuscate_tokens(self, tokens):
|
|
28
27
|
code = untokenize(tokens)
|
|
29
28
|
return [
|
|
30
29
|
(NAME, "exec"),
|
|
31
30
|
(LPAR, "("),
|
|
32
|
-
*
|
|
31
|
+
*self.decode_tokens(self.encode_tokens(code)),
|
|
33
32
|
(RPAR, ")"),
|
|
34
33
|
(NEWLINE, "\n"),
|
|
35
34
|
]
|
pof/obfuscator/constants.py
CHANGED
|
@@ -41,6 +41,7 @@ import random
|
|
|
41
41
|
from tokenize import DEDENT, ENCODING, INDENT, NAME, NEWLINE, NUMBER, OP, STRING
|
|
42
42
|
|
|
43
43
|
from pof.utils.generator import BasicGenerator
|
|
44
|
+
from pof.utils.tokens import merge_implicit_strings
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class ConstantsObfuscator:
|
|
@@ -233,6 +234,7 @@ class ConstantsObfuscator:
|
|
|
233
234
|
return [(NAME, variables[tokval][0])], variables
|
|
234
235
|
|
|
235
236
|
def obfuscate_tokens(self, tokens):
|
|
237
|
+
tokens = merge_implicit_strings(tokens)
|
|
236
238
|
variables = {}
|
|
237
239
|
result = []
|
|
238
240
|
parenthesis_depth = 0 # parenthesis depth
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# POF, a free and open source Python obfuscation framework.
|
|
2
|
+
# Copyright (C) 2022 - 2026 Deoktr
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
from .control_flow_flatten import ControlFlowFlattenObfuscator
|
|
18
|
+
|
|
19
|
+
__all__ = ["ControlFlowFlattenObfuscator"]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# POF, a free and open source Python obfuscation framework.
|
|
2
|
+
# Copyright (C) 2022 - 2026 Deoktr
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import io
|
|
19
|
+
import random
|
|
20
|
+
from tokenize import ENCODING, ENDMARKER, generate_tokens
|
|
21
|
+
|
|
22
|
+
from pof.utils.tokens import untokenize
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ControlFlowFlattenObfuscator:
|
|
26
|
+
"""Transform sequential function code into a state-machine dispatcher."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, min_statements: int = 3) -> None:
|
|
29
|
+
self.min_statements = min_statements
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _should_skip_function(body: list[ast.stmt]) -> bool:
|
|
33
|
+
"""Return True if the function body contains unsupported constructs."""
|
|
34
|
+
for node in ast.walk(ast.Module(body=body, type_ignores=[])):
|
|
35
|
+
if isinstance(
|
|
36
|
+
node,
|
|
37
|
+
(
|
|
38
|
+
ast.Yield,
|
|
39
|
+
ast.YieldFrom,
|
|
40
|
+
ast.AsyncFor,
|
|
41
|
+
ast.AsyncWith,
|
|
42
|
+
ast.Await,
|
|
43
|
+
),
|
|
44
|
+
):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _has_return(stmts: list[ast.stmt]) -> bool:
|
|
50
|
+
"""Check if any statement in the list is or contains a return."""
|
|
51
|
+
for node in ast.walk(ast.Module(body=stmts, type_ignores=[])):
|
|
52
|
+
if isinstance(node, ast.Return):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _flatten_body(cls, body: list[ast.stmt]) -> list[ast.stmt]:
|
|
58
|
+
"""Transform a list of sequential statements into a state-machine dispatcher."""
|
|
59
|
+
num_blocks = len(body)
|
|
60
|
+
all_states = random.sample(range(100, 999), num_blocks + 1)
|
|
61
|
+
exit_state = all_states[-1]
|
|
62
|
+
block_states = all_states[:num_blocks]
|
|
63
|
+
|
|
64
|
+
state_var = "_state"
|
|
65
|
+
ret_var = "_ret"
|
|
66
|
+
has_ret = cls._has_return(body)
|
|
67
|
+
|
|
68
|
+
dispatcher_cases: list[ast.If | None] = []
|
|
69
|
+
|
|
70
|
+
for idx, (state_num, stmt) in enumerate(zip(block_states, body)):
|
|
71
|
+
next_state = block_states[idx + 1] if idx + 1 < num_blocks else exit_state
|
|
72
|
+
|
|
73
|
+
case_body: list[ast.stmt] = []
|
|
74
|
+
|
|
75
|
+
if isinstance(stmt, ast.Return):
|
|
76
|
+
# return value -> _ret = value; _state = exit
|
|
77
|
+
if stmt.value is not None:
|
|
78
|
+
case_body.append(
|
|
79
|
+
ast.Assign(
|
|
80
|
+
targets=[ast.Name(id=ret_var, ctx=ast.Store())],
|
|
81
|
+
value=stmt.value,
|
|
82
|
+
lineno=0,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
case_body.append(
|
|
86
|
+
ast.Assign(
|
|
87
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
88
|
+
value=ast.Constant(value=exit_state),
|
|
89
|
+
lineno=0,
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
elif isinstance(stmt, ast.If):
|
|
93
|
+
# if/else -> execute block, then set state based on which branch
|
|
94
|
+
# for simplicity, keep the if/else inside the state block and
|
|
95
|
+
# set next state after
|
|
96
|
+
case_body.append(stmt)
|
|
97
|
+
case_body.append(
|
|
98
|
+
ast.Assign(
|
|
99
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
100
|
+
value=ast.Constant(value=next_state),
|
|
101
|
+
lineno=0,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
case_body.append(stmt)
|
|
106
|
+
case_body.append(
|
|
107
|
+
ast.Assign(
|
|
108
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
109
|
+
value=ast.Constant(value=next_state),
|
|
110
|
+
lineno=0,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
test = ast.Compare(
|
|
115
|
+
left=ast.Name(id=state_var, ctx=ast.Load()),
|
|
116
|
+
ops=[ast.Eq()],
|
|
117
|
+
comparators=[ast.Constant(value=state_num)],
|
|
118
|
+
)
|
|
119
|
+
dispatcher_cases.append((test, case_body))
|
|
120
|
+
|
|
121
|
+
if not dispatcher_cases:
|
|
122
|
+
return body
|
|
123
|
+
|
|
124
|
+
random.shuffle(dispatcher_cases)
|
|
125
|
+
|
|
126
|
+
current: ast.stmt | None = None
|
|
127
|
+
for test, case_body in reversed(dispatcher_cases):
|
|
128
|
+
if current is None:
|
|
129
|
+
current = ast.If(test=test, body=case_body, orelse=[])
|
|
130
|
+
else:
|
|
131
|
+
current = ast.If(test=test, body=case_body, orelse=[current])
|
|
132
|
+
|
|
133
|
+
init_state = ast.Assign(
|
|
134
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
135
|
+
value=ast.Constant(value=block_states[0]),
|
|
136
|
+
lineno=0,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
init_ret: list[ast.stmt] = []
|
|
140
|
+
if has_ret:
|
|
141
|
+
init_ret.append(
|
|
142
|
+
ast.Assign(
|
|
143
|
+
targets=[ast.Name(id=ret_var, ctx=ast.Store())],
|
|
144
|
+
value=ast.Constant(value=None),
|
|
145
|
+
lineno=0,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
while_loop = ast.While(
|
|
150
|
+
test=ast.Compare(
|
|
151
|
+
left=ast.Name(id=state_var, ctx=ast.Load()),
|
|
152
|
+
ops=[ast.NotEq()],
|
|
153
|
+
comparators=[ast.Constant(value=exit_state)],
|
|
154
|
+
),
|
|
155
|
+
body=[current],
|
|
156
|
+
orelse=[],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result: list[ast.stmt] = [init_state, *init_ret, while_loop]
|
|
160
|
+
|
|
161
|
+
if has_ret:
|
|
162
|
+
result.append(ast.Return(value=ast.Name(id=ret_var, ctx=ast.Load())))
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def obfuscate_tokens(self, tokens: list) -> list:
|
|
167
|
+
source = untokenize(tokens)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
tree = ast.parse(source)
|
|
171
|
+
except SyntaxError:
|
|
172
|
+
return tokens
|
|
173
|
+
|
|
174
|
+
modified = False
|
|
175
|
+
|
|
176
|
+
for node in ast.walk(tree):
|
|
177
|
+
if not isinstance(node, ast.FunctionDef):
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
body = node.body
|
|
181
|
+
if len(body) < self.min_statements:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if self._should_skip_function(body):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
node.body = self._flatten_body(body)
|
|
188
|
+
modified = True
|
|
189
|
+
|
|
190
|
+
if not modified:
|
|
191
|
+
return tokens
|
|
192
|
+
|
|
193
|
+
ast.fix_missing_locations(tree)
|
|
194
|
+
new_source = ast.unparse(tree)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
new_tokens = list(generate_tokens(io.StringIO(new_source + "\n").readline))
|
|
198
|
+
except Exception: # noqa: BLE001
|
|
199
|
+
return tokens
|
|
200
|
+
|
|
201
|
+
# strip ENCODING and ENDMARKER
|
|
202
|
+
result: list[tuple[int, str]] = []
|
|
203
|
+
for toknum, tokval, *_ in new_tokens:
|
|
204
|
+
if toknum in (ENCODING, ENDMARKER):
|
|
205
|
+
continue
|
|
206
|
+
result.append((toknum, tokval))
|
|
207
|
+
|
|
208
|
+
return result
|
pof/obfuscator/esoteric/doc.py
CHANGED
|
@@ -20,6 +20,7 @@ from tokenize import NAME, NUMBER, OP, STRING
|
|
|
20
20
|
|
|
21
21
|
from pof.errors import PofError
|
|
22
22
|
from pof.logger import logger
|
|
23
|
+
from pof.utils.tokens import merge_implicit_strings
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class CharFromDocObfuscator:
|
|
@@ -228,6 +229,7 @@ class CharFromDocObfuscator:
|
|
|
228
229
|
def obfuscate_tokens(self, tokens):
|
|
229
230
|
# print.__doc__[0] = 'P'
|
|
230
231
|
# __builtins__.__doc__[0] = 'B'
|
|
232
|
+
tokens = merge_implicit_strings(tokens)
|
|
231
233
|
result = []
|
|
232
234
|
|
|
233
235
|
for _index, (toknum, tokval, *_) in enumerate(tokens):
|
|
@@ -14,55 +14,11 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
# TODO (deoktr): WORK IN PROGRESS !
|
|
18
|
-
#
|
|
19
|
-
# Look at `Ruff` for "variable extraction"
|
|
20
|
-
#
|
|
21
|
-
# IDEA: maybe put every declaration at the start of the function, so that it has way
|
|
22
|
-
# less chance to break the actual function
|
|
23
|
-
#
|
|
24
|
-
# example output:
|
|
25
|
-
#
|
|
26
|
-
# ```
|
|
27
|
-
# import os
|
|
28
|
-
# BASE = "/home/test/"
|
|
29
|
-
# path = os.path.join(BASE, "file.txt")
|
|
30
|
-
# print(path)
|
|
31
|
-
# ```
|
|
32
|
-
#
|
|
33
|
-
# ```
|
|
34
|
-
# import os
|
|
35
|
-
# u = "/home/test/"
|
|
36
|
-
# BASE = u
|
|
37
|
-
# a = "file.txt"
|
|
38
|
-
# path = os.path.join(BASE, a)
|
|
39
|
-
# x = path
|
|
40
|
-
# print(x)
|
|
41
|
-
# ```
|
|
42
|
-
#
|
|
43
|
-
# FIXME (deoktr): parenthesis variables:
|
|
44
|
-
# ```
|
|
45
|
-
# if (
|
|
46
|
-
# x < 1 and y > 2
|
|
47
|
-
# )
|
|
48
|
-
# ```
|
|
49
|
-
# this would break because the variables would be added INSIDE the parenthesis
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# FIXME (deoktr): decorators:
|
|
53
|
-
# ```
|
|
54
|
-
# class Foo:
|
|
55
|
-
# @classmethod
|
|
56
|
-
# def bar(a=1, b=2):
|
|
57
|
-
# pass
|
|
58
|
-
# ```
|
|
59
|
-
# after classmethod and before def variables a and b will be obfuscated,
|
|
60
|
-
# breaking the code
|
|
61
|
-
#
|
|
62
17
|
import keyword
|
|
63
18
|
from tokenize import DEDENT, ENCODING, INDENT, NAME, NEWLINE, NL, NUMBER, OP, STRING
|
|
64
19
|
|
|
65
20
|
from pof.utils.generator import BasicGenerator
|
|
21
|
+
from pof.utils.tokens import merge_implicit_strings
|
|
66
22
|
|
|
67
23
|
|
|
68
24
|
class ExtractVariablesObfuscator:
|
|
@@ -241,6 +197,8 @@ class ExtractVariablesObfuscator:
|
|
|
241
197
|
RESERVED = RESERVED_WORDS + BUILTINS + tuple(keyword.kwlist)
|
|
242
198
|
KEYWORDS = tuple(keyword.kwlist)
|
|
243
199
|
|
|
200
|
+
CONTINUATION_KEYWORDS = ("elif", "else", "except", "finally")
|
|
201
|
+
|
|
244
202
|
def __init__(self, generator=None) -> None:
|
|
245
203
|
if generator is None:
|
|
246
204
|
generator = BasicGenerator.alphabet_generator()
|
|
@@ -249,12 +207,15 @@ class ExtractVariablesObfuscator:
|
|
|
249
207
|
def generate_new_name(self):
|
|
250
208
|
return next(self.generator)
|
|
251
209
|
|
|
252
|
-
def obfuscate_tokens(self, tokens):
|
|
210
|
+
def obfuscate_tokens(self, tokens): # noqa: C901
|
|
211
|
+
tokens = merge_implicit_strings(tokens)
|
|
253
212
|
result = []
|
|
254
213
|
new_line_buffer = []
|
|
255
214
|
line_buffer = []
|
|
256
|
-
parenthesis_depth = 0
|
|
215
|
+
parenthesis_depth = 0
|
|
257
216
|
prev_toknum = None
|
|
217
|
+
in_decorator = False
|
|
218
|
+
|
|
258
219
|
for toknum, tokval, *_ in tokens:
|
|
259
220
|
new_tokens = [(toknum, tokval)]
|
|
260
221
|
|
|
@@ -263,17 +224,28 @@ class ExtractVariablesObfuscator:
|
|
|
263
224
|
elif toknum == OP and tokval == ")":
|
|
264
225
|
parenthesis_depth -= 1
|
|
265
226
|
|
|
227
|
+
# track decorator context, suppress flushing between @ and def/class
|
|
228
|
+
if toknum == OP and tokval == "@":
|
|
229
|
+
in_decorator = True
|
|
230
|
+
elif in_decorator and toknum == NAME and tokval in ("def", "class"):
|
|
231
|
+
in_decorator = False
|
|
232
|
+
|
|
266
233
|
is_docstring = toknum == STRING and (
|
|
267
|
-
prev_toknum
|
|
268
|
-
in [
|
|
269
|
-
NEWLINE,
|
|
270
|
-
DEDENT,
|
|
271
|
-
INDENT,
|
|
272
|
-
ENCODING,
|
|
273
|
-
]
|
|
234
|
+
prev_toknum in [NEWLINE, DEDENT, INDENT, ENCODING]
|
|
274
235
|
)
|
|
275
236
|
|
|
276
|
-
if
|
|
237
|
+
# check if current line starts with a continuation keyword if so,
|
|
238
|
+
# skip extraction to avoid scope issues
|
|
239
|
+
first_name_in_line = None
|
|
240
|
+
for tok in line_buffer:
|
|
241
|
+
if tok[0] == NAME:
|
|
242
|
+
first_name_in_line = tok[1]
|
|
243
|
+
break
|
|
244
|
+
on_continuation_line = first_name_in_line in self.CONTINUATION_KEYWORDS
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
(toknum == STRING and not is_docstring) or toknum == NUMBER
|
|
248
|
+
) and not on_continuation_line:
|
|
277
249
|
random_name = self.generate_new_name()
|
|
278
250
|
new_line_buffer.extend(
|
|
279
251
|
[
|
|
@@ -285,19 +257,11 @@ class ExtractVariablesObfuscator:
|
|
|
285
257
|
)
|
|
286
258
|
new_tokens = [(NAME, random_name)]
|
|
287
259
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
newline_count = [t[1] for t in line_buffer].count("\n")
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
((toknum in (NEWLINE, NL)) and tokval == "\n") and not has_decorator
|
|
294
|
-
) or (newline_count > 1):
|
|
295
|
-
if has_decorator:
|
|
296
|
-
line_buffer = [(NEWLINE, "\n"), *line_buffer]
|
|
297
|
-
new_tokens = new_line_buffer + line_buffer + new_tokens
|
|
298
|
-
else:
|
|
299
|
-
new_tokens = new_line_buffer + new_tokens + line_buffer
|
|
260
|
+
is_newline = toknum in (NEWLINE, NL) and tokval == "\n"
|
|
261
|
+
can_flush = is_newline and parenthesis_depth == 0 and not in_decorator
|
|
300
262
|
|
|
263
|
+
if can_flush:
|
|
264
|
+
new_tokens = new_line_buffer + new_tokens + line_buffer
|
|
301
265
|
new_line_buffer = []
|
|
302
266
|
line_buffer = []
|
|
303
267
|
elif toknum in (INDENT, DEDENT):
|
pof/obfuscator/remove/newline.py
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
from tokenize import
|
|
17
|
+
from tokenize import COMMENT, NEWLINE, NL
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class NewlineObfuscator:
|
|
@@ -31,9 +31,8 @@ class NewlineObfuscator:
|
|
|
31
31
|
# remove empty lines created after token manipulations
|
|
32
32
|
# \n after \n --> 2 new lines in a row = one is useless
|
|
33
33
|
# \n after NL --> same ^
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
toknum == NEWLINE and (prev_toknum in (NEWLINE, NL, INDENT))
|
|
34
|
+
if (toknum == NL and prev_toknum != COMMENT) or (
|
|
35
|
+
toknum == NEWLINE and (prev_toknum in (NEWLINE, NL))
|
|
37
36
|
):
|
|
38
37
|
new_tokens = None
|
|
39
38
|
|