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