During these days, I have been working on cracking expensive commercial software. That’s why I write up in English. Hopes that no one spotted it 😽

Understand the Python packed binary

I have succeeded in cracking this software long ago. At that time the programs are naïve. The newer version confuses me. When I opened it under reverse engineering software such as Ghidra and IDA Pro, I got only confusing codes. A genius guy at the BBS releases a patcher written in Golang for the older versions. I then reverse-engineer the patcher. The code isn’t clear to me. But it says an immediate value of “40202b2028207370”, meaning “@ + ( sp” in my opinion. I suddenly realized, the program is compiled to an executable file (PE or ELF) from Python scripts!

From the Internet, many articles tell me how to unpack the binary compiled by PyInstaller. The official pyi-archive_viewer could do it for me. However, it confuses me because I got only several files from the top-level, instead of the innumerable files in the PYZ-00.pyz! I think pyinstxtractor is easier to use and straightforward. It extracts all the files in one run.

The pyc to py by uncompyle6 goes smoothly, except that you have to use Python versions earlier than 3.9. There are, indeed, a lot of struggling efforts.

After obtaining the Python source codes, I then lost. I could, of course, patch the .py files. I cannot restore them to the binary file. It is not mentioned in the Internet. Maybe it is impossible. One thing to mention is, the file is packed using Python 2.7.

And how I got to know where to patch is, I run the program under strace. It is, the Python program calls a binary program with a certain parameter and parses the output. I can patch that binary program!

Have to patch the binary

Well, the code isn’t clear to me either. No, it is me that is a dummy and weak. I patched the program using Ghidra by changing three JNZ to JMP in the .so.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
                             **************************************************************
* Licensing::LicensingStateLexActivator::is_licensed(Lice... *
**************************************************************
undefined __thiscall is_licensed(LicensingStateLexActiva
undefined AL:1 <RETURN>
LicensingState RDI:8 (auto) this
FeatureIdentif RSI:8 param_1
undefined1 Stack[-0x20]:1 local_20 XREF[2]: 001c5343(*),
001c53a0(*)
_ZNK9Licensing26LicensingStateLexActivator11is XREF[6]: Entry Point(*), 003cdda4,
Licensing::LicensingStateLexActivator::is_lice 003e1844(*), 00417764(*),
0041776e(*), 00432070(*)
001c52f0 41 55 PUSH R13
001c52f2 48 8d 15 LEA RDX,[Licensing::LicensingStateLexActivator::is
47 f3 ff ff
001c52f9 41 54 PUSH R12
001c52fb 49 89 f4 MOV R12,param_1
001c52fe 55 PUSH RBP
001c52ff 48 89 fd MOV RBP,this
001c5302 48 83 ec 10 SUB RSP,0x10
001c5306 48 8b 07 MOV RAX,qword ptr [this]
001c5309 48 8b 40 60 MOV RAX,qword ptr [RAX + 0x60]
001c530d 48 39 d0 CMP RAX,RDX
001c5310 75 6e JNZ LAB_001c5380 <- JMP
001c5312 48 8b 47 10 MOV RAX,qword ptr [this + 0x10]
001c5316 8b 40 10 MOV EAX,dword ptr [RAX + 0x10]
001c5319 89 c2 MOV EDX,EAX
001c531b 83 e2 fd AND EDX,0xfffffffd
001c531e 83 fa 14 CMP EDX,0x14
001c5321 74 63 JZ LAB_001c5386
001c5323 83 f8 01 CMP EAX,0x1
001c5326 74 5e JZ LAB_001c5386
LAB_001c5328 XREF[1]: 001c5384(j)
001c5328 48 8b 45 00 MOV RAX,qword ptr [RBP]
001c532c 48 8d 15 LEA RDX,[Licensing::LicensingStateLexActivator::ge
9d ff ff ff
001c5333 48 8b 80 MOV RAX,qword ptr [RAX + 0xa8]
a8 00 00 00
001c533a 48 39 d0 CMP RAX,RDX
001c533d 75 61 JNZ LAB_001c53a0 <- JMP
001c533f 48 8b 75 10 MOV param_1,qword ptr [RBP + 0x10]
001c5343 4c 8d 6c LEA R13=>local_20,[RSP + 0x8]
24 08
001c5348 4c 89 ef MOV this,R13
001c534b 48 83 c6 58 ADD param_1,0x58
LAB_001c534f XREF[1]: 00417762(*)
001c534f e8 fc d5 CALL QList<QString>::QList undefined QList(QList<QString> *
fb ff

.....

LAB_001c5380 XREF[2]: 001c5310(j), 0041776b(*)
001c5380 ff d0 CALL RAX
001c5382 84 c0 TEST AL,AL
001c5384 75 a2 JNZ LAB_001c5328 <- JMP
LAB_001c5386 XREF[2]: 001c5321(j), 001c5326(j)
001c5386 48 83 c4 10 ADD RSP,0x10

It starts but fails to conduct a search. I then NOP the CALL in the main program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                     try { // try from 004039dc to 004039e0 has its CatchHandler @
LAB_004039dc XREF[1]: 004055c7(*)
004039dc e8 cf f8 CALL libthomasv2_kernel.so.1.0.0::Licensing::Checke undefined is_licensed_or_exit(Fe <- NOP
ff ff
} // end try from 004039dc to 004039e0
004039e1 4c 89 e7 MOV RDI,R12
004039e4 48 8d 5c LEA RBX=>local_2b4,[RSP + 0x34]
24 34
004039e9 e8 f2 05 CALL QString::~QString undefined ~QString(QString * this)
00 00
004039ee be 01 00 MOV ESI,0x1
00 00
004039f3 48 89 df MOV RDI,RBX
004039f6 48 89 5c MOV qword ptr [RSP + local_2e0],RBX
24 08

The story in Windows

The Windows version is similar to this, only the 1 could bring you to the right place. Three JMP is enough.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
                      LAB_1400138a2                                            XREF[1]:             14001386f(j)  
1400138a2 48 8b 46 10 MOV RAX,qword ptr [RSI + 0x10]
1400138a6 8b 48 10 MOV param_1,dword ptr [RAX + 0x10]
1400138a9 83 f9 16 CMP param_1,0x16
1400138ac 75 2d JNZ LAB_1400138db <- JMP
1400138ae 48 8b 07 MOV RAX,qword ptr [RDI]
1400138b1 48 8b 58 38 MOV RBX,qword ptr [RAX + 0x38]
1400138b5 48 8b 06 MOV RAX,qword ptr [RSI]
1400138b8 48 8d 55 40 LEA param_2=>local_res8,[RBP + 0x40]
1400138bc 48 8b ce MOV param_1,RSI
1400138bf ff 90 88 CALL qword ptr [RAX + 0x88]
00 00 00
1400138c5 90 NOP
1400138c6 4c 8b c0 MOV param_3,RAX
1400138c9 49 8b d4 MOV param_2,R12
1400138cc 48 8b cf MOV param_1,RDI
1400138cf ff d3 CALL RBX
1400138d1 90 NOP
1400138d2 48 8d 4d 40 LEA param_1=>local_res8,[RBP + 0x40]
1400138d6 e9 42 02 JMP LAB_140013b1d
00 00
LAB_1400138db XREF[1]: 1400138ac(j)
1400138db 83 f9 14 CMP param_1,0x14
1400138de 75 2d JNZ LAB_14001390d <- JMP
1400138e0 48 8b 07 MOV RAX,qword ptr [RDI]
1400138e3 48 8b 58 40 MOV RBX,qword ptr [RAX + 0x40]
1400138e7 48 8b 06 MOV RAX,qword ptr [RSI]
1400138ea 48 8d 55 40 LEA param_2=>local_res8,[RBP + 0x40]
1400138ee 48 8b ce MOV param_1,RSI
1400138f1 ff 90 80 CALL qword ptr [RAX + 0x80]
00 00 00
1400138f7 90 NOP
1400138f8 4c 8b c0 MOV param_3,RAX
1400138fb 49 8b d4 MOV param_2,R12
1400138fe 48 8b cf MOV param_1,RDI
140013901 ff d3 CALL RBX
140013903 90 NOP
140013904 48 8d 4d 40 LEA param_1=>local_res8,[RBP + 0x40]
140013908 e9 10 02 JMP LAB_140013b1d
00 00
LAB_14001390d XREF[1]: 1400138de(j)
14001390d 83 f9 01 CMP param_1,0x1 <- JMP
140013910 75 28 JNZ LAB_14001393a
140013912 48 8b 4e 10 MOV param_1,qword ptr [RSI + 0x10]
140013916 48 83 c1 60 ADD param_1,0x60
14001391a e8 41 53 CALL FUN_140008c60 undef
ff ff
14001391f 48 8b 17 MOV param_2,qword ptr [RDI]

The bad thing for GDB in Windows is that the entry point is keeping changing. You can use info file to manually read it. That is,

1
2
3
4
5
(gdb) starti
(gdb) info file
# It tells you the Entry point is 0x7ff76017bcf0.
(gdb) layout asm # Find the asm code, and search in the ghidra.
# You will do the transformation between 0x14001bcf0 in file to 0x7ff76017bcf0 in the memory, manually.

The Windows version needs another patch, say

1
2
3
4
5
6
7
8
18000cae0 48 8d 4c        LEA        RCX=>local_res10,[RSP + 0x58]
24 58
18000cae5 ff 15 3d CALL qword ptr [->QT5CORE.DLL::QString::~QString] = 0005f9e2
3b 03 00
18000caeb 84 db TEST BL,BL
18000caed 74 29 JZ LAB_18000cb18 <- NOP
18000caef 48 83 c4 40 ADD RSP,0x40
18000caf3 5b POP RBX

For the Windows version, the code is different. I can bypass the check through a wrapper program. I’m still studying the logic of the Windows version.

Now it can search in both GNU/Linux and Windows.

Another binary

It doesn’t use Python. But I cannot find strings. I give up.

Finding the strings through grep, there are many .do files. I give up.

Dixi et salvavi animam meam

I say nothing of the software’s name. I do respect the software developers.