wip: co_code
This commit is contained in:
@@ -6,6 +6,8 @@ Generally this project aims to statically convert (without executing) armored da
|
||||
|
||||
Currently we are trying to support Pyarmor 8.0 - latest (9.1.0), Python 3.7 - 3.13, platforms covering Windows, Linux, macOS, and Android, with obfuscating options as many as possible. (However, we only have limited tests.)
|
||||
|
||||
If the data starts with `PY` followed by six digits, it is supported. Otherwise, if it starts with `PYARMOR`, it is generated by Pyarmor 7 or before, and is not supported.
|
||||
|
||||
We cannot wait to make it public. Detailed write-up will be available soon. For those who are curious, temporarily you can check out [the similar work of G DATA Advanced Analytics](https://cyber.wtf/2025/02/12/unpacking-pyarmor-v8-scripts/).
|
||||
|
||||
## Build
|
||||
@@ -20,7 +22,7 @@ mv pyarmor-1shot[.exe] ../helpers
|
||||
|
||||
## Usage
|
||||
|
||||
Make sure the executable `pyarmor-1shot` exists in `helpers` directory, and run `helpers/shot.py` in Python 3 (no need to use the same version with obfuscated scripts) with the "root" directory of obfuscated scripts. It will recursively find and handle `pyarmor_runtime` and as much armored data as possible. For example:
|
||||
Make sure the executable `pyarmor-1shot` (`pyarmor-1shot.exe` on Windows) exists in `helpers` directory, and run `helpers/shot.py` in Python 3 (no need to use the same version with obfuscated scripts) with the "root" directory of obfuscated scripts. It will recursively find and handle `pyarmor_runtime` and as much armored data as possible. For example:
|
||||
|
||||
``` bash
|
||||
$ ls /path/to/scripts
|
||||
@@ -37,6 +39,10 @@ Note:
|
||||
- Subdirectories called `__pycache__` or `site-packages` will not be touched, and symbolic links will not be followed, to avoid repeat or forever loop and save time. If you really need them, run the script later in these directories (as "root" directory) and specify the runtime.
|
||||
- Archives, executables generated by PyInstaller and so on, must be unpacked by other tools before decrypting, or you will encounter undefined behavior.
|
||||
|
||||
## Feedback
|
||||
|
||||
Feel free to open an issue if you have any questions, suggestions, or problems. Don't forget to attach the armored data and the `pyarmor_runtime` executable if possible.
|
||||
|
||||
## Todo (PR Welcome!)
|
||||
|
||||
- [ ] Write-up
|
||||
|
@@ -62,6 +62,9 @@ class RuntimeInfo:
|
||||
data = f.read(16 * 1024 * 1024)
|
||||
cur = data.index(b'pyarmor-vax')
|
||||
|
||||
if data[cur+11:cur+18] == b'\x00' * 7:
|
||||
raise ValueError(f'{self.file_path} is a runtime template')
|
||||
|
||||
self.part_1 = data[cur:cur+20]
|
||||
|
||||
cur += 36
|
||||
|
1340
plusaes.hpp
Normal file
1340
plusaes.hpp
Normal file
File diff suppressed because it is too large
Load Diff
108
pyc_code.cpp
108
pyc_code.cpp
@@ -1,6 +1,7 @@
|
||||
#include "pyc_code.h"
|
||||
#include "pyc_module.h"
|
||||
#include "data.h"
|
||||
#include "plusaes.hpp"
|
||||
|
||||
/* == Marshal structure for Code object ==
|
||||
1.0 1.3 1.5 2.1 2.3 3.0 3.8 3.11
|
||||
@@ -120,6 +121,113 @@ void PycCode::load(PycData* stream, PycModule* mod)
|
||||
m_exceptTable = LoadObject(stream, mod).cast<PycString>();
|
||||
else
|
||||
m_exceptTable = new PycString;
|
||||
|
||||
// Pyarmor extra fields
|
||||
|
||||
if (!pyarmor_co_obfuscated_flag)
|
||||
return;
|
||||
|
||||
unsigned char extra_data[256] = {0};
|
||||
unsigned char extra_length = stream->getByte();
|
||||
stream->getBuffer(extra_length, extra_data);
|
||||
|
||||
unsigned char pyarmor_fn_count = extra_data[0] & 3;
|
||||
unsigned char pyarmor_co_descriptor_count = (extra_data[0] >> 2) & 3;
|
||||
if (extra_data[0] & 0xF0)
|
||||
{
|
||||
fprintf(stderr, "Unsupported Pyarmor CO extra flag (%02X)\n", extra_data[0]);
|
||||
fprintf(stderr, "Please open an issue at https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/issues to request support and help to make this tool better.\n");
|
||||
}
|
||||
if (pyarmor_co_descriptor_count > 1)
|
||||
{
|
||||
fprintf(stderr, "Unsupport multiple Pyarmor CO descriptors (%d in total)\n", pyarmor_co_descriptor_count);
|
||||
fprintf(stderr, "Please open an issue at https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/issues to request support and help to make this tool better.\n");
|
||||
}
|
||||
|
||||
unsigned char *extra_ptr = extra_data + 4;
|
||||
for (unsigned char i = 0; i < pyarmor_fn_count; i++)
|
||||
{
|
||||
unsigned char item_length = (*extra_ptr >> 6) + 2;
|
||||
// Ignore the details
|
||||
extra_ptr += item_length;
|
||||
}
|
||||
for (unsigned char i = 0; i < pyarmor_co_descriptor_count; i++)
|
||||
{
|
||||
unsigned char item_length = (*extra_ptr >> 6) + 2;
|
||||
unsigned char *item_end = extra_ptr + item_length;
|
||||
// Ignore low 6 bits
|
||||
extra_ptr++;
|
||||
unsigned long consts_index = 0;
|
||||
while (extra_ptr < item_end)
|
||||
{
|
||||
consts_index = (consts_index << 8) | *extra_ptr;
|
||||
extra_ptr++;
|
||||
}
|
||||
|
||||
pyarmorDecryptCoCode(consts_index, mod);
|
||||
}
|
||||
}
|
||||
|
||||
void PycCode::pyarmorDecryptCoCode(unsigned long consts_index, PycModule *mod)
|
||||
{
|
||||
PycRef<PycString> descriptor = getConst(consts_index).cast<PycString>();
|
||||
const std::string &descriptor_str = descriptor->strValue();
|
||||
if (descriptor_str.length() < 20)
|
||||
{
|
||||
fprintf(stderr, "Pyarmor CO descriptor is too short\n");
|
||||
return;
|
||||
}
|
||||
|
||||
PyarmorCoDescriptor *desc = (PyarmorCoDescriptor *)(descriptor_str.data() + 8);
|
||||
bool copy_prologue = desc->flags & 0x8;
|
||||
bool xor_aes_nonce = desc->flags & 0x4;
|
||||
bool short_code = desc->flags & 0x2;
|
||||
|
||||
unsigned int nonce_index = short_code
|
||||
? desc->short_nonce_index
|
||||
: desc->short_nonce_index + desc->decrypt_begin_index + desc->decrypt_length;
|
||||
unsigned char nonce[16] = {0};
|
||||
memcpy(nonce, m_code->value() + nonce_index, 12);
|
||||
nonce[15] = 2;
|
||||
if (xor_aes_nonce)
|
||||
{
|
||||
if (!mod->pyarmor_co_code_aes_nonce_xor_enabled)
|
||||
{
|
||||
fprintf(stderr, "FATAL: Pyarmor CO code AES nonce XOR is not enabled but used\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
unsigned char *xor_key = mod->pyarmor_co_code_aes_nonce_xor_key;
|
||||
for (int i = 0; i < 12; i++)
|
||||
nonce[i] ^= xor_key[i];
|
||||
}
|
||||
}
|
||||
|
||||
std::string &code_bytes = (std::string &)m_code->strValue();
|
||||
|
||||
plusaes::crypt_ctr(
|
||||
(unsigned char *)&code_bytes[desc->decrypt_begin_index],
|
||||
desc->decrypt_length,
|
||||
mod->pyarmor_aes_key,
|
||||
16,
|
||||
&nonce);
|
||||
|
||||
if (copy_prologue)
|
||||
{
|
||||
memcpy(
|
||||
&code_bytes[0],
|
||||
&code_bytes[desc->decrypt_length],
|
||||
desc->decrypt_begin_index);
|
||||
// Assume tail of code is not used there
|
||||
memset(
|
||||
&code_bytes[desc->decrypt_length],
|
||||
9, // NOP
|
||||
desc->decrypt_begin_index);
|
||||
}
|
||||
|
||||
// When running, the first 8 bytes are set to &PyCodeObject
|
||||
std::string new_str = "<COAddr>" + descriptor_str.substr(8);
|
||||
descriptor->setValue(new_str);
|
||||
}
|
||||
|
||||
PycRef<PycString> PycCode::getCellVar(PycModule* mod, int idx) const
|
||||
|
12
pyc_code.h
12
pyc_code.h
@@ -8,6 +8,16 @@
|
||||
class PycData;
|
||||
class PycModule;
|
||||
|
||||
struct PyarmorCoDescriptor
|
||||
{
|
||||
unsigned char flags;
|
||||
unsigned char short_nonce_index;
|
||||
unsigned char _;
|
||||
unsigned char decrypt_begin_index;
|
||||
unsigned int decrypt_length;
|
||||
unsigned int _enter_count;
|
||||
};
|
||||
|
||||
class PycCode : public PycObject {
|
||||
public:
|
||||
typedef std::vector<PycRef<PycString>> globals_t;
|
||||
@@ -45,6 +55,8 @@ public:
|
||||
|
||||
void load(PycData* stream, PycModule* mod) override;
|
||||
|
||||
void pyarmorDecryptCoCode(unsigned long consts_index, PycModule *mod);
|
||||
|
||||
int argCount() const { return m_argCount; }
|
||||
int posOnlyArgCount() const { return m_posOnlyArgCount; }
|
||||
int kwOnlyArgCount() const { return m_kwOnlyArgCount; }
|
||||
|
@@ -356,7 +356,7 @@ void PycModule::loadFromOneshotSequenceFile(const char *filename)
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
void pyarmorCoCodeAesNonceXorKeyCalculate(const char *in_buffer, unsigned int in_buffer_length, char *out_buffer)
|
||||
void pyarmorCoCodeAesNonceXorKeyCalculate(const char *in_buffer, unsigned int in_buffer_length, unsigned char *out_buffer)
|
||||
{
|
||||
unsigned char *cur = (unsigned char *)in_buffer + 16;
|
||||
unsigned char *end = (unsigned char *)in_buffer + in_buffer_length;
|
||||
|
12
pyc_module.h
12
pyc_module.h
@@ -81,6 +81,11 @@ public:
|
||||
|
||||
static bool isSupportedVersion(int major, int minor);
|
||||
|
||||
unsigned char pyarmor_aes_key[16];
|
||||
unsigned char pyarmor_mix_str_aes_nonce[12];
|
||||
bool pyarmor_co_code_aes_nonce_xor_enabled;
|
||||
unsigned char pyarmor_co_code_aes_nonce_xor_key[12];
|
||||
|
||||
private:
|
||||
void setVersion(unsigned int magic);
|
||||
|
||||
@@ -88,16 +93,11 @@ private:
|
||||
int m_maj, m_min;
|
||||
bool m_unicode;
|
||||
|
||||
char pyarmor_aes_key[16];
|
||||
char pyarmor_mix_str_aes_nonce[12];
|
||||
bool pyarmor_co_code_aes_nonce_xor_enabled;
|
||||
char pyarmor_co_code_aes_nonce_xor_key[12];
|
||||
|
||||
PycRef<PycCode> m_code;
|
||||
std::vector<PycRef<PycString>> m_interns;
|
||||
std::vector<PycRef<PycObject>> m_refs;
|
||||
};
|
||||
|
||||
void pyarmorCoCodeAesNonceXorKeyCalculate(const char *in_buffer, unsigned int in_buffer_length, char *out_buffer);
|
||||
void pyarmorCoCodeAesNonceXorKeyCalculate(const char *in_buffer, unsigned int in_buffer_length, unsigned char *out_buffer);
|
||||
|
||||
#endif
|
||||
|
Reference in New Issue
Block a user