python編譯后的pyd爆破
最近接觸一個(gè)國外某app的協(xié)議軟件,是python3.8寫的,它把關(guān)鍵模塊都編譯成了pyd,然后使用pyinstaller打包發(fā)布給用戶。軟件啟動(dòng)后檢查機(jī)器碼,然后需要輸入授權(quán)碼才可使用,看著很是惱火,所以想嘗試破解。其中關(guān)鍵應(yīng)該是需要爆破pyd里的邏輯,修改匯編代碼來實(shí)現(xiàn)繞過授權(quán)。
前提知識(shí)
1. py、pyc、pyo、pyd
py: python 腳本文件(source code)
pyc: 腳本文件編譯得到的字節(jié)碼, 二進(jìn)制文件,python文件經(jīng)過編譯器編譯之后的文件。可以提高文件加載速度。
pyo: 腳本文件開啟優(yōu)化編譯選項(xiàng)(-O)編譯得到的字節(jié)碼,二進(jìn)制文件,優(yōu)化編譯后的文件??梢酝ㄟ^python -O file.py生成。
pyd: 基本的Windows DLL文件,python的動(dòng)態(tài)鏈接庫。
2. 編譯pyd
要編譯的腳本:uitl1.py
- def fun_hello(s):
- if s == 1:
- return 'hello world'
- elif s == 2:
- return '222222222'
提供編譯腳本:setup.py
- from setuptools import setup
- from Cython.Build import cythonize
- setup(
- name='test',
- ext_modules=cythonize('util1.py')
- )
在setup.py文件所在目錄下進(jìn)行如下命令:
- python setup.py build_ext --inplace
這樣就能看到同級(jí)目錄下生成pyd文件了。32位的python生成pyd文件是32位的,64位的python生成的是64位的。
3. 使用pyd
test.py
- import util1
- if __name__ == '__main__':
- print(util1.fun_hello(2))
4. pyinstaller打包py到exe
- pip install pyinstaller
- pyinstaller test.py
5. 解包pyinstaller打包的exe
pyinstxtractor.py即可。這個(gè)代碼不長(zhǎng),可以調(diào)試看看,熟悉下打包的exe組成。需要注意的是,被打包的文件都是zlib.compress壓縮過后,再按照固定格式組成exe的,所以直接修改打包后的exe的16進(jìn)制碼來爆破貌似不好操作。只能解包后修改pyd,然后找齊依賴的庫,重新pyinstaller打包,實(shí)現(xiàn)爆破。
https://github.com/countercept/python-exe-unpacker
6. pyc反編譯
uncompyle6支持python3.8的pyc的反編譯。
需要注意的是,如果是pyinstaller解包后取到的pyc文件,文件頭部的magic被抹除過了,所以需要把對(duì)應(yīng)版本python的magic加上來,可以裝對(duì)應(yīng)版本python,然后到安裝目錄下隨便找個(gè)pyc文件,看一下頭部,然后用010Editor復(fù)制到解包后的pyc,就可以正常反編譯了。
下圖是python3.8_32位的magic頭:
https://github.com/rocky/python-uncompyle6
pyd文件匯編代碼和python腳本的對(duì)應(yīng)關(guān)系分析
前面的前提知識(shí),隨便搜搜都能找到。但是如何才能直接修改pyd的匯編代碼,實(shí)現(xiàn)python腳本流程的更改呢?
我百度谷歌搜了半天也沒找到合適的資料,也許很少有人破解python編譯打包的exe吧。
那么下面就是我做的工作了,也是本帖的價(jià)值所在了。
我自己寫了一個(gè)python小腳本,然后編譯成了pyd,它會(huì)生成一個(gè)中間的util1.c文件,代碼大概有3000多行。只要花時(shí)間精力熟悉這個(gè)c文件,然后對(duì)照著ida就可以了解python腳本轉(zhuǎn)成C然后編譯成匯編指令,它們3者之間大概對(duì)應(yīng)關(guān)系了。
下面略過大概1天的工作量,直接給出我們拿到一個(gè)pyd后,怎么快速找到我們要找的關(guān)鍵python代碼。然后直接爆破。
把要分析的pyd文件拖到對(duì)應(yīng)32位或64位的IDA:
大概所有的pyd都只有這個(gè)一個(gè)導(dǎo)出函數(shù),當(dāng)這個(gè)pyd模塊被其他py腳本import時(shí)會(huì)調(diào)用這個(gè)導(dǎo)出函數(shù)進(jìn)行模塊初始化。
跳轉(zhuǎn)到dword_1000634C可以看到一個(gè)結(jié)構(gòu)體,里面有一個(gè)關(guān)鍵的成員__pyx_moduledef_slots。
這個(gè)成員是一個(gè)結(jié)構(gòu)體數(shù)組。
里面有個(gè)關(guān)鍵函數(shù)__pyx_pymod_exec_util1負(fù)責(zé)初始化python腳本里的所有變量,函數(shù),常量等等,把他們都對(duì)應(yīng)到pyobject,然后就只使用這些pyobject了。所以匯編里看流程就很難,因?yàn)闆]有明顯的明文了。
定位到 __pyx_pymod_exec_util1后,我們主要的目的是找常量和pyobject的對(duì)照表,python腳本里的函數(shù)名和匯編函數(shù)的對(duì)照表,有這2個(gè)表,python腳本和匯編的對(duì)應(yīng)關(guān)系就明朗了。這里就只能手動(dòng)往下翻了。
翻到類似調(diào)用 PyUnicode_InternFromString 的地方,大概就是我們要找的常量對(duì)照表了。
也就是C文件里的這個(gè)表。
其中offset dword_10006DFC就是代表字符串"222222222"的pyobject,直接找它的交叉引用就可以定位一些關(guān)鍵代碼了。
我們繼續(xù)在 __pyx_pymod_exec_util1 里找python腳本函數(shù)對(duì)應(yīng)匯編函數(shù)的那個(gè)表。
跳過去:
aFunHello指向python腳本里的函數(shù)名。
__pyx_pf_5util1_fun_hello就是對(duì)應(yīng)的匯編函數(shù)。
可以看到,只要找到這個(gè)表,就很容易定位我們要找的python腳本函數(shù)對(duì)應(yīng)的匯編實(shí)現(xiàn)了。
其實(shí)我們也可以不必如上這么麻煩。只要在.data段里翻一翻?;蛘遱tring窗口找到感興趣的字符串交叉引用也能很快找到這個(gè)表。
需要知道的就是, aFunHello下面就是對(duì)應(yīng)的匯編實(shí)現(xiàn)函數(shù)。
現(xiàn)在終于可以去分析fun_hello這個(gè)python腳本函數(shù)對(duì)應(yīng)的匯編函數(shù)了。
可以看到腳本里的 s == 1 對(duì)應(yīng)的匯編就是 __Pyx_PyInt_EqObjC 然后下面會(huì)使用PyObject_IsTrue判斷這個(gè)函數(shù)的返回值。
那么爆破點(diǎn)就找到了。把 jz short loc_10004753 改成jnz short loc_10004753即可。
IDA-》edit-》Patch program-》Assemble修改,然后 IDA-》edit-》Patch program-》Apply patches to input file即可得到修改后的pyd文件。
這樣就實(shí)現(xiàn)了修改python腳本的執(zhí)行邏輯了。
正常腳本應(yīng)該是輸出22222222才對(duì),因?yàn)槲覀兊谋?,輸出了hello world!
這里我只是簡(jiǎn)單分析了if語句的修改,可以多寫幾個(gè)例子。實(shí)現(xiàn)修改其他流程。
在此只是拋轉(zhuǎn)引玉,給大家一點(diǎn)點(diǎn)參考。省一點(diǎn)點(diǎn)時(shí)間。
另:大家看了半天以為我是分析那個(gè)國外app協(xié)議軟件,其實(shí)我還沒有搞定那個(gè)破解,所以只把自己這段時(shí)間的分析工作貼了上來,僅供參考。