Python merupakan suatu scripting language atau bahasa scripting karena sifatnya yang ringan biasanya digunakan untuk tujuan automation, exploit development, pengembangan tools bahkan ada juga web apps yang memakai python dengan framework seperti flask maupun django.
Karena mudah dipelajari dan hampir compatible di semua platform, python banyak digunakan mulai dari programmer hingga pentester dan pada topik kali ini kita akan membahas bagaimana melakukan reverse engineering pada python karena biasanya programmer melakukan semacam teknik packing pada source code seperti melakukan obfuscate, compile serta converting kedalam bytecode.
Hal yang perlu diketahui disini beberapa program python pada banyak kasus tidak bisa dikembalikan kedalam source code aslinya jika programmer melakukan compile dan serialization menjadi bytecode seperti menggunakan marshal, pyarmor ataupun menjadikannya suatu executable file tapi jika programmer hanya melakukan obfuscate pada code menggunakan fungsi semacam eval, lambda, zlib, exec besar kemungkinan kita masih bisa mendapatkan source code asli pada program.
Kita bisa contohkan dengan program sederhana
import codecs
def validation(input):
valid_input = codecs.decode("ouvarxn-grpu1336", "rot-13")
if input == valid_input:
return True
return False
print("Enter valid input: ", end="")
input_user = input()
if validation(input_user):
print("Input correct!!")
else:
print("Input incorrect no have access!!")
Scriptnya sangat sederhana dimana terdapat satu function untuk melakukan validasi input dengan string yang sudah dilakukan substitusi menggunakan ROT-13 lalu setelah di obfuscate menjadi seperti ini :
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'A+pAW8w/+9cxY35EeyFVcLbyqDKGpUOanCFCUVVwGWItIYh9LzSSfRhole9RY8NVAqm0Bah4cir/PYFOiiCVIi1GYChx1LLEryJr+r3w0YzDOW0X8wGWYV90lCZwxIi5r4DAhED74pv6ZfYp7jf3tX4rqiVTMgCpUKa7CWGdZ0xV2dom1pnQNOz+Uk9uDlDPVEkizxnF4pDe1NnS9rWKmbls9A3Ho+wyR66EjIz3Qm4sckZDEMgKEIjogXq07OlXac8e8DY9HHOVzSQ5tXx5M8GZxa3nAx41zRazZRm2UPgh7ZpwocDx6/uQ05I03N63GPjsmpXjCZ/kHtbPvtGe1jWfqZf+tNkqC3e9Yy8OYJIh85XaZnesf7xZmjskHUtcJNkebJ5XA08w2Ptm/hHMbot288OWyQrTNR2ZOOXC+sSIa1wRL7YdNFAWUSd97hoc5Z7Am26D9Lm+wN4moDKJOXM639h0v39+3y2iBd3s6nvx1ZMJ7apc0LVYh6OSxTBG6VbOPDNAAAkQ2udwFwJe'))
Dan jika kita run keduanya sama sama menampilkan output yang sama dan program juga berjalan dengan benar hanya saja pada code obfuscate kita tidak mengerti fungsi apa dan seperti apa source code didalamnya karena sudah dilakukan encoding dengan zlib dan base64.

Jadi mudah saja untuk melakukan deobfuscate kita tinggal me-replace fungsi exec() dengan print() karena kita tau jika fungsi exec() digunakan untuk melakukan eksekusi source code sedangkan fungsi print() digunakan untuk menampilkan output ke layar
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));print((_)(b'A+pAW8w/+9cxY35EeyFVcLbyqDKGpUOanCFCUVVwGWItIYh9LzSSfRhole9RY8NVAqm0Bah4cir/PYFOiiCVIi1GYChx1LLEryJr+r3w0YzDOW0X8wGWYV90lCZwxIi5r4DAhED74pv6ZfYp7jf3tX4rqiVTMgCpUKa7CWGdZ0xV2dom1pnQNOz+Uk9uDlDPVEkizxnF4pDe1NnS9rWKmbls9A3Ho+wyR66EjIz3Qm4sckZDEMgKEIjogXq07OlXac8e8DY9HHOVzSQ5tXx5M8GZxa3nAx41zRazZRm2UPgh7ZpwocDx6/uQ05I03N63GPjsmpXjCZ/kHtbPvtGe1jWfqZf+tNkqC3e9Yy8OYJIh85XaZnesf7xZmjskHUtcJNkebJ5XA08w2Ptm/hHMbot288OWyQrTNR2ZOOXC+sSIa1wRL7YdNFAWUSd97hoc5Z7Am26D9Lm+wN4moDKJOXM639h0v39+3y2iBd3s6nvx1ZMJ7apc0LVYh6OSxTBG6VbOPDNAAAkQ2udwFwJe'))
Disini kita ubah exec() dengan print() lalu jika kita jalankan

terlihat menampilkan output tetapi output masih menunjukkan code yang masih ter-obfuscate dan bisa kita asumsikan jika obfuscate pada program menggunakan double encoding dan caranya untuk mendapatkan source code aslinya dengan copy ouput dan kita buat file python baru lalu ubah exec() menjadi print() jalankan dan jika belum menampilkan code asli ulangi hingga menemukan source code aslinya.
Cara cepatnya lebih baik kita membuat script automation decoding agar tidak perlu repot repot melakukan decoding
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));
obfuscate_str = (_)(b'A+pAW8w/+9cxY35EeyFVcLbyqDKGpUOanCFCUVVwGWItIYh9LzSSfRhole9RY8NVAqm0Bah4cir/PYFOiiCVIi1GYChx1LLEryJr+r3w0YzDOW0X8wGWYV90lCZwxIi5r4DAhED74pv6ZfYp7jf3tX4rqiVTMgCpUKa7CWGdZ0xV2dom1pnQNOz+Uk9uDlDPVEkizxnF4pDe1NnS9rWKmbls9A3Ho+wyR66EjIz3Qm4sckZDEMgKEIjogXq07OlXac8e8DY9HHOVzSQ5tXx5M8GZxa3nAx41zRazZRm2UPgh7ZpwocDx6/uQ05I03N63GPjsmpXjCZ/kHtbPvtGe1jWfqZf+tNkqC3e9Yy8OYJIh85XaZnesf7xZmjskHUtcJNkebJ5XA08w2Ptm/hHMbot288OWyQrTNR2ZOOXC+sSIa1wRL7YdNFAWUSd97hoc5Z7Am26D9Lm+wN4moDKJOXM639h0v39+3y2iBd3s6nvx1ZMJ7apc0LVYh6OSxTBG6VbOPDNAAAkQ2udwFwJe')
decode = str(obfuscate_str).replace('b"exec', "").replace('"', '')
result = b""
for i in range(0, 4):
x = eval(decode)
decode = str(x).replace('b"exec', "").replace('"', '')
result = x
print(result.decode('utf-8'))
Jadi fungsi pada script diatas yaitu akan melakukan iterasi sebanyak 5 kali serta melakukan replace fungsi exec lalu dilakukan eval untuk menyimpan hasil decoding pada variable x hingga menemukan source code asli dan terakhir menggunakan print untuk menampilkan output code pada layar

Jika dijalankan maka akan manampilkan source code hasil deobfuscate yang menunjukkan kita berhasil mendapatkan source code aslinya. Case disini source code bisa dikembalikan pada bentuk semula karena hanya obfuscate memakai double encoding tapi bagaimana jika programmer melakukan compiling ataupun serialization ? jawabannya dengan melakukan analysis bytecode
Kita perlu kenalan dulu dengan marshal, jadi marshal merupakan fungsi atau modul bawaan dari python yang digunakan untuk mendapatkan byte code dari suatu script python itu sendiri jadi bisa kita sebut marshal ini sebagai suatu pseudo compiled dimana dengan marshal kita bisa melakukan converting code python ke binary format dan serialization menjadi bytecode object.
kita bisa melakukan compile python script menggunakan command :
python3 -m py_compile source_codes.py
dan outputnya nanti akan tersimpan pada directory __pycache__ yang dimana extension file berubah menjadi .pyc. Sebenarnya.pyc merupakan file marshal yang ditambahkan header dan jika beruntung kita bisa melakukan decompile menggunakan uncompyle6 untuk mendapatkan source code asli tapi tidak selalu berhasil jadi cara alternatifnya kita perlu membaca bytecode mengunakan modul python yaitu dis.
untuk mengambil bytecode script python pada pyc mudah saja kita perlu mengambil byte setelah header dan paddingnya yaitu 16 byte lalu kita simpan pada file terpisah dan lakukan load menggunakan marshal.loads()

Kita bisa ambil row kedua setelah 16 byte pada offset 00000010
import marshal
f = open("__pycache__/source_code.cpython-311.pyc", "rb").read()[16:]
#f = open("source_code.py", "r").read()
print(f)
#code = compile(f, '', 'exec')
#print(marshal.dumps(code))
dan jika kita jalankan

maka akan menampilkan semua bytecode marshal pada file pyc kemudian kita bisa copas semua output lalu buat file baru yang nantinya kita akan eksekusi menggunakan marshal.loads()

jika kita eksekusi sama saja dengan program .pyc kita

Disini bytecode dari pyc berguna untuk melakukan disassemble menggunakan modul dis yang kita bahas sebelumnya

kita hanya perlu import modul dis dan lakukan pemanggilan fungsi dis.dis dengan parameter hasil marshal.loads() yang sebelumnya kita dapatkan

Maka akan menampilkan hasil disassemble opcode dari script python hal ini bisa sulit jika codenya rumit tapi dari sini kita bisa melihat fungsi apa saja dan bagaimana suatu program itu bekerja
Disassembly of <code object validation at 0x7fe1e3401330, file "source_code.py", line 3>:
3 0 RESUME 0
4 2 LOAD_GLOBAL 1 (NULL + codecs)
14 LOAD_ATTR 1 (decode)
24 LOAD_CONST 1 ('ouvarxn-grpu1336')
26 LOAD_CONST 2 ('rot-13')
28 PRECALL 2
32 CALL 2
42 STORE_FAST 1 (valid_input)
5 44 LOAD_FAST 0 (input)
46 LOAD_FAST 1 (valid_input)
48 COMPARE_OP 2 (==)
54 POP_JUMP_FORWARD_IF_FALSE 2 (to 60)
6 56 LOAD_CONST 3 (True)
58 RETURN_VALUE
9 >> 60 LOAD_CONST 4 (False)
62 RETURN_VALUE
Contohnya pada fungsi validation dimana ada beberapa mnemonic seperti LOAD_GLOBAL yang digunakan untuk mengambil fungsi global dalam hal ini codecs lalu ada LOAD_ATTR yang mengambil attribute dengan 2 parameter yaitu ouvarxn-grpu1336 dan rot-13 yang di inisialisasi menggunakan LOAD_CONST dan dilakukan perbandingan menggunakan COMPARE_OP dari variable input(dalam hal ini input dari user) dengan valid_input yang mengambil dari LOAD_GLOBAL codecs.
Disini kita bisa analysis jika codecs melakukan decode ROT-13 pada string ouvarxn-grpu1336 dan jika kita decode ROT13 string asli merupakan bhineka-tech1336 lalu kita coba inputkan pada program

dan berhasil yang menunjukkan output Input Correct
Penutup
Sebenarnya masih banyak teknik yang bisa kita lakukan untuk RE pada python contohnya kita juga bisa melakukan overwrite internal module untuk hooking dan menampilkan suatu string / data tertentu atau jika pada executable file kita bisa memanfaatkan teknik memory dump untuk melihat data pada saat program berjalan. Jadi kita tidak mungkin membahas semuanya karena itu akan sangat panjang dan tulisan ini dibuat untuk para reverser engineer agar lebih memahami bagaimana melakukan reverse engineering pada python script jadi mungkin itu saja sekiann.
“Yang penting bukan seberapa pintar kamu, tetapi seberapa keras kamu berusaha.” -Albert Einstein
