Proteggere codice python: impresa impossibile?
Marco Federighi, Dev@Nephila
Problema
Voglio proteggere il codice sorgente del mio programma
Perchè?
- proteggere soluzioni che rendono concorrenziale il software
- evitare attacchi informatici di vario genere
- richiesta del cliente
- ...
Problema
Le soluzioni dipendono dall'implementazione del linguaggio di programmazione utilizzato
Premessa teorica
- linguaggi compilati: il codice del programma viene convertito, in linguaggio macchina (es. C, C++)
- linguaggio interpretati: il codice del programma viene direttamente eseguito da un interprete (es. Perl)
Nota: ogni linguaggio di programmazione può essere implementato in entrambi i modi.
Premessa teorica
Esistono anche soluzioni miste
Ovvero?
il codice sorgente viene compilato in una forma intermedia, detta bytecode, che poi sarà interpretata o compilata a tempo di esecuzione
Python
python è compilato o interpretato? entrambe le cose
Il codice sorgente può essere eseguito direttamente, tuttavia si può anche generare del codice intermedio
(bytecode)
Problema
proteggere il codice di un programma python è difficile per la natura stessa del linguaggio.
Perchè?
Il nostro codice segretissimo (uh)!
ID_SOMETHING = 'some_id_code'
def print_secret(x, y):
"""
So secret function
:param x: important input
:param y: important input
:return: important output
"""
return (ID_SOMETHING * x) + y
main.py
Idea #1
Distribuire i file .pyc
- vantaggio: semplici da generare e distribuire
- problema: si possono analizzare e decompilare facilmente!
$ python -m compileall main.py
$ ls
main.py main.pyc
Come si fa?
Idea #1
Struttura file pyc
- magic number (4 byte)
- timestamp (4 byte)
- marshalled code
Idea #1
Analizzare un file .pyc
- Leggere un dump esadecimale, ad esempio tramite xxd
$ xxd main.pyc
0000000: 03f3 0d0a 0e5b 2e5b 6300 0000 0000 0000 .....[.[c.......
0000010: 0001 0000 0040 0000 0073 1300 0000 6400 .....@...s....d.
0000020: 005a 0000 6401 0084 0000 5a01 0064 0200 .Z..d.....Z..d..
0000030: 5328 0300 0000 740c 0000 0073 6f6d 655f S(....t....some_
0000040: 6964 5f63 6f64 6563 0200 0000 0200 0000 id_codec........
0000050: 0200 0000 4300 0000 730c 0000 0074 0000 ....C...s....t..
0000060: 7c00 0014 7c01 0017 5328 0100 0000 7376 |...|...S(....sv
0000070: 0000 000a 2020 2020 536f 2073 6563 7265 .... So secre
0000080: 7420 6675 6e63 7469 6f6e 0a20 2020 203a t function. :
0000090: 7061 7261 6d20 783a 2069 6d70 6f72 7461 param x: importa
00000a0: 6e74 2069 6e70 7574 0a20 2020 203a 7061 nt input. :pa
00000b0: 7261 6d20 793a 2069 6d70 6f72 7461 6e74 ram y: important
00000c0: 2069 6e70 7574 0a20 2020 203a 7265 7475 input. :retu
00000d0: 726e 3a20 696d 706f 7274 616e 7420 6f75 rn: important ou
00000e0: 7470 7574 0a20 2020 2028 0100 0000 740c tput. (....t.
00000f0: 0000 0049 445f 534f 4d45 5448 494e 4728 ...ID_SOMETHING(
0000100: 0200 0000 7401 0000 0078 7401 0000 0079 ....t....xt....y
0000110: 2800 0000 0028 0000 0000 7307 0000 006d (....(....s....m
0000120: 6169 6e2e 7079 740c 0000 0070 7269 6e74 ain.pyt....print
0000130: 5f73 6563 7265 7403 0000 0073 0200 0000 _secret....s....
0000140: 0007 4e28 0200 0000 5201 0000 0052 0400 ..N(....R....R..
0000150: 0000 2800 0000 0028 0000 0000 2800 0000 ..(....(....(...
0000160: 0073 0700 0000 6d61 696e 2e70 7974 0800 .s....main.pyt..
0000170: 0000 3c6d 6f64 756c 653e 0100 0000 7302 ..<module>....s.
0000180: 0000 0006 02 .....
Idea #1
Analizzare un file .pyc
- Ispezioniamolo tramite...python!
import dis, marshal, struct
with open('main.pyc', 'rb') as f:
magic = f.read(4)
timestamp = f.read(4)
code = f.read()
code = marshal.loads(code)
magic = struct.unpack('<H', magic[:2])
timestamp = struct.unpack('<I', timestamp)
print(magic, timestamp)
print(code)
print(dis.disassemble(code))
((62211,), (1529764622,))
<code object <module> at 0x7f4c0c798eb0, file "main.py", line 1>
1 0 LOAD_CONST 0 ('some_id_code')
3 STORE_NAME 0 (ID_SOMETHING)
3 6 LOAD_CONST 1 (<code object print_secret at 0x7f4c0c798cb0, file "main.py", line 3>)
9 MAKE_FUNCTION 0
12 STORE_NAME 1 (print_secret)
15 LOAD_CONST 2 (None)
18 RETURN_VALUE
None
Idea #1
Ricostruiamo il sorgente python!
- scrivendo un algoritmo per decompilare i file .pyc
- utilizzando i tool a disposizione, ad es. uncompyle6 (https://github.com/rocky/python-uncompyle6)
$ uncompyle6 main.pyc
# uncompyle6 version 3.2.3
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.9 (default, Jun 29 2016, 13:08:31)
# [GCC 4.9.2]
# Embedded file name: main.py
# Compiled at: 2018-06-23 17:07:15
ID_SOMETHING = 'some_id_code'
def print_secret(x, y):
"""
So secret function
:param x: important input
:param y: important input
:return: important output
"""
return ID_SOMETHING * x + y
# okay decompiling main.pyc
Idea #1
Bonus stage: file .pyo
Sono generati tramite flag di ottimizzazione
$ python -OO -m compileall main.py
Si ottiene un risultato più compatto (no assert, docstring, etc.)
Idea #2
Offuscare il codice sorgente
Come?
- utilizziamo del codice custom
- utilizziamo un tool, ad es. pyminifier (https://github.com/liftoff/pyminifier)
$ pyminifier --nonlatin --replacement-length=100 main.py
𦃧𞣁䲜ﰂ𩳹𐫅𡔣ﰂ𐱃꺞퀷𞠬𧢍𐡪嚐𖠯ﰐ𐦖될𝞴𒅑𞸖𩅺𐠝ꓷ𞸁𨺢𖫔𞢲𐳍𥵻𬧄𐳚眔ﯕ𩀽𐠍𐳅𢠨𪮙𦤁𦒘𞢇𞡗ﴉ𐬈𤰶𓃰𩀃𐣩㛒𧫎偶𐲁ﮭ𐱃𞣄𐬗𞺎𐤡豪퀬𫳼𞢃𠽱𣗀𐬤𐲙ࡄ脴𐳃𫘩𐪌כֿ𐤥掊סּ𞸧𣏔𐬳نﵓ𞸛餧𐳪𞢼𐦍𨮪𐢕㜻𢚔膲𐦬諹𣜾𣰓𨘳𒃇𐳨ﲥ='some_id_code'
def 𦃧𞣁䲜ﰂ𩳹𐫅𡔣ﰂ𐱃꺞퀷𞠬𧢍𐡪嚐𖠯ﰐ𐦖될𝞴𒅑𞸖𩅺𐠝ꓷ𞸁𨺢𖫔𞢲𐳍𥵻𬧄𐳚眔ﯕ𩀽𐠍𐳅𢠨𪮙𦤁𦒘𞢇𞡗ﴉ𐬈𤰶𓃰𩀃𐣩㛒𧫎偶𐲁ﮭ𐱃𞣄𐬗𞺎𐤡豪퀬𫳼𞢃𠽱𣗀𐬤𐲙ࡄ脴𐳃𫘩𐪌כֿ𐤥掊סּ𞸧𣏔𐬳نﵓ𞸛餧𐳪𞢼𐦍𨮪𐢕㜻𢚔膲𐦬諹𣜾𣰓𨘳𒃇𐳨𐠳(x,y):
return(𦃧𞣁䲜ﰂ𩳹𐫅𡔣ﰂ𐱃꺞퀷𞠬𧢍𐡪嚐𖠯ﰐ𐦖될𝞴𒅑𞸖𩅺𐠝ꓷ𞸁𨺢𖫔𞢲𐳍𥵻𬧄𐳚眔ﯕ𩀽𐠍𐳅𢠨𪮙𦤁𦒘𞢇𞡗ﴉ𐬈𤰶𓃰𩀃𐣩㛒𧫎偶𐲁ﮭ𐱃𞣄𐬗𞺎𐤡豪퀬𫳼𞢃𠽱𣗀𐬤𐲙ࡄ脴𐳃𫘩𐪌כֿ𐤥掊סּ𞸧𣏔𐬳نﵓ𞸛餧𐳪𞢼𐦍𨮪𐢕㜻𢚔膲𐦬諹𣜾𣰓𨘳𒃇𐳨ﲥ*x)+y
# Created by pyminifier (https://github.com/liftoff/pyminifier)
Idea #2
Offuscare il codice sorgente
Problemi
- il bytecode rimane analizzabile
- si possono utilizzare tecniche per semplificare il codice offuscato e ridurne la complessità
$pyminifier -O obfuscated.py
V='some_id_code'
def p(x,y):
return(V*x)+y
# Created by pyminifier (https://github.com/liftoff/pyminifier)
Idea #3
pacchettizzare il programma
py2exe, cx_Freeze, ...
ProblemA:
Reverse engineering abbastanza semplice (miriadi di tool e tecniche per ricavare i sorgenti :/ )
Idea #4
CYTHON (http://cython.org/)
Trasformiamo il codice Python in codice C
$ cython main.py -o main.c
$ cat main.c
/* Generated by Cython 0.28.3 */
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#ifndef Py_PYTHON_H
#error Python headers needed to compile C extensions, please install development version of Python.
#elif PY_VERSION_HEX < 0x02060000 || (0x03000000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x03030000)
#error Cython requires Python 2.6+ or Python 3.3+.
#else
#define CYTHON_ABI "0_28_3"
#define CYTHON_FUTURE_DIVISION 0
#include <stddef.h>
#ifndef offsetof
#define offsetof(type, member) ( (size_t) & ((type*)0) -> member )
#endif
#if !defined(WIN32) && !defined(MS_WINDOWS)
#ifndef __stdcall
...
...
...
gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing \
-I/usr/include/python2.7 -o main.so main.c
Generiamo una libreria (.so)!
Verifichiamo
In [1]: from main import print_secret
In [2]: import dis
In [3]: dis.disassemble(print_secret.func_code)
Niente co_code !
Idea #4
Vantaggi
- si possono applicare le tecniche di sicurezza adottate in C
- performances
- reverse engineering difficile (non impossibile!)
Svantaggi
- cython non è perfetto (ha le sue limitazioni)
Idea #5
Modificare l'interprete python
...
/* Instruction opcodes for compiled code */
#define POP_TOP 1
#define ROT_TWO 2
#define ROT_THREE 3
#define DUP_TOP 4
#define DUP_TOP_TWO 5
#define NOP 9
#define UNARY_POSITIVE 10
#define UNARY_NEGATIVE 11
#define UNARY_NOT 12
...
opcode.h
idea stupida: opcode shuffling
Idea #5
vantaggio: diventa difficile analizzare un file .pyc
svantaggio: bisogna distribuire l'interprete (e proteggerlo in qualche modo)
Idea #6
Combinazioni di tecniche diverse
- pacchettizzare l'applicazione assieme all'interprete
- offuscare il codice C dell'interprete (modificato)
- disabilitare alcune funzionalità di introspezione e di compilazione del bytecode
- compilare nativamente alcuni parti di codice critiche
- gestione custom import moduli (con crittografia)
Considerazioni finali
Ne vale la pena, ma dipende dal progetto !
Considerare altre strade:
- Saas (software as a service)
- utilizzare altri linguaggi (ad es. C)
- soluzioni miste
Grazie!
domande? idee?
deck
By Marco Federighi
deck
- 840