Skip to content

Commit 2076337

Browse files
committed
Update README with BCC notes
1 parent f7cc77a commit 2076337

1 file changed

Lines changed: 110 additions & 1 deletion

File tree

README.md

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ This repository contains some scripts for statically decrypting Python scripts p
44

55
Blog post for further reading: https://cyber.wtf/2025/02/12/unpacking-pyarmor-v8-scripts/
66

7+
Please also take note of the [Disclaimer](#disclaimer) section below before you continue.
8+
79
## Usage
810

911
Initial preparation: Go into `py312` and run `docker build -t pyarmor312 .`. Also create a virtual env where you install `pycryptodome`.
1012

11-
1. Open up the native Pyarmor module in IDA, find the MD5 key derivation function, adjust `ida_getkey.py` and run it in IDAPython. Adjust `decrypt_gcm.py` with the key you obtained.
13+
1. Open up the native Pyarmor module in IDA, find the MD5 key derivation function, adjust `ida_getkey.py` and run it in IDAPython. Adjust `decrypt_gcm.py` with the key you obtained. Alternatively, a Binary Ninja version of the script is available (`bn_getkey.py`).
1214
1. Run `python decrypt_gcm.py /path/to/malware/malware.py`
1315
1. Run `docker run --rm -u $(id -u):$(id -g) -v $(pwd)/analyze_crypted_code.py:/script.py:ro -v /path/to/malware:/data -it pyarmor312 /script.py /data/malware.py.dec`
1416
1. Run `python decrypt_gcm.py /path/to/malware/malware.py.dec` - it will now use the json file generated by the step above to decrypt individual functions and generate a `dec2` file
@@ -24,6 +26,97 @@ Go to the PyInit export and scroll almost all the way down, until you see a plac
2426
Inside that function you'll find the necessary details for `ida_getkey.py`.
2527

2628

29+
## BCC mode
30+
31+
If the script you're analyzing is protected with BCC mode, step 2 will additionally yield an ELF file (`.dec.elf`).
32+
This ELF contains Python functions that were compiled to native code.
33+
34+
You can use the `bcc_info.py` script with the custom Python interpreter for obtaining further details about the BCC functions.
35+
That includes:
36+
* Function offsets in the ELF
37+
* Suggested function names
38+
* List of constants used by the native code
39+
40+
This information is also written to a json file, which can be used with the IDAPython script `ida_annotate_bcc.py` in order to import the data into IDA.
41+
42+
**Note:** The ELF format is only followed in the broadest strokes, and IDA may refuse to load the file as ELF. In that case, simply load it as binary file. For Windows targets, you should make sure to set the compiler to Visual C++ so that the correct ABI will be used.
43+
44+
45+
You may find the following reverse engineered definitions (Pyarmor v9, Python 3.11/3.12) helpful when looking at BCC code:
46+
```c
47+
enum GLOBAL_OPS
48+
{
49+
GLOBAL_DELETE = 0x0,
50+
GLOBAL_GET = 0x1,
51+
GLOBAL_RETURN_GLOBALS = 0x2,
52+
GLOBAL_SPECIAL_ENTER = 0x4, /* __enter__ */
53+
GLOBAL_SPECIAL_EXIT = 0x5, /* __exit__ */
54+
GLOBAL_SET_MIN = 0x10, /* anything above means Set (pointer value instead of int) */
55+
};
56+
57+
struct bcc_ftable
58+
{
59+
_QWORD p_stdin;
60+
void (__fastcall *memset)(void *, _QWORD, _QWORD);
61+
_QWORD p_stderr;
62+
_QWORD fprintf;
63+
void *(__fastcall *PyNumber_operator)(void *a, void *b, int operatortype);
64+
__int64 (*build_collection)(__int64 colltype, __int64 count, ...);
65+
__int64 *(__fastcall *call_python_func)(__int64 bDontCall, __int64 *pyCallable, int bArgsRequired, int bKwArgsRequired, __int64 *argsTuple, __int64 *kwargsDict);
66+
int (__fastcall *set_exception_if_none_was_raised)(int mode);
67+
void *(__fastcall *comparison)(void *unused, int operatortype, void *left, void *right);
68+
_QWORD qword48;
69+
_QWORD fetch_exception;
70+
_QWORD string_format;
71+
void *(__fastcall *globals_operation)(void *unused, void *key, GLOBAL_OPS modeOrValueForSet);
72+
_QWORD op_mkfunc_not_available;
73+
void *(__fastcall *iter_next)(void *);
74+
_QWORD qword78;
75+
_QWORD qword80;
76+
_QWORD update_exception_info;
77+
_QWORD qword90;
78+
__int64 (__fastcall *unpack_values)(void *unused, void *input, int maxCount, void **output);
79+
_QWORD qwordA0;
80+
_QWORD new_function;
81+
_QWORD qwordB0;
82+
_QWORD import_stuff;
83+
_BYTE gapC0[32];
84+
_QWORD Py_NoneStruct;
85+
_QWORD Py_TrueStruct;
86+
_QWORD Py_FalseStruct;
87+
_QWORD gapF8;
88+
int (__fastcall *PyBytes_AsStringAndSize)(void *obj, char **buffer, _QWORD *length);
89+
void *(__fastcall *PyCell_Get)(void *cell);
90+
void *(__fastcall *PyCell_New)(void *ob);
91+
int (__fastcall *PyCell_Set)(void *cell, void *value);
92+
void (__fastcall *PyErr_Clear)();
93+
void *(__fastcall *PyErr_Occurred)();
94+
void (__fastcall *PyErr_SetObject)(void *type, void *value);
95+
void *(__fastcall *PyEval_GetGlobals)();
96+
void *(__fastcall *PyImport_ImportModule)(const char *name);
97+
void *(__fastcall *PyImport_ImportModuleLevel)(const char *name, void *globals, void *locals, void *fromlist, int level);
98+
int (__fastcall *PyList_Append)(void *list, void *item);
99+
void *(__fastcall *PyList_New)(__int64 len);
100+
void *(*PyObject_CallFunction_SizeT)(void *callable, const char *format, ...);
101+
void *(*PyObject_CallFunctionObjArgs)(void *callable, ...);
102+
void *(*PyObject_CallMethod_SizeT)(void *obj, const char *name, const char *format, ...);
103+
int (__fastcall *PyObject_DelItem)(void *, void *key);
104+
void *(__fastcall *PyObject_GetAttr)(void *, void *attr_name);
105+
void *(__fastcall *PyObject_GetItem)(void *, void *key);
106+
void *(__fastcall *PyObject_GetIter)(void *);
107+
int (__fastcall *PyObject_IsTrue)(void *);
108+
int (__fastcall *PyObject_SetAttr)(void *, void *attr_name, void *v);
109+
int (__fastcall *PyObject_SetItem)(void *, void *key, void *v);
110+
int (__fastcall *PySet_Add)(void *, void *key);
111+
void *(__fastcall *PySet_New)(void *iterable);
112+
void *(__fastcall *PySlice_New)(void *start, void *stop, void *step);
113+
void *(__fastcall *PyTuple_GetItem)(void *p, __int64 pos);
114+
void (__fastcall *Py_DecRef)(void *);
115+
void (__fastcall *Py_IncRef)(void *);
116+
};
117+
```
118+
119+
27120
# Custom Python notes
28121

29122
The Docker image builds a custom Python version that is able to read objects serialized by Pyarmor.
@@ -35,3 +128,19 @@ The patch introduces an `armor` flag into `RFILE` so that we can only apply the
35128
Otherwise, Python breaks because it cannot unmarshal its builtin objects.
36129

37130
**If you have a protected version that utilizes a different Python version, you need to build that specific version and possibly adjust the patch.**
131+
132+
133+
# Disclaimer
134+
135+
This repository contains tools developed by G DATA Advanced Analytics GmbH intended strictly for malware analysis and related security research.
136+
137+
**Important Notice**
138+
- These tools are designed for legitimate purposes only, such as analyzing malicious software in controlled environments.
139+
- Use of these tools for any unauthorized or illegal activities, including the analysis of non-malicious software, is strictly prohibited.
140+
- Users are solely responsible for ensuring compliance with all applicable local, national, and international laws and regulations.
141+
142+
**No Warranty**
143+
144+
This software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from, out of, or in connection with the software or the use or other dealings in the software.
145+
146+
By using this software, you agree to these terms.

0 commit comments

Comments
 (0)