Module refinery.units.malware.kramer

Expand source code Browse git
from __future__ import annotations

import re

from types import CodeType

from refinery.lib.py import disassemble_code, extract_code_from_buffer
from refinery.units import Unit


class _WrongKey(ValueError):
    pass


class kramer(Unit):
    """
    Deobfuscate Python samples obfuscated with Kramer.
    """
    @classmethod
    def handles(cls, data) -> bool | None:
        from refinery.lib.id import PycMagicPattern
        if PycMagicPattern.fullmatch(data[:4]):
            return True

    _LINEBREAK_MAGIC = 950

    def _crawl(self, code: CodeType, version, strings: list[str], secret: set[int], depth: int = 1):
        """
        Collects the string and integer arguments of all instructions in the given code object and
        in the code objects it contains, in the order of disassembly.
        """
        for instruction in disassemble_code(code, version):
            arg = instruction.argval
            if arg is None:
                continue
            if isinstance(arg, tuple):
                continue
            if isinstance(arg, str):
                strings.append(arg)
                continue
            if isinstance(arg, int):
                secret.add(arg)
                continue
            try:
                self._crawl(arg, version, strings, secret, depth + 1)
            except Exception as E:
                self.log_info(F'error crawling arg of type {type(arg).__name__} at depth {depth}: {E}')

    def process(self, data):
        strings: list[str] = []
        secret: set[int] = set()

        for code in extract_code_from_buffer(bytes(data)):
            self._crawl(code.container, code.version, strings, secret)

        kramer = max(strings, key=len, default='')

        if not kramer:
            raise ValueError('could not find the encoded string')

        separator = re.search('[^a-fA-F0-9]+', kramer)

        if not separator:
            raise ValueError('no separator detected; encoding method may have changed')

        def rotchar(c: int):
            if c in range(0x61, 0x7a) or c in range(0x30, 0x39):
                return c + 1
            if c == 0x7a:
                return 0x30
            if c == 0x39:
                return 0x61
            return c

        def decrypt(c: int, k: int):
            if c >= k:
                out = rotchar(c - k)
                if out not in range(0x100):
                    raise _WrongKey
                return out
            if c == self._LINEBREAK_MAGIC:
                return 0x0A
            raise _WrongKey

        def decrypt_with_key(key: int):
            decrypted = bytearray(decrypt(c, key) for c in encrypted)
            if not re.fullmatch(B'[\\s!-~]+', decrypted):
                raise _WrongKey
            return decrypted

        separator = separator.group(0)
        encrypted = [ord(bytes.fromhex(e).decode()) for e in kramer.split(separator)]

        ubound = min(x for x in encrypted if x != self._LINEBREAK_MAGIC)
        lbound = ubound - 0xFF

        secret = {k for k in secret if k > lbound and k < ubound}
        self.log_debug('potential secrets from code:', secret)

        for key in sorted(secret, reverse=True):
            try:
                return decrypt_with_key(key)
            except _WrongKey:
                pass

        self.log_info(F'all candidates failed, searching [{lbound}, {ubound}]')

        for key in range(ubound, lbound - 1, -1):
            try:
                self.log_debug('attempting key:', key)
                return decrypt_with_key(key)
            except _WrongKey:
                pass

        raise RuntimeError('could not find decryption key')

Classes

class kramer

Deobfuscate Python samples obfuscated with Kramer.

Expand source code Browse git
class kramer(Unit):
    """
    Deobfuscate Python samples obfuscated with Kramer.
    """
    @classmethod
    def handles(cls, data) -> bool | None:
        from refinery.lib.id import PycMagicPattern
        if PycMagicPattern.fullmatch(data[:4]):
            return True

    _LINEBREAK_MAGIC = 950

    def _crawl(self, code: CodeType, version, strings: list[str], secret: set[int], depth: int = 1):
        """
        Collects the string and integer arguments of all instructions in the given code object and
        in the code objects it contains, in the order of disassembly.
        """
        for instruction in disassemble_code(code, version):
            arg = instruction.argval
            if arg is None:
                continue
            if isinstance(arg, tuple):
                continue
            if isinstance(arg, str):
                strings.append(arg)
                continue
            if isinstance(arg, int):
                secret.add(arg)
                continue
            try:
                self._crawl(arg, version, strings, secret, depth + 1)
            except Exception as E:
                self.log_info(F'error crawling arg of type {type(arg).__name__} at depth {depth}: {E}')

    def process(self, data):
        strings: list[str] = []
        secret: set[int] = set()

        for code in extract_code_from_buffer(bytes(data)):
            self._crawl(code.container, code.version, strings, secret)

        kramer = max(strings, key=len, default='')

        if not kramer:
            raise ValueError('could not find the encoded string')

        separator = re.search('[^a-fA-F0-9]+', kramer)

        if not separator:
            raise ValueError('no separator detected; encoding method may have changed')

        def rotchar(c: int):
            if c in range(0x61, 0x7a) or c in range(0x30, 0x39):
                return c + 1
            if c == 0x7a:
                return 0x30
            if c == 0x39:
                return 0x61
            return c

        def decrypt(c: int, k: int):
            if c >= k:
                out = rotchar(c - k)
                if out not in range(0x100):
                    raise _WrongKey
                return out
            if c == self._LINEBREAK_MAGIC:
                return 0x0A
            raise _WrongKey

        def decrypt_with_key(key: int):
            decrypted = bytearray(decrypt(c, key) for c in encrypted)
            if not re.fullmatch(B'[\\s!-~]+', decrypted):
                raise _WrongKey
            return decrypted

        separator = separator.group(0)
        encrypted = [ord(bytes.fromhex(e).decode()) for e in kramer.split(separator)]

        ubound = min(x for x in encrypted if x != self._LINEBREAK_MAGIC)
        lbound = ubound - 0xFF

        secret = {k for k in secret if k > lbound and k < ubound}
        self.log_debug('potential secrets from code:', secret)

        for key in sorted(secret, reverse=True):
            try:
                return decrypt_with_key(key)
            except _WrongKey:
                pass

        self.log_info(F'all candidates failed, searching [{lbound}, {ubound}]')

        for key in range(ubound, lbound - 1, -1):
            try:
                self.log_debug('attempting key:', key)
                return decrypt_with_key(key)
            except _WrongKey:
                pass

        raise RuntimeError('could not find decryption key')

Ancestors

Subclasses

Class variables

var reverse

The type of the None singleton.

Inherited members