├── cape_parsers ├── malduck │ ├── __init__.py │ ├── test_malduck.py │ └── README.md ├── mwcp │ ├── __init__.py │ ├── README.md │ └── test_mwcp.py ├── CAPE │ ├── core │ │ ├── __init__.py │ │ ├── README.md │ │ ├── test_cape.py │ │ ├── GuLoader.py │ │ ├── Formbook.py │ │ ├── Socks5Systemz.py │ │ ├── BruteRatel.py │ │ ├── IcedIDLoader.py │ │ ├── Azorult.py │ │ ├── Strrat.py │ │ ├── DoppelPaymer.py │ │ ├── BitPaymer.py │ │ ├── AuraStealer.py │ │ ├── SquirrelWaffle.py │ │ ├── BlackDropper.py │ │ ├── AdaptixBeacon.py │ │ ├── IcedID.py │ │ ├── WarzoneRAT.py │ │ └── Quickbind.py │ ├── __init__.py │ └── community │ │ ├── __init__.py │ │ ├── README.md │ │ ├── QuasarRAT.py │ │ ├── AsyncRAT.py │ │ ├── DCRat.py │ │ ├── VenomRAT.py │ │ ├── XWorm.py │ │ ├── XenoRAT.py │ │ ├── MyKings.py │ │ ├── Fareit.py │ │ ├── SparkRAT.py │ │ ├── WinosStager.py │ │ ├── AuroraStealer.py │ │ ├── MonsterV2.py │ │ ├── Arkei.py │ │ ├── KoiLoader.py │ │ └── AgentTesla.py ├── utils │ ├── __init__.py │ ├── blzpack_lib.so │ ├── dotnet_utils.py │ ├── blzpack.py │ ├── strings.py │ └── lznt1.py ├── RATDecoders │ ├── __init__.py │ ├── README.md │ └── test_rats.py └── deprecated │ ├── Rozena.py │ ├── Punisher.py │ ├── BackOffLoader.py │ ├── BuerLoader.py │ ├── BackOffPOS.py │ ├── unrecom.py │ ├── Hancitor.py │ ├── BlackNix.py │ ├── Pandora.py │ ├── _VirusRat.py │ ├── ChChes.py │ ├── JavaDropper.py │ ├── REvil.py │ ├── Greame.py │ ├── Enfal.py │ ├── SmallNet.py │ ├── RedLeaf.py │ ├── _ShadowTech.py │ ├── EvilGrab.py │ ├── xRAT.py │ └── PoisonIvy.py ├── .gitignore ├── tests_parsers ├── readme.md ├── test_icedid.py ├── test_smokeloader.py ├── test_carbanak.py ├── test_bumblebee.py ├── test_koiloader.py ├── test_arkei.py ├── test_stealc.py ├── test_snake.py ├── test_aurorastealer.py ├── test_redline.py ├── test_oyster.py ├── test_blackdropper.py ├── test_mykings.py ├── test_sparkrat.py ├── test_rhadamanthys.py ├── test_asyncrat.py ├── test_quickbind.py ├── test_qakbot.py ├── test_agenttesla.py ├── test_latrodectus.py ├── test_zloader.py ├── test_winosstager.py ├── test_lumma.py ├── test_monsterv2.py ├── test_aurastealer.py ├── test_darkgate.py ├── test_pikabot.py ├── test_amadey.py ├── test_amatera.py ├── test_nitrogenbunyydownloader.py ├── test_adaptixbeacon.py ├── test_nanocore.py ├── test_njrat.py └── test_cobaltstrikebeacon.py ├── tests └── test_aplib.py ├── .github ├── workflows │ ├── pip-audit.yml │ ├── yara-audit.yml │ ├── python-package-uv.yml │ ├── reusable-setup.yml │ ├── export-requirements.yml │ ├── publish.yml │ └── python-package.yml ├── actions │ └── python-setup │ │ └── action.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── README.md ├── LICENSE └── pyproject.toml /cape_parsers/malduck/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cape_parsers/mwcp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/__init__.py: -------------------------------------------------------------------------------- 1 | # Init 2 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cape_parsers/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Init 2 | -------------------------------------------------------------------------------- /cape_parsers/RATDecoders/__init__.py: -------------------------------------------------------------------------------- 1 | # Init 2 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/README.md: -------------------------------------------------------------------------------- 1 | ### CAPE modules 2 | -------------------------------------------------------------------------------- /cape_parsers/mwcp/README.md: -------------------------------------------------------------------------------- 1 | ### Community modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */.DS_Store 3 | *.pyc 4 | dist/* 5 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/README.md: -------------------------------------------------------------------------------- 1 | ### Community modules 2 | -------------------------------------------------------------------------------- /cape_parsers/RATDecoders/README.md: -------------------------------------------------------------------------------- 1 | ### Community modules 2 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/test_cape.py: -------------------------------------------------------------------------------- 1 | def extract_config(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests_parsers/readme.md: -------------------------------------------------------------------------------- 1 | * Sample goes to: 2 | * https://github.com/CAPESandbox/CAPE-TestFiles/tree/main/malware 3 | -------------------------------------------------------------------------------- /cape_parsers/utils/blzpack_lib.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CAPESandbox/CAPE-parsers/HEAD/cape_parsers/utils/blzpack_lib.so -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/QuasarRAT.py: -------------------------------------------------------------------------------- 1 | from rat_king_parser.rkp import RATConfigParser 2 | 3 | 4 | def extract_config(data: bytes): 5 | return RATConfigParser(data=data).report.get("config", {}) 6 | -------------------------------------------------------------------------------- /cape_parsers/mwcp/test_mwcp.py: -------------------------------------------------------------------------------- 1 | from mwcp.parser import Parser 2 | 3 | 4 | class MWCP_TEST(Parser): 5 | DESCRIPTION = "Test module to ensure that framework loads properly." 6 | AUTHOR = "doomedraven" 7 | 8 | def run(self): 9 | pass 10 | -------------------------------------------------------------------------------- /cape_parsers/malduck/test_malduck.py: -------------------------------------------------------------------------------- 1 | from malduck.extractor import Extractor 2 | 3 | __author__ = "doomedraven" 4 | __version__ = "1.0.0" 5 | 6 | 7 | class TEST_MALDUCK(Extractor): 8 | """ 9 | TEST Configuration Extractor 10 | """ 11 | 12 | family = "TEST_MALDUCK" 13 | 14 | def TEST_MALDUCK(self): 15 | pass 16 | -------------------------------------------------------------------------------- /tests_parsers/test_icedid.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.core.IcedIDLoader import extract_config 2 | 3 | 4 | def test_icedid(): 5 | with open("tests/data/malware/7aaf80eb1436b946b2bd710ab57d2dcbaad2b1553d45602f2f3af6f2cfca5212", "rb") as data: 6 | conf = extract_config(data.read()) 7 | assert conf == {"CNCs": "http://anscowerbrut.com", "campaign": 2738000827} 8 | -------------------------------------------------------------------------------- /tests_parsers/test_smokeloader.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.core.SmokeLoader import extract_config 2 | 3 | 4 | def test_smokeloader(): 5 | with open("tests/data/malware/6929fff132c05ae7d348867f4ea77ba18f84fb8fae17d45dde3571c9e33f01f8", "rb") as data: 6 | conf = extract_config(data.read()) 7 | assert conf == {"CNCs": ["http://host-file-host6.com/", "http://host-host-file8.com/"]} 8 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/GuLoader.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | url_regex = re.compile(rb"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") 4 | 5 | 6 | def extract_config(data): 7 | try: 8 | urls = [url.lower().decode() for url in url_regex.findall(data)] 9 | if urls: 10 | return {"CNCs": urls} 11 | except Exception as e: 12 | print(e) 13 | 14 | return None 15 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Rozena.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def extract_config(data: bytes): 5 | patternIP_PORT = re.compile(rb"\x68(....)\x68..(..)\x89", re.DOTALL) 6 | config_dict = {} 7 | 8 | matches = patternIP_PORT.findall(data) 9 | if matches: 10 | ip = "".join(".".join(f"{c}" for c in matches[0][0])) 11 | port = int.from_bytes(matches[0][1], byteorder="big") 12 | config_dict["CNCs"] = f"{ip}:{port}" 13 | return {} 14 | -------------------------------------------------------------------------------- /tests/test_aplib.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.utils import aplib 6 | 7 | 8 | def test_apilib_decompress(): 9 | data = b"T\x00he quick\xecb\x0erown\xcef\xaex\x80jumps\xed\xe4veur`t?lazy\xead\xfeg\xc0\x00" 10 | assert aplib.decompress(data) == b"The quick brown fox jumps over the lazy dog" 11 | -------------------------------------------------------------------------------- /.github/workflows/pip-audit.yml: -------------------------------------------------------------------------------- 1 | name: PIP audit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * 1' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 20 11 | strategy: 12 | matrix: 13 | python-version: ["3.10"] 14 | 15 | steps: 16 | - name: Check out repository code 17 | uses: actions/checkout@v4 18 | 19 | - uses: pypa/gh-action-pip-audit@v1.0.8 20 | with: 21 | inputs: requirements.txt 22 | -------------------------------------------------------------------------------- /tests_parsers/test_carbanak.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.community.Carbanak import extract_config 2 | 3 | 4 | def test_carbanak(): 5 | with open("tests/data/malware/c9c1b06cb9c9bd6fc4451f5e2847a1f9524bb2870d7bb6f0ee09b9dd4e3e4c84", "rb") as data: 6 | assert extract_config(data.read()) == { 7 | "version": "1.7", 8 | "raw": {"Unknown 1": "60", "Unknown 2": "xoR9jtbNLlyJWw3dDRho7tqI8aRY5n"}, 9 | "CNCs": ["https://5.161.223.210:443", "https://207.174.30.226:443"], 10 | "campaign": "rabt11901b_x64", 11 | } 12 | -------------------------------------------------------------------------------- /tests_parsers/test_bumblebee.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.BumbleBee import extract_config 6 | 7 | 8 | def test_bumblebee(): 9 | with open("tests/data/malware/f8a6eddcec59934c42ea254cdd942fb62917b5898f71f0feeae6826ba4f3470d", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == {"botnet": "YTBSBbNTWU", "campaign": "1904r", "CNCs": ["444"], "raw": {"Data": "XNgHUGLrCD"}} 12 | -------------------------------------------------------------------------------- /tests_parsers/test_koiloader.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.KoiLoader import extract_config 6 | 7 | def test_koiloader(): 8 | with open("tests/data/malware/b462e3235c7578450b2b56a8aff875a3d99d22f6970a01db3ba98f7ecb6b01a0", "rb") as data: 9 | conf = extract_config(data.read()) 10 | assert conf == {"CNCs": ["http://91.202.233.209/hypermetropia.php", "https://admiralpub.ca/wp-content/uploads/2017"]} 11 | -------------------------------------------------------------------------------- /tests_parsers/test_arkei.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Arkei import extract_config 6 | 7 | 8 | def test_arkei(): 9 | with open("tests/data/malware/69ba4e2995d6b11bb319d7373d150560ea295c02773fe5aa9c729bfd2c334e1e", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": ["http://coin-file-file-19.com/tratata.php"], 13 | "botnet": "Default" 14 | } 15 | -------------------------------------------------------------------------------- /tests_parsers/test_stealc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Stealc import extract_config 6 | 7 | 8 | def test_stealc(): 9 | with open("tests/data/malware/619751f5ed0a9716318092998f2e4561f27f7f429fe6103406ecf16e33837470", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": ["http://95.217.125.57/2f571d994666c8cb.php"], 13 | "botnet": "5385386367" 14 | } 15 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/Formbook.py: -------------------------------------------------------------------------------- 1 | def extract_config(data): 2 | config_dict = {} 3 | i = 0 4 | try: 5 | lines = data.decode().split("\n") 6 | except Exception: 7 | return 8 | if lines[0].startswith("POST"): 9 | while lines[i] != "dat=": 10 | i += 1 11 | if lines[i] == "dat=": 12 | i += 1 13 | elif "www." not in lines[0]: 14 | return 15 | config_dict["CNCs"] = lines[i] 16 | decoys = [] 17 | i += 1 18 | while len(lines[i]) > 0: 19 | decoys.append(lines[i]) 20 | i += 1 21 | config_dict.setdefault("raw", {})["Decoys"] = decoys 22 | return config_dict 23 | -------------------------------------------------------------------------------- /tests_parsers/test_snake.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Snake import extract_config 6 | 7 | 8 | def test_snake(): 9 | with open("tests/data/malware/7b81c12fb7db9f0c317f36022ecac9faa45f5efefe24085c339c43db8b963ae2", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "raw": {"Type": "Telegram"}, 13 | "CNCs": ["https://api.telegram.org/bot7952998151:AAFh98iY7kaOlHAR0qftD3ZcqGbQm0TXbBY/sendMessage?chat_id=5692813672"], 14 | } 15 | -------------------------------------------------------------------------------- /tests_parsers/test_aurorastealer.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.community.AuroraStealer import extract_config 2 | 3 | def test_aurorastealer(): 4 | with open("tests/data/malware/8da8821d410b94a2811ce7ae80e901d7e150ad3420d677b158e45324a6606ac4", "rb") as data: 5 | conf = extract_config(data.read()) 6 | assert conf == { 7 | 'CNCs': ["tcp://77.91.85.73"], 8 | "build": "x64pump", 9 | "raw": { 10 | "MD5Hash": "f29f33b296b35ec5e7fc3ee784ef68ee", 11 | "Architecture": "X64", 12 | "BuildGroup": "x64pump", 13 | "BuildAccept": "0", 14 | "Date": "2023-04-06 19", 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests_parsers/test_redline.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file "docs/LICENSE" for copying permission. 4 | 5 | from cape_parsers.CAPE.core.RedLine import extract_config 6 | 7 | 8 | 9 | def test_redline(): 10 | with open("tests/data/malware/000608d875638ba7d6c467ece976c1496e6a6ec8ce3e7f79e0fd195ae3045078", "rb") as data: 11 | conf = extract_config(data.read()) 12 | assert conf == { 13 | "raw": {"Authorization": "9059ea331e4599de3746df73ccb24514"}, 14 | "CNCs": "77.91.68.68:19071", 15 | "botnet": "krast", 16 | "cryptokey": "Formative", 17 | } 18 | -------------------------------------------------------------------------------- /cape_parsers/RATDecoders/test_rats.py: -------------------------------------------------------------------------------- 1 | # https://youtu.be/C_ijc7A5oAc?list=OLAK5uy_kGTSX7lmPmKwIVzgFLqd0x3dSF6HQhE-I 2 | from contextlib import suppress 3 | 4 | HAVE_MLW_CONFIGS = False 5 | with suppress(ImportError): 6 | # We do not install this by default as is outdated now, but if installed will be imported 7 | from malwareconfig.common import Decoder 8 | 9 | HAVE_MLW_CONFIGS = True 10 | 11 | 12 | # ToDo add xfail if not HAVE_MLW_CONFIGS 13 | class TEST_RATS(Decoder): 14 | decoder_name = "TestRats" 15 | decoder__version = 1 16 | decoder_author = "doomedraven" 17 | decoder_description = "Test module to ensure that framework loads properly." 18 | 19 | def __init__(self): 20 | pass 21 | -------------------------------------------------------------------------------- /tests_parsers/test_oyster.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.Oyster import extract_config 6 | 7 | 8 | def test_oyster(): 9 | with open("tests/data/malware/8bae0fa9f589cd434a689eebd7a1fde949cc09e6a65e1b56bb620998246a1650", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": ["https://connectivity-check.linkpc.net/"], 13 | "version": "v1.0 #ads 2", 14 | "raw": {"Strings": ["api/connect", "Content-Type: application/json", "api/session"]}, 15 | } 16 | -------------------------------------------------------------------------------- /tests_parsers/test_blackdropper.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.BlackDropper import extract_config 6 | 7 | def test_blackdropper(): 8 | with open("tests/data/malware/f8026ae3237bdd885e5fcaceb86bcab4087d8857e50ba472ca79ce44c12bc257", "rb") as data: 9 | conf = extract_config(data.read()) 10 | assert conf == { 11 | "CNCs": ["http://72.5.42.222:8568/api/dll/", "http://72.5.42.222:8568/api/fileZip"], 12 | "campaign": "oFwQ0aQ3v", 13 | "raw": {"directories": ["\\Music\\dkcydqtwjv"]} 14 | } 15 | -------------------------------------------------------------------------------- /tests_parsers/test_mykings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from cape_parsers.CAPE.community.MyKings import extract_config 3 | 4 | @pytest.mark.xfail(reason="todo") 5 | def test_mykings(): 6 | with open( 7 | "tests/data/malware/9f51e2a881a5d53799d7aecb7afd7d220ae012040b44b212678755085888b8fb", 8 | "rb", 9 | ) as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "raw": { 13 | "CNCs": [ 14 | "46.28.71.32", 15 | "108.174.197.104:777", 16 | "cmd1.cmd-230812.ru:9999", 17 | "https://pastebin.com/raw/vz9pet6K", 18 | "250922.duckdns.org" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests_parsers/test_sparkrat.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.community.SparkRAT import extract_config 2 | 3 | 4 | def test_sparkrat(): 5 | with open("tests/data/malware/ec349cfacc7658eed3640f1c475eb958c5f05bae7c2ed74d4cdb7493176daeba", "rb") as data: 6 | conf = extract_config(data.read()) 7 | assert conf == { 8 | "raw": { 9 | "secure": False, 10 | "host": "67.217.62.106", 11 | "port": 4443, 12 | "path": "/", 13 | "uuid": "8dc7e7d8f8576f3e55a00850b72887db", 14 | "key": "a1348fb8969ad7a9f85ac173c2027622135e52e0e6d94d10e6a81916a29648ac", 15 | }, 16 | "CNCs": ["http://67.217.62.106:4443/"], 17 | } 18 | -------------------------------------------------------------------------------- /tests_parsers/test_rhadamanthys.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.Rhadamanthys import extract_config 6 | 7 | 8 | def test_rhadamanthys(): 9 | with open("tests/data/malware/aec7e18e752d06b62ecf48a392dacb9e0ca476ade84f01c1f5114536e22207f8", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": [ 13 | "https://185.198.234.232/apichk/bief8u31.ao3gp", 14 | "https://104.164.55.233/apichk/bief8u31.ao3gp", 15 | "https://103.245.231.203/apichk/bief8u31.ao3gp" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests_parsers/test_asyncrat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from cape_parsers.CAPE.community.AsyncRAT import extract_config 3 | 4 | @pytest.mark.skip(reason="ToDo fix it") 5 | def test_asyncrat(): 6 | with open("tests/data/malware/f08b325f5322a698e14f97db29d322e9ee91ad636ac688af352d51057fc56526", "rb") as data: 7 | conf = extract_config(data.read()) 8 | assert conf == { 9 | "raw": { 10 | "CNCs": ["todfg.duckdns.org"], 11 | "Ports": "6745", 12 | "Version": "0.5.7B", 13 | "Folder": "%AppData%", 14 | "Filename": "updateee.exe", 15 | "Install": "false", 16 | "Mutex": "AsyncMutex_6SI8OkPnk", 17 | "Pastebin": "null", 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests_parsers/test_quickbind.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.Quickbind import extract_config 6 | 7 | 8 | def test_quickbind(): 9 | with open("tests/data/malware/bfcb215f86fc4f8b4829f6ddd5acb118e80fb5bd977453fc7e8ef10a52fc83b7", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "user_agent": "Mozilla / 4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;.NET CLR 1.0.3705)", 13 | "cryptokey": "24de21a8dc08434c", 14 | "cryptokey_type": "RC4", 15 | "CNCs": ["http://185.49.69.41"], 16 | "mutex": "15432a4d-34ca-4d0d-a4ac-04df9a373862", 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CAPE-parsers 2 | CAPE core and community parsers 3 | 4 | [![PyPI version](https://img.shields.io/pypi/v/CAPE-parsers)](https://pypi.org/project/CAPE-parsers/) 5 | 6 | ### Configs structure 7 | ``` 8 | CNCs: [] 9 | campaign: str 10 | botnet: str 11 | dga_seed: hex str 12 | version: str 13 | mutex: str 14 | user_agent: str 15 | build: str 16 | cryptokey: str 17 | cryptokey_type: str (algorithm). Ex: RC4, RSA public key. salsa20, (x)chacha20 18 | raw: {any other data goes here} 19 | ``` 20 | * All CNC entries should be in URL format. aka `://:/` 21 | * Schema examples: `tcp://`, `ftp://`, `udp://`, `http(s)`, etc. 22 | * Old CAPE configs still have lack of this structures as most of them are dead families. 23 | * This CNC simplification make it easier to parse with tools like `tldextract` or `urlparse` 24 | -------------------------------------------------------------------------------- /tests_parsers/test_qakbot.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cape_parsers.CAPE.core.QakBot import extract_config 4 | 5 | 6 | @pytest.mark.skip(reason="Missed file") 7 | def test_qakbot(): 8 | with open("tests/data/malware/59559e97962e40a15adb2237c4d01cfead03623aff1725616caeaa5a8d273a35", "rb") as data: 9 | conf = extract_config(data.read()) 10 | assert conf == { 11 | "raw": { 12 | "C2s": ["62.204.41.234:2222", "77.105.162.176:995", "31.210.173.10:443", "5.252.177.195:443"], 13 | "Exe timestamp": "18:14:52 20-03-2024", 14 | }, 15 | "CNCs": [ 16 | "http://62.204.41.234:2222", 17 | "http://77.105.162.176:995", 18 | "http://31.210.173.10:443", 19 | "http://5.252.177.195:443", 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/Socks5Systemz.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from contextlib import suppress 3 | 4 | 5 | def _is_ip(ip): 6 | try: 7 | socket.inet_aton(ip) 8 | return True 9 | except Exception: 10 | return False 11 | 12 | 13 | def extract_config(data): 14 | config_dict = {} 15 | with suppress(Exception): 16 | if data[:2] == b"MZ": 17 | return 18 | for line in data.decode().split("\n"): 19 | if _is_ip(line) and line not in config_dict.get("CNCs", []): 20 | config_dict["CNCs"].append(line) 21 | elif line and "\\" in line: 22 | config_dict.setdefault("Timestamp path", []).append(line) 23 | elif "." in line and "=" not in line and line not in config_dict["CNCs"]: 24 | config_dict.setdefault("raw", {}).setdefault("Dummy domain", []).append(line) 25 | return config_dict 26 | -------------------------------------------------------------------------------- /tests_parsers/test_agenttesla.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.community.AgentTesla import extract_config 2 | 3 | 4 | def test_agenttesla(): 5 | # AgentTeslaV5 6 | with open("tests/data/malware/893f4dc8f8a1dcee05a0840988cf90bc93c1cda5b414f35a6adb5e9f40678ce9", "rb") as data: 7 | conf = extract_config(data.read()) 8 | assert conf == { 9 | "raw": { 10 | "Protocol": "SMTP", 11 | "CNCs": ["mail.guestequipment.com.au"], 12 | "Username": "sendlog@guestequipment.com.au", 13 | "Password": "Clone89!", 14 | "EmailTo": "info@marethon.com", 15 | "Persistence_Filename": "newfile.exe", 16 | "ExternalIPCheckServices": ["http://ip-api.com/line/?fields=hosting"], 17 | }, 18 | "CNCs": ["smtp://sendlog@guestequipment.com.au:Clone89!@mail.guestequipment.com.au:587"], 19 | } 20 | -------------------------------------------------------------------------------- /tests_parsers/test_latrodectus.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.Latrodectus import extract_config 6 | 7 | 8 | def test_latrodectus(): 9 | with open("tests/data/malware/719e19ead52a80b15bf887f3b9a6ab6d50c15d026766db41302c5e4b12949295", "rb") as data: 10 | conf = extract_config(data.read()) 11 | del conf["raw"]["Strings"] 12 | assert conf == { 13 | "CNCs": ["https://piloferstaf.com/test/", "https://ypredoninen.com/test/"], 14 | "campaign": 2386938644, 15 | "version": "1.8.4", 16 | "cryptokey": "XTpawuOlVTpNs6JsxElGCO0gbRa6Gkw7oEmotQWSfM9Qu3j1GYCDs2JETmfatWCI", 17 | "cryptokey_type": "RC4", 18 | "raw": { 19 | "Group name": "Sigma" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/yara-audit.yml: -------------------------------------------------------------------------------- 1 | name: YARA tests 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * 1' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 20 11 | strategy: 12 | matrix: 13 | python-version: ["3.10"] 14 | 15 | steps: 16 | - name: Check out repository code 17 | uses: actions/checkout@v4 18 | 19 | - name: Checkout test files repo 20 | uses: actions/checkout@v4 21 | with: 22 | repository: CAPESandbox/CAPE-TestFiles 23 | path: tests/data/ 24 | 25 | - uses: ./.github/actions/python-setup/ 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | sudo apt install yara 32 | poetry run pip install yara-python 33 | 34 | - name: Run unit tests 35 | run: poetry run pytest tests/test_yara.py -s --import-mode=append 36 | -------------------------------------------------------------------------------- /tests_parsers/test_zloader.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.Zloader import extract_config 6 | 7 | 8 | def test_zloader(): 9 | with open("tests/data/malware/adbd0c7096a7373be82dd03df1aae61cb39e0a155c00bbb9c67abc01d48718aa", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "botnet": "Bing_Mod5", 13 | "campaign": "M1", 14 | "CNCs": ["https://dem.businessdeep.com"], 15 | "cryptokey": "-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKGAOWVkikqE7TyKIMtWI8dFsaleTaJNXMJNIPnRE/fGCzqrV+rtY3+ex4MCHEtq2Vwppthf0Rglv8OiWgKlerIN5P6NEyCfIsFYUMDfldQTF03VES8GBIvHq5SjlIz7lawuwfdjdEkaHfOmmu9srraftkI9gZO8WRQgY1uNdsXwIDAQAB-----END PUBLIC KEY-----", 16 | "cryptokey_type": "RSA Public key" 17 | } 18 | -------------------------------------------------------------------------------- /tests_parsers/test_winosstager.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.community.WinosStager import extract_config 2 | 3 | 4 | def test_winosstager(): 5 | with open( 6 | "tests/data/malware/ed8a86bb6d3c3d907984062e3bd3d0962aa1cb481f6aaf2e36ce084f92696f2c", 7 | "rb", 8 | ) as data: 9 | conf = extract_config(data.read()) 10 | assert conf == { 11 | "CNCs": ["tcp://150.5.145.84:443"], 12 | "campaign": "default", 13 | "version": "1.0", 14 | "raw": { 15 | "execution_delay_seconds": "1", 16 | "communication_interval_seconds": "1", 17 | "version": "1.0", 18 | "comment": "", 19 | "keylogger": "1", 20 | "end_bluescreen": "0", 21 | "anti_traffic_monitoring": "0", 22 | "entrypoint": "0", 23 | "process_daemon": "1", 24 | "process_hollowing": "0", 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /.github/actions/python-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Python setup steps that can be reused' 2 | description: 'Install dependencies, poetry, requirements' 3 | inputs: 4 | python-version: 5 | required: true 6 | description: The python version 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Install dependencies 12 | if: ${{ runner.os == 'Linux' }} 13 | shell: bash 14 | run: | 15 | sudo apt-get update && sudo apt-get install -y --no-install-recommends python3-dev 16 | 17 | - name: Install poetry 18 | shell: bash 19 | run: | 20 | PIP_BREAK_SYSTEM_PACKAGES=1 pip install poetry poetry-dynamic-versioning --user 21 | 22 | - name: Set up Python ${{ inputs.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ inputs.python-version }} 26 | cache: 'poetry' 27 | 28 | - name: Install requirements 29 | shell: bash 30 | run: | 31 | PIP_BREAK_SYSTEM_PACKAGES=1 poetry install --no-interaction --no-root 32 | -------------------------------------------------------------------------------- /tests_parsers/test_lumma.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Lumma import extract_config 6 | 7 | 8 | def test_lumma(): 9 | with open("tests/data/malware/ede02b81615e9011835b26039b5963db0eb9c4569e5535da58c6aefa7c1b7217", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": [ 13 | "https://roriwfq.xyz/auyw", 14 | "https://narrathfpt.top/tekq", 15 | "https://escczlv.top/bufi", 16 | "https://localixbiw.top/zlpa", 17 | "https://korxddl.top/qidz", 18 | "https://stochalyqp.xyz/alfp", 19 | "https://diecam.top/laur", 20 | "https://citellcagt.top/gjtu", 21 | "https://saokwe.xyz/plxa", 22 | ], 23 | "build": "490cef3c0ae4b5f900506d5988954245474b4975ef" 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/python-package-uv.yml: -------------------------------------------------------------------------------- 1 | name: Main CI Pipeline 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | # Job that calls the reusable workflow 10 | setup: 11 | name: "Setup Environment" 12 | # Note the indentation and the './' prefix 13 | uses: ./.github/workflows/reusable-setup.yml 14 | with: 15 | python-version: '3.12' 16 | 17 | # Job that runs after the setup is complete 18 | test: 19 | name: "Run Tests" 20 | needs: setup # Depends on the setup job 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: '3.12' 27 | - name: Install uv 28 | run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.cargo/bin" >> $GITHUB_PATH 29 | - name: Restore cached venv 30 | uses: actions/cache/restore@v4 31 | with: 32 | path: .venv 33 | key: ${{ runner.os }}-python-3.12-uv-${{ hashFiles('poetry.lock') }} 34 | - name: Run tests 35 | run: uvx pytest 36 | -------------------------------------------------------------------------------- /tests_parsers/test_monsterv2.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from cape_parsers.CAPE.community.MonsterV2 import extract_config 3 | 4 | @pytest.mark.xfail(reason="todo") 5 | def test_monsterv2(): 6 | with open( 7 | "tests/data/malware/b842281e64924baa7c011501d7778075da412d66986e6aa65fd7d171cf074d70", 8 | "rb", 9 | ) as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "raw": { 13 | "anti_dbg": False, 14 | "anti_sandbox": False, 15 | "aurotun": False, 16 | "build_name": "Se2", 17 | "disable_mutex": False, 18 | "ip": "162.33.177.183", 19 | "kx_pk": "gUpjAMXma3Kiq8lZx/2UY1kcKTlsy5CMf+/y2IuUg1A=", 20 | "port": 7712, 21 | "priviledge_escalation": True, 22 | "seal_pk": "J2urRTT6yFi+WosGLdpP8bSyxwOPg9PTYYDVEpCZhw0=", 23 | "sign_pk": "XkEb9v3B99RHP+k4LgFOvrq+WQPKPKHpIx4VKvw6IQc=", 24 | }, 25 | "CNCs": ["tcp://162.33.177.183:7712"], 26 | "build": "Se2", 27 | } 28 | -------------------------------------------------------------------------------- /tests_parsers/test_aurastealer.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.AuraStealer import extract_config 6 | 7 | 8 | def test_aurastealer(): 9 | with open("tests/data/malware/a9c47f10d5eb77d7d6b356be00b4814a7c1e5bb75739b464beb6ea03fc36cc85", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | 'CNCs': ['https://armydevice.shop', 'https://glossmagazine.shop'], 13 | 'user_agent': [''], 14 | 'version': '1.0.0', 15 | 'build': '9f594914-9bc5-422b-b4d7-8733894b0b5c', 16 | 'cryptokey': '02220b5fef521c38dbf3e59c36b522e462a8cd36046bd01c9082e6322eacd1d1', 17 | 'cryptokey_type': 'AES', 18 | 'raw': { 19 | 'iv': '816ff36da9d9627592f2618045149d22', 20 | 'anti_vm': True, 21 | 'anti_dbg': True, 22 | 'self_del': True, 23 | 'run_delay': 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CAPE Sandbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/AsyncRAT.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import sys 3 | import os 4 | 5 | from rat_king_parser.rkp import RATConfigParser 6 | 7 | HAVE_ASYNCRAT_COMMON = False 8 | module_file_path = "/opt/CAPEv2/data/asyncrat_common.py" 9 | if os.path.exists(module_file_path): 10 | try: 11 | module_name = os.path.basename(module_file_path).replace(".py", "") 12 | spec = importlib.util.spec_from_file_location(module_name, module_file_path) 13 | asyncrat_common = importlib.util.module_from_spec(spec) 14 | sys.modules[module_name] = asyncrat_common 15 | spec.loader.exec_module(asyncrat_common) 16 | HAVE_ASYNCRAT_COMMON = True 17 | except Exception as e: 18 | print("Error loading asyncrat_common.py", e) 19 | 20 | 21 | def extract_config(data: bytes): 22 | config = RATConfigParser(data=data, remap_config=True).report.get("config", {}) 23 | if config and HAVE_ASYNCRAT_COMMON: 24 | config = asyncrat_common.convert_config(config) 25 | 26 | return config 27 | 28 | 29 | if __name__ == "__main__": 30 | data = open(sys.argv[1], "rb").read() 31 | print(extract_config(data)) 32 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/DCRat.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import sys 3 | import os 4 | 5 | from rat_king_parser.rkp import RATConfigParser 6 | 7 | HAVE_ASYNCRAT_COMMON = False 8 | module_file_path = "/opt/CAPEv2/data/asyncrat_common.py" 9 | if os.path.exists(module_file_path): 10 | try: 11 | module_name = os.path.basename(module_file_path).replace(".py", "") 12 | spec = importlib.util.spec_from_file_location(module_name, module_file_path) 13 | asyncrat_common = importlib.util.module_from_spec(spec) 14 | sys.modules[module_name] = asyncrat_common 15 | spec.loader.exec_module(asyncrat_common) 16 | HAVE_ASYNCRAT_COMMON = True 17 | except Exception as e: 18 | print("Error loading asyncrat_common.py", e) 19 | 20 | 21 | def extract_config(data: bytes): 22 | config = RATConfigParser(data=data, remap_config=True).report.get("config", {}) 23 | if config and HAVE_ASYNCRAT_COMMON: 24 | config = asyncrat_common.convert_config(config) 25 | 26 | return config 27 | 28 | 29 | if __name__ == "__main__": 30 | data = open(sys.argv[1], "rb").read() 31 | print(extract_config(data)) 32 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/VenomRAT.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import sys 3 | import os 4 | 5 | from rat_king_parser.rkp import RATConfigParser 6 | 7 | HAVE_ASYNCRAT_COMMON = False 8 | module_file_path = "/opt/CAPEv2/data/asyncrat_common.py" 9 | if os.path.exists(module_file_path): 10 | try: 11 | module_name = os.path.basename(module_file_path).replace(".py", "") 12 | spec = importlib.util.spec_from_file_location(module_name, module_file_path) 13 | asyncrat_common = importlib.util.module_from_spec(spec) 14 | sys.modules[module_name] = asyncrat_common 15 | spec.loader.exec_module(asyncrat_common) 16 | HAVE_ASYNCRAT_COMMON = True 17 | except Exception as e: 18 | print("Error loading asyncrat_common.py", e) 19 | 20 | 21 | def extract_config(data: bytes): 22 | config = RATConfigParser(data=data, remap_config=True).report.get("config", {}) 23 | if config and HAVE_ASYNCRAT_COMMON: 24 | config = asyncrat_common.convert_config(config) 25 | 26 | return config 27 | 28 | 29 | if __name__ == "__main__": 30 | data = open(sys.argv[1], "rb").read() 31 | print(extract_config(data)) 32 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/XWorm.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import sys 3 | import os 4 | 5 | from rat_king_parser.rkp import RATConfigParser 6 | 7 | HAVE_ASYNCRAT_COMMON = False 8 | module_file_path = "/opt/CAPEv2/data/asyncrat_common.py" 9 | if os.path.exists(module_file_path): 10 | try: 11 | module_name = os.path.basename(module_file_path).replace(".py", "") 12 | spec = importlib.util.spec_from_file_location(module_name, module_file_path) 13 | asyncrat_common = importlib.util.module_from_spec(spec) 14 | sys.modules[module_name] = asyncrat_common 15 | spec.loader.exec_module(asyncrat_common) 16 | HAVE_ASYNCRAT_COMMON = True 17 | except Exception as e: 18 | print("Error loading asyncrat_common.py", e) 19 | 20 | 21 | def extract_config(data: bytes): 22 | config = RATConfigParser(data=data, remap_config=True).report.get("config", {}) 23 | if config and HAVE_ASYNCRAT_COMMON: 24 | config = asyncrat_common.convert_config(config) 25 | 26 | return config 27 | 28 | 29 | if __name__ == "__main__": 30 | data = open(sys.argv[1], "rb").read() 31 | print(extract_config(data)) 32 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/XenoRAT.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import sys 3 | import os 4 | 5 | from rat_king_parser.rkp import RATConfigParser 6 | 7 | HAVE_ASYNCRAT_COMMON = False 8 | module_file_path = "/opt/CAPEv2/data/asyncrat_common.py" 9 | if os.path.exists(module_file_path): 10 | try: 11 | module_name = os.path.basename(module_file_path).replace(".py", "") 12 | spec = importlib.util.spec_from_file_location(module_name, module_file_path) 13 | asyncrat_common = importlib.util.module_from_spec(spec) 14 | sys.modules[module_name] = asyncrat_common 15 | spec.loader.exec_module(asyncrat_common) 16 | HAVE_ASYNCRAT_COMMON = True 17 | except Exception as e: 18 | print("Error loading asyncrat_common.py", e) 19 | 20 | 21 | def extract_config(data: bytes): 22 | config = RATConfigParser(data=data, remap_config=True).report.get("config", {}) 23 | if config and HAVE_ASYNCRAT_COMMON: 24 | config = asyncrat_common.convert_config(config) 25 | 26 | return config 27 | 28 | 29 | if __name__ == "__main__": 30 | data = open(sys.argv[1], "rb").read() 31 | print(extract_config(data)) 32 | -------------------------------------------------------------------------------- /.github/workflows/reusable-setup.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Python and UV Setup 2 | 3 | # CRITICAL: This makes the workflow "reusable" 4 | on: 5 | workflow_call: 6 | inputs: 7 | python-version: 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | # The single job inside the reusable workflow 13 | setup-and-cache: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ inputs.python-version }} 20 | - name: Install uv 21 | run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.cargo/bin" >> $GITHUB_PATH 22 | - uses: actions/cache@v4 23 | id: cache-uv-venv 24 | with: 25 | path: .venv 26 | key: ${{ runner.os }}-python-${{ inputs.python-version }}-uv-${{ hashFiles('poetry.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-python-${{ inputs.python-version }}-uv- 29 | - if: steps.cache-uv-venv.outputs.cache-hit != 'true' 30 | run: | 31 | uv venv --python ${{ inputs.python-version }} 32 | uv pip sync --all-extras pyproject.toml 33 | -------------------------------------------------------------------------------- /cape_parsers/malduck/README.md: -------------------------------------------------------------------------------- 1 | :duck: Malduck 2 | ========= 3 | 4 | Community 5 | 6 | Malduck is your ducky companion in malware analysis journeys. It is mostly based on [Roach](https://github.com/hatching/roach) project, which derives many concepts from [mlib](https://github.com/mak/mlib) 7 | library created by [Maciej Kotowicz](https://lokalhost.pl). The purpose of fork was to make Roach independent from [Cuckoo Sandbox](https://cuckoosandbox.org/) project, but still supporting its internal `procmem` format. 8 | 9 | Malduck provides many improvements resulting from CERT.pl codebase, making scripts written for malware analysis purposes much shorter and more powerful. 10 | 11 | Improvements 12 | ============ 13 | 14 | * Support for (non)memory-mapped PE images without header fix-up. 15 | * Searching for wildcarded byte sequences 16 | * Support for x64 disassembly 17 | * Fixed-precision integer types 18 | * Many improvements in ProcessMemory 19 | 20 | Usage 21 | ========== 22 | 23 | Installing may be performed by running 24 | 25 | ``` 26 | pip install malduck 27 | ``` 28 | 29 | Usage documentation can be found [on readthedocs](https://malduck.readthedocs.io/en/latest/). 30 | 31 | ![Co-financed by the Connecting Europe Facility by of the European Union](https://www.cert.pl/wp-content/uploads/2019/02/en_horizontal_cef_logo-1.png) 32 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/BruteRatel.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | 3 | 4 | def extract_config(data): 5 | config = {} 6 | 7 | with suppress(Exception): 8 | i = 0 9 | lines = data.decode().split("\n") 10 | for line in lines: 11 | if line.startswith("Mozilla"): 12 | cncs = list(set(lines[i - 2].split(","))) 13 | port = lines[i - 1] 14 | uris = lines[i + 3].split(",") 15 | keys = [lines[i + 1], lines[i + 2]] 16 | 17 | for cnc in cncs: 18 | # ToDo need to verify if we have schema and uri has slash 19 | for uri in uris: 20 | config.setdefault("CNCs", []).append(f"{cnc}:{port}{uri}") 21 | 22 | config["raw"] = { 23 | "User Agent": line, 24 | "C2": cncs, 25 | "Port": port, 26 | "URI": uri, 27 | # ToDo move to proper field 28 | "Keys": keys, 29 | } 30 | break 31 | i += 1 32 | 33 | return config 34 | 35 | 36 | if __name__ == "__main__": 37 | import sys 38 | from pathlib import Path 39 | 40 | filedata = Path(sys.argv[1]).read_bytes() 41 | print(extract_config(filedata)) 42 | -------------------------------------------------------------------------------- /tests_parsers/test_darkgate.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.core.DarkGate import extract_config 2 | 3 | 4 | def test_darkgate(): 5 | with open("tests/data/malware/1c3ae64795b61034080be00601b947819fe071efd69d7fc791a99ec666c2043d", "rb") as data: 6 | assert extract_config(data.read()) == { 7 | "CNCs": ["http://80.66.88.145:2842"], 8 | "raw": { 9 | "c2_port": "2842", 10 | "startup_persistence": "Yes", 11 | "rootkit": "Yes", 12 | "anti_vm": "No", 13 | "check_disk": "No", 14 | "min_disk": "35", 15 | "anti_analysis": "No", 16 | "check_ram": "No", 17 | "min_ram": "4096", 18 | "check_xeon": "No", 19 | "internal_mutex": "aFcade", 20 | "crypter_rawstub": "No", 21 | "crypter_dll": "No", 22 | "crypter_au3": "Yes", 23 | "unknown_14": "4", 24 | "crypto_key": "SygEDGfHvmMftg", 25 | "c2_ping_interval": "4", 26 | "anti_debug": "No", 27 | "unknown_18": "Yes", 28 | "BSOD_protect": "Yes", 29 | "unknown_20": "Yes", 30 | }, 31 | "mutex": "aFcade", 32 | "cryptokey": "SygEDGfHvmMftg", 33 | } 34 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Punisher.py: -------------------------------------------------------------------------------- 1 | def extract_config(data): 2 | config_parts = data.split("abccba") 3 | if len(config_parts) <= 5: 4 | return None 5 | config_dict = { 6 | "Domain": config_parts[1], 7 | "Port": config_parts[2], 8 | "Campaign Name": config_parts[3], 9 | "Copy StartUp": config_parts[4], 10 | "Unknown": config_parts[5], 11 | "Add To Registry": config_parts[6], 12 | "Registry Key": config_parts[7], 13 | "Password": config_parts[8], 14 | "Anti Kill Process": config_parts[9], 15 | "USB Spread": config_parts[10], 16 | "Anti VMWare VirtualBox": config_parts[11], 17 | "Kill Sandboxie": config_parts[12], 18 | "Kill WireShark / Apate DNS": config_parts[13], 19 | "Kill NO-IP": config_parts[14], 20 | "Block Virus Total": config_parts[15], 21 | "Install Name": config_parts[16], 22 | "ByPass Malware Bytes": config_parts[20], 23 | "Kill SpyTheSPy": config_parts[21], 24 | "Connection Delay": config_parts[22], 25 | "Copy To All Drives": config_parts[23], 26 | "HideProcess": config_parts[24], 27 | } 28 | 29 | if config_parts[17] == "True": 30 | config_dict["Install Path"] = "App Data" 31 | if config_parts[18] == "True": 32 | config_dict["Install Path"] = "TEMP" 33 | if config_parts[19] == "True": 34 | config_dict["Install Path"] = "Documents" 35 | if config_dict: 36 | return {"raw": config_dict} 37 | -------------------------------------------------------------------------------- /tests_parsers/test_pikabot.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.core.PikaBot import extract_config 6 | 7 | 8 | def test_pikabot(): 9 | with open("tests/data/malware/7600d0efc92ecef06320a1a6ffd85cd90d3d98470a381b03202e81d93bcdd03c", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": [ 13 | "http://154.53.55.165:13783", 14 | "http://158.247.240.58:5632", 15 | "http://70.34.223.164:5000", 16 | "http://70.34.199.64:9785", 17 | "http://45.77.63.237:5632", 18 | "http://198.38.94.213:2224", 19 | "http://94.72.104.80:5000", 20 | "http://84.46.240.42:2083", 21 | "http://154.12.236.248:13786", 22 | "http://94.72.104.77:13724", 23 | "http://209.126.86.48:1194", 24 | ], 25 | "version": "1.8.32-beta", 26 | "user_agent": "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; A7F; BRI/2; Tablet PC 2.0; wbx 1.0.0; Microsoft Outlook 14.0.7233; ms-office;", 27 | "campaign": "GG24_T@T@f0adda360d2b4ccda11468e026526576", 28 | "raw": {"Registry Key": "MWnkl"} 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/export-requirements.yml: -------------------------------------------------------------------------------- 1 | name: Update requirements.txt file 2 | 3 | on: 4 | push: 5 | branches: [ main, staging ] 6 | paths: 7 | - "pyproject.toml" 8 | - "poetry.lock" 9 | 10 | jobs: 11 | update: 12 | if: ${{ !github.event.act }} # skip during local actions testing 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | strategy: 16 | matrix: 17 | python-version: ["3.10"] 18 | 19 | steps: 20 | - name: Check out repository code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install poetry 24 | run: pip install poetry poetry-plugin-export --user 25 | 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v5 28 | with: 29 | # check-latest: true 30 | python-version: ${{ matrix.python-version }} 31 | cache: 'poetry' 32 | 33 | - name: Export requirements.txt 34 | run: poetry export --format requirements.txt --output requirements.txt 35 | 36 | - name: Commit changes if any 37 | # Skip this step if being run by nektos/act 38 | if: ${{ !env.ACT }} 39 | run: | 40 | git config user.name "GitHub Actions" 41 | git config user.email "action@github.com" 42 | if output=$(git status --porcelain) && [ ! -z "$output" ]; then 43 | git pull -f 44 | git add requirements.txt 45 | git commit -m "ci: Update requirements.txt" -a 46 | git push 47 | fi 48 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/BackOffLoader.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | from hashlib import md5 3 | from struct import unpack_from 4 | from sys import argv 5 | 6 | import pefile 7 | from Cryptodome.Cipher import ARC4 8 | 9 | CFG_START = "1020304050607080" 10 | 11 | 12 | def RC4(key, data): 13 | cipher = ARC4.new(key) 14 | return cipher.decrypt(data) 15 | 16 | 17 | def extract_config(data): 18 | config_data = {} 19 | pe = pefile.PE(data=data, fast_load=True) 20 | for section in pe.sections: 21 | if b".data" in section.Name: 22 | data = section.get_data() 23 | if CFG_START != hexlify(unpack_from(">8s", data, offset=8)[0]): 24 | return None 25 | rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=24))) 26 | key = md5(rc4_seed).digest()[:5] 27 | enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=32))) 28 | dec_data = RC4(key, enc_data) 29 | config_data = { 30 | "Version": unpack_from(">5s", data, offset=16)[0], 31 | "RC4Seed": hexlify(rc4_seed), 32 | "EncryptionKey": hexlify(key), 33 | "OnDiskConfigKey": unpack_from("20s", data, offset=8224)[0], 34 | "Build": dec_data[:16].strip("\x00"), 35 | "URLs": [url.strip("\x00") for url in dec_data[16:].split("|")], 36 | } 37 | return config_data 38 | 39 | 40 | if __name__ == "__main__": 41 | filename = argv[1] 42 | with open(filename, "r") as infile: 43 | t = extract_config(infile.read()) 44 | print(t) 45 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/BuerLoader.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Kevin O'Reilly kevoreilly@gmail.com 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from contextlib import suppress 16 | 17 | import pefile 18 | 19 | DESCRIPTION = "BuerLoader configuration parser." 20 | AUTHOR = "kevoreilly" 21 | 22 | 23 | def decrypt_string(string): 24 | return "".join(chr(ord(char) - 6) for char in string) 25 | 26 | 27 | def extract_config(filebuf): 28 | cfg = {} 29 | pe = pefile.PE(data=filebuf, fast_load=True) 30 | data_sections = [s for s in pe.sections if s.Name.find(b".data") != -1] 31 | if not data_sections: 32 | return None 33 | data = data_sections[0].get_data() 34 | for item in data.split(b"\x00\x00"): 35 | with suppress(Exception): 36 | dec = decrypt_string(item.lstrip(b"\x00").rstrip(b"\x00").decode()) 37 | if "dll" not in dec and " " not in dec and ";" not in dec and "." in dec: 38 | cfg.setdefault("CNCs", []).append(dec) 39 | return cfg 40 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/MyKings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Description: MyKings AKA Smominru config parser 3 | Author: x.com/YungBinary 4 | """ 5 | 6 | from contextlib import suppress 7 | import json 8 | import re 9 | import base64 10 | 11 | 12 | def contains_non_printable(byte_array): 13 | for byte in byte_array: 14 | if not chr(byte).isprintable(): 15 | return True 16 | return False 17 | 18 | 19 | def extract_base64_strings(data: bytes, minchars: int, maxchars: int) -> list: 20 | pattern = b"([A-Za-z0-9+/=]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00{4}" 21 | strings = [] 22 | for string in re.findall(pattern, data): 23 | decoded_string = base64_and_printable(string.decode()) 24 | if decoded_string: 25 | strings.append(decoded_string) 26 | return strings 27 | 28 | 29 | def base64_and_printable(b64_string: str): 30 | with suppress(Exception): 31 | decoded_bytes = base64.b64decode(b64_string) 32 | if not contains_non_printable(decoded_bytes): 33 | return decoded_bytes.decode('ascii') 34 | 35 | 36 | def extract_config(data: bytes) -> dict: 37 | config_dict = {} 38 | with suppress(Exception): 39 | cncs = extract_base64_strings(data, 12, 60) 40 | if cncs: 41 | # as they don't have schema they going under raw 42 | config_dict["raw"] = {"CNCs": cncs} 43 | return config_dict 44 | 45 | return {} 46 | 47 | 48 | if __name__ == "__main__": 49 | import sys 50 | 51 | with open(sys.argv[1], "rb") as f: 52 | print(json.dumps(extract_config(f.read()), indent=4)) 53 | -------------------------------------------------------------------------------- /tests_parsers/test_amadey.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Amadey import extract_config 6 | 7 | 8 | def test_amadey(): 9 | with open("tests/data/malware/994d115922a3ce8324114199fb7d06d7c8276779f83523b66b8c05505b81376e", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": [ 13 | "http://85.208.84.41/f7ehhfadDSk/index.php", 14 | "http://76.46.157.65/07hesnhcxD/index.php" 15 | ], 16 | "version": "5.55", 17 | "cryptokey": "f988f0065903c81142dd38f63d7ddc4e", 18 | "cryptokey_type": "RC4", 19 | "campaign_id": "1f3bdd", 20 | "raw": { 21 | "install_dir": "29da05bd1a", 22 | "install_file": "Vlimvoi.exe", 23 | } 24 | } 25 | with open("tests/data/malware/d7a366fa4d31c901ce3bcb6760d7bb5aa7cab49bb54d8c6551b3df14c8cf64e7", "rb") as data: 26 | conf = extract_config(data.read()) 27 | assert conf == { 28 | "CNCs": [ 29 | "http://91.92.243.129/0gjSy4hf3/index.php" 30 | ], 31 | "version": "5.70", 32 | "cryptokey": "f936986d553273aef6eeaeef713ad28f", 33 | "cryptokey_type": "RC4", 34 | "campaign_id": "07072f", 35 | "raw": { 36 | "install_dir": "067640a009", 37 | "install_file": "Yfgfwb.exe" 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /tests_parsers/test_amatera.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Amatera import extract_config 6 | 7 | 8 | def test_amatera(): 9 | with open("tests/data/malware/26db2f20d3d84657af15509ba39f62690a06175c2d5671795e239bdbe3acbaef", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "xor_key": "852149723", 13 | "cryptokey": "7640fed98a53856641763683163f4127b9fc00f9a788773c00ee1f2634cec82f", 14 | "cryptokey_type": "AES", 15 | "payload_guid_1": "f1575b64-8492-4e8b-b102-4d26e8c70371", 16 | "payload_guid_2": "08de0189-4e5e-477f-8700-1cd264a45266", 17 | "fake_c2": "aether100pronotification.table.core.windows.net", 18 | "CNCs": [ 19 | "https://91.98.229.246" 20 | ], 21 | } 22 | with open("tests/data/malware/674300bf497042020ffa74b4da8e8bc4c0abd95b90c17f55ae0c907ff8fccd53", "rb") as data: 23 | conf = extract_config(data.read()) 24 | assert conf == { 25 | "xor_key": "852149723", 26 | "cryptokey": "7640fed98a53856641763683163f4127b9fc00f9a788773c00ee1f2634cec82f", 27 | "cryptokey_type": "AES", 28 | "payload_guid_1": "f1575b64-8492-4e8b-b102-4d26e8c70371", 29 | "payload_guid_2": "08de2157-a1ab-4275-8705-4eaf40a53c78", 30 | "fake_c2": "cdn.extremepornvideos.com", 31 | "CNCs": [ 32 | "https://46.62.199.102" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/BackOffPOS.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | from hashlib import md5 3 | from struct import unpack_from 4 | from sys import argv 5 | 6 | import pefile 7 | from Cryptodome.Cipher import ARC4 8 | 9 | header_ptrn = b"Content-Type: application/x-www-form-urlencoded" 10 | 11 | 12 | def RC4(key, data): 13 | cipher = ARC4.new(key) 14 | return cipher.decrypt(data) 15 | 16 | 17 | def extract_config(data): 18 | config_data = {} 19 | pe = pefile.PE(data=data, fast_load=True) 20 | for section in pe.sections: 21 | if b".data" in section.Name: 22 | data = section.get_data() 23 | cfg_start = data.find(header_ptrn) 24 | if not cfg_start or cfg_start == -1: 25 | return None 26 | start_offset = cfg_start + len(header_ptrn) + 1 27 | rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=start_offset))) 28 | key = md5(rc4_seed).digest()[:5] 29 | enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=start_offset + 8))) 30 | dec_data = RC4(key, enc_data) 31 | config_data = { 32 | "RC4Seed": hexlify(rc4_seed), 33 | "EncryptionKey": hexlify(key), 34 | "Build": dec_data[:16].strip("\x00"), 35 | "URLs": [url.strip("\x00") for url in dec_data[16:].split("|")], 36 | "Version": unpack_from(">5s", data, offset=start_offset + 16 + 8192)[0], 37 | } 38 | return config_data 39 | 40 | 41 | if __name__ == "__main__": 42 | filename = argv[1] 43 | with open(filename, "rb") as infile: 44 | t = extract_config(infile.read()) 45 | print(t) 46 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/IcedIDLoader.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 kevoreilly, enzo 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | import struct 16 | from contextlib import suppress 17 | 18 | import pefile 19 | 20 | 21 | def extract_config(filebuf): 22 | config = {} 23 | pe = None 24 | with suppress(Exception): 25 | pe = pefile.PE(data=filebuf, fast_load=False) 26 | if pe is None: 27 | return 28 | for section in pe.sections: 29 | if section.Name == b".d\x00\x00\x00\x00\x00\x00": 30 | config_section = bytearray(section.get_data()) 31 | dec = [] 32 | for n, x in enumerate(config_section): 33 | k = x ^ config_section[n + 64] 34 | dec.append(k) 35 | if n > 32: 36 | break 37 | campaign, c2 = struct.unpack("I30s", bytes(dec)) 38 | c2 = c2.split(b"\00", 1)[0].decode() 39 | config["CNCs"] = f"http://{c2}" 40 | config["campaign"] = campaign 41 | return config 42 | 43 | 44 | if __name__ == "__main__": 45 | import sys 46 | from pathlib import Path 47 | 48 | data = Path(sys.argv[1]).read_bytes() 49 | print(extract_config(data)) 50 | -------------------------------------------------------------------------------- /cape_parsers/utils/dotnet_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: 4 | import dnfile 5 | 6 | HAVE_DNFILE = True 7 | logging.getLogger("dnfile").setLevel(logging.CRITICAL) 8 | logging.getLogger("dnfile.stream").setLevel(logging.CRITICAL) 9 | except ImportError: 10 | HAVE_DNFILE = False 11 | 12 | 13 | log = logging.getLogger("dotnet_utils") 14 | 15 | 16 | # TODO add extract string by mdtoken 17 | 18 | 19 | def dotnet_user_strings(file: str = False, data: bytes = False, dn_whitelisting: list = []): 20 | 21 | if not HAVE_DNFILE: 22 | return [] 23 | 24 | try: 25 | if file: 26 | dn = dnfile.dnPE(file) 27 | elif data: 28 | dn = dn = dnfile.dnPE(data=data) 29 | 30 | dn_strings = [] 31 | if not hasattr(dn, "net") or not hasattr(dn.net, "metadata") or not hasattr(dnfile, "streams"): 32 | return [] 33 | 34 | us: dnfile.stream.UserStringHeap = dn.net.metadata.streams.get(b"#US", None) 35 | if us: 36 | size = us.sizeof() 37 | offset = 1 38 | while offset < size: 39 | ret = us.get_with_size(offset) 40 | if not ret: 41 | break 42 | 43 | buf, readlen = ret 44 | try: 45 | if not buf.endswith(b"\x00\x00\x00"): 46 | buf = buf[:-1] 47 | s = dnfile.stream.UserString(buf) 48 | if s.value and not any([pattern in s.value for pattern in dn_whitelisting]): 49 | dn_strings.append(s.value) 50 | except UnicodeDecodeError: 51 | log.debug("Bad string:", buf.replace(b"\x00", b"")) 52 | # continue to next entry 53 | offset += readlen 54 | except Exception as e: 55 | log.error("dnFile error: ", str(e)) 56 | 57 | dn.close() 58 | return dn_strings 59 | -------------------------------------------------------------------------------- /tests_parsers/test_nitrogenbunyydownloader.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.core.NitroBunnyDownloader import extract_config 2 | 3 | 4 | def test_nitrogenbunnydownloader(): 5 | with (open("tests/data/malware/960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b", "rb") as data): 6 | conf = extract_config(data.read()) 7 | assert conf == { 8 | "CNCs": [ 9 | "https://617e7511-4.b-cdn.net/s?k=electronics", 10 | "https://617e7511-4.b-cdn.net/gp/product/B08J5W8Q7N", 11 | "https://617e7511-4.b-cdn.net/cart", 12 | "https://617e7511-4.b-cdn.net/hz/wishlist/ls/26564", 13 | "https://38.132.122.237/s?k=electronics", 14 | "https://38.132.122.237/gp/product/B08J5W8Q7N", 15 | "https://38.132.122.237/cart", 16 | "https://38.132.122.237/hz/wishlist/ls/26564"], 17 | "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", 18 | "raw": { 19 | "http_header_items": [ 20 | "Accept: application/json, application/javascript, text/javascript; charset=utf-8", 21 | "Content-Type: application/json; charset=utf-8", 22 | "X-Amazon-Trace-Id: Base64Encode(intern-session_01234567890)", 23 | "X-Amz-User-Agent: AmazonEnterpriseApplication/android-1.0.1 (Android 9API HoneyComb)", 24 | "X-Amz-Target: AmazonEnterpriseClient.BusinessLogic", 25 | "X-Amz-Date: 2038-03-14T03:14:07Z", 26 | "Amz-Safe-Signature: TURBECOMPLEXification_0123456789", 27 | "Amz-Security-Flag: Amazon8Imag genitalsHTLM5", 28 | "Cookie: sessionId=321116abbcdefXXxc8qRVfk; expires=600"], 29 | "unknown_1": 2261840800, 30 | "unknown_2": 1765860472, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Having problem/bug/issue 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## About accounts on [capesandbox.com](https://capesandbox.com/) 11 | * Issues isn't the way to ask for account activation. Ping capesandbox in [Twitter](https://twitter.com/capesandbox) with your username 12 | 13 | ## This is open source and you are getting __free__ support so be friendly! 14 | 15 | # Prerequisites 16 | 17 | Please answer the following questions for yourself before submitting an issue. 18 | 19 | - [ ] I am running the latest version 20 | - [ ] I did read the README! 21 | - [ ] I checked the documentation and found no answer 22 | - [ ] I checked to make sure that this issue has not already been filed 23 | - [ ] I'm reporting the issue to the correct repository (for multi-repository projects) 24 | - [ ] I have read and checked all configs (with all optional parts) 25 | 26 | 27 | # Expected Behavior 28 | 29 | Please describe the behavior you are expecting. __If your samples(x64) stuck in pending ensure that you set tags=x64 in hypervisor conf for x64 vms__ 30 | 31 | # Current Behavior 32 | 33 | What is the current behavior? 34 | 35 | # Failure Information (for bugs) 36 | 37 | Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template. 38 | 39 | ## Steps to Reproduce 40 | 41 | Please provide detailed steps for reproducing the issue. 42 | 43 | 1. step 1 44 | 2. step 2 45 | 3. you get it... 46 | 47 | ## Context 48 | 49 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. Operating system version, bitness, installed software versions, test sample details/hash/binary (if applicable). 50 | 51 | | Question | Answer 52 | |------------------|-------------------- 53 | | Git commit | Type `$ git log \| head -n1` to find out 54 | | OS version | Ubuntu 16.04, Windows 10, macOS 10.12.3 55 | 56 | ## Failure Logs 57 | 58 | Please include any relevant log snippets or files here. 59 | -------------------------------------------------------------------------------- /tests_parsers/test_adaptixbeacon.py: -------------------------------------------------------------------------------- 1 | from cape_parsers.CAPE.core.AdaptixBeacon import extract_config 2 | 3 | 4 | def test_adaptixbeacon(): 5 | # Adaptix Beacon 6 | with open("tests/data/malware/f78f5803be5704420cbb2e0ac3c57fcb3d9cdf443fbf1233c069760bee115b5d", "rb") as data: 7 | conf = extract_config(data.read()) 8 | assert conf == { 9 | "raw": { 10 | "cryptokey": "9030edf2700574ff942f8dadb826fac8", 11 | "cryptokey_type": "RC4", 12 | "agent_type": "BE4C0149", 13 | "use_ssl": 1, 14 | "servers": ["689535ed-3.b-cdn.net"], 15 | "ports": [443], 16 | "http_method": "POST", 17 | "uri": "/amazon/Trust/disputes/press-requests.php", 18 | "parameter": "A-Wabbon-Id", 19 | "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/131.0.2903.86", 20 | "http_headers": "Accept: application/json, application/javascript, text/javascript; charset=utf-8\r\nContent-Type: application/json; charset=utf-8\r\nX-Amazon-Trace-Id: Base64Encode(intern-session_01234567890)\r\nX-Amz-User-Agent: AmazonEnterpriseApplication/android-1.0.1 (Android 9API HoneyComb)\r\nX-Amz-Target: AmazonEnterpriseClient.BusinessLogic\r\nX-Amz-Date: 2038-03-14T03:14:07Z\r\nAmz-Safe-Signature: TURBECOMPLEXification_0123456789\r\nAmz-Security-Flag: Amazon8Imag genitalsHTLM5\r\nCookie: sessionId=321116abbcdefXXxc8qRVfk; expires=600\r\n", 21 | "ans_pre_size": 26, 22 | "ans_size": 21, 23 | "kill_date": 0, 24 | "working_time": 0, 25 | "sleep_delay": 20, 26 | "jitter_delay": 20, 27 | }, 28 | "cryptokey": "9030edf2700574ff942f8dadb826fac8", 29 | "cryptokey_type": "RC4", 30 | "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/131.0.2903.86", 31 | "CNCs": ["https://689535ed-3.b-cdn.net:443/amazon/Trust/disputes/press-requests.php"], 32 | } 33 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/Fareit.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from pathlib import Path 4 | 5 | """ 6 | rule pony { 7 | meta: 8 | author = "adam" 9 | description = "Detect pony" 10 | 11 | strings: 12 | $s1 = "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}" 13 | $s2 = "YUIPWDFILE0YUIPKDFILE0YUICRYPTED0YUI1.0" 14 | 15 | condition: 16 | $s1 and $s2 17 | } 18 | """ 19 | 20 | 21 | gate_url = re.compile(b".*\\.php$") 22 | exe_url = re.compile(b".*\\.exe$") 23 | dll_url = re.compile(b".*\\.dll$") 24 | 25 | 26 | def extract_config(memdump_path, read=False): 27 | if read: 28 | F = Path(memdump_path).read_bytes() 29 | else: 30 | F = memdump_path 31 | """ 32 | # Get the aPLib header + data 33 | buf = re.findall(r"aPLib .*PWDFILE", cData, re.DOTALL|re.MULTILINE) 34 | # Strip out the header 35 | if buf and len(buf[0]) > 200: 36 | cData = buf[0][200:] 37 | """ 38 | config = {} 39 | artifacts_raw = {} 40 | 41 | start = F.find(b"YUIPWDFILE0YUIPKDFILE0YUICRYPTED0YUI1.0") 42 | if start: 43 | F = F[start - 600 : start + 500] 44 | 45 | output = re.findall( 46 | b"(https?://.[A-Za-z0-9-\\.\\_\\~\\:\\/\\?\\#\\[\\]\\@\\!\\$\\&'\\(\\)\\*\\+\\,\\;\\=]+(?:\\.php|\\.exe|\\.dll))", F 47 | ) 48 | for url in output: 49 | try: 50 | if b"\x00" not in url: 51 | # url = self._check_valid_url(url) 52 | if url is None: 53 | continue 54 | url = url.lower() 55 | if gate_url.match(url): 56 | config.setdefault("CNCs", []).append(url.decode()) 57 | elif exe_url.match(url) or dll_url.match(url): 58 | artifacts_raw.setdefault("downloads", []).append(url.decode()) 59 | except Exception as e: 60 | print(e, sys.exc_info(), "PONY") 61 | if "downloads" in artifacts_raw: 62 | config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"])) 63 | return config 64 | 65 | 66 | if __name__ == "__main__": 67 | res = extract_config(sys.argv[1], read=True) 68 | print(res) 69 | -------------------------------------------------------------------------------- /tests_parsers/test_nanocore.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.NanoCore import extract_config 6 | 7 | 8 | def test_nanocore(): 9 | with open("tests/data/malware/f1bd511b69f95c26f489157272884a12225c1cf7a453207bfc46ce48a91eae96", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "raw": { 13 | "BuildTime": "2023-11-22 00:25:26.569697", 14 | "Version": "1.2.2.0", 15 | "Mutex": "dc5ce709-95b6-4a26-9175-16a1a8446828", 16 | "DefaultGroup": "6coinc", 17 | "PrimaryConnectionHost": "6coinc.zapto.org", 18 | "BackupConnectionHost": "127.0.0.1", 19 | "ConnectionPort": "6696", 20 | "RunOnStartup": "True", 21 | "RequestElevation": "False", 22 | "BypassUserAccountControl": "True", 23 | "ClearZoneIdentifier": "True", 24 | "ClearAccessControl": "False", 25 | "SetCriticalProcess": "False", 26 | "PreventSystemSleep": "True", 27 | "ActivateAwayMode": "False", 28 | "EnableDebugMode": "False", 29 | "RunDelay": "0", 30 | "ConnectDelay": "4000", 31 | "RestartDelay": "5000", 32 | "TimeoutInterval": "5000", 33 | "KeepAliveTimeout": "30000", 34 | "MutexTimeout": "5000", 35 | "LanTimeout": "2500", 36 | "WanTimeout": "8000", 37 | "BufferSize": "65535", 38 | "MaxPacketSize": "10485760", 39 | "GCThreshold": "10485760", 40 | "UseCustomDnsServer": "True", 41 | "PrimaryDnsServer": "8.8.8.8", 42 | "BackupDnsServer": "8.8.4.4", 43 | }, 44 | "CNCs": ["tcp://6coinc.zapto.org:6696", "tcp://127.0.0.1:6696"], 45 | "mutex": "dc5ce709-95b6-4a26-9175-16a1a8446828", 46 | "version": "1.2.2.0", 47 | "campaign": "6coinc", 48 | } 49 | -------------------------------------------------------------------------------- /tests_parsers/test_njrat.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.Njrat import extract_config 6 | 7 | 8 | def test_njrat(): 9 | with open("tests/data/malware/09bf19c00f3d8c63b8896edadd4622724a01f7d74de583733ee57a7d11eacd86", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "CNCs": ["tcp://peter-bikini.gl.at.ply.gg:64215"], 13 | "campaign": "HacKed", 14 | "version": "Njrat 0.7 Golden By Hassan Amiri", 15 | } 16 | 17 | 18 | """ 19 | https://github.com/kevoreilly/CAPEv2/pull/1957 20 | 21 | 09bf19c00f3d8c63b8896edadd4622724a01f7d74de583733ee57a7d11eacd86 22 | 2a5eb2f4bb25b89a9c3d325d893b87ed58fe87a6ada67c24f7cdef54b2138567 23 | 2e18a6a4b191741e57d8fb63bddb498f769344130e0f658d8ef5d74bd95c5c9b 24 | 4c8198288b00c70aeb7c9fcaae179873c618c1d5a804d36a54ac6e5c7fbacee2 25 | 4e1a8dff073c5648dbeaf55a6b3320461bcb0252cee9f8f5624f46e6d05b6584 26 | 55acd192c7cca3e46b8d1c0a24f98259ae093762722de3493a7da248e83ec07c 27 | 59f0979f3123e02ee0a13e3afa6b45d27b2fdbae75edc339d57d473d340851d8 28 | 5b147e624ad96d036c27aa9f526ed2e7daa9ca7bfe6639404dc8e71e1177a145 29 | 614b15eaa2b19e4f9ddb26639dbf5574126f552ae48afd7899a77bd6c7b8980d 30 | 646ed3f6856f58b90b4641ab24cdd1b6f9860b44243dfeaec952df7f0954b18a 31 | 710507e1f3e61b7010a445728b3c414efe068e22cac28c1dd3b8db56968262d7 32 | 77d1fcf6f8bea79cac80e284a9a5dbcc36b8b57eb86c9b74c538107d4baa2c1a 33 | 8b1b215f6a6f9881bc2b76ab409b0dff080dca31c538147a9d273ba7d05919e9 34 | a4e7f6de5b6c1514b5a4e3361191624127320bcff249ad16207ce79644ffb9c1 35 | a6c954599bf0b6a3f4e5b1d8bed604a09d1115a6b35b7e9a6de66f11a9977b81 36 | aeece6134d1a1f0789c8c35d2541164ebc6f23511e2d6781497a82e1bec73abd 37 | af2d5ae5ed7a72a3fa6a36cda93e163b84d8ad70a78afb08bcd1afa63d54f61e 38 | bb7efdb9cb3673c1768a0681989e2662d3f9683b45aded8f5b780a3310bec1bb 39 | c2c788ce1d3e55537c75684ceb961c01d9d9d0eb6b69c915c58433943320ffe5 40 | e5967d1012f24bad8914ecfbc79af2211ef491a4a16e2ac390d7d26089c5307a 41 | e69befafb01863bce3c730481fa21ff8e57c72351eec8002154538fe01e3cc9e 42 | e8636547c991ba1557cf0532a143ad2316427e773bcbe474a60d8ba2bcf3cea3 43 | f45abfb1e4d789528a7ce1469255a249a6cdf010045868992689d28c2b791719 44 | """ 45 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/unrecom.py: -------------------------------------------------------------------------------- 1 | import string 2 | import xml.etree.ElementTree as ET 3 | from io import StringIO 4 | from zipfile import ZipFile 5 | 6 | from Cryptodome.Cipher import ARC4 7 | 8 | 9 | def extract_embedded(zip_data): 10 | raw_embedded = None 11 | archive = StringIO(zip_data) 12 | with ZipFile(archive) as zip: 13 | for name in zip.namelist(): # get all the file names 14 | if name == "load/ID": # contains first part of key 15 | partial_key = zip.read(name) 16 | enckey = f"{partial_key}DESW7OWKEJRU4P2K" # complete key 17 | if name == "load/MANIFEST.MF": # this is the embedded jar 18 | raw_embedded = zip.read(name) 19 | if raw_embedded is None: 20 | return None 21 | # Decrypt the raw file 22 | return ARC4.new(enckey).decrypt(raw_embedded) 23 | 24 | 25 | def parse_embedded(data): 26 | newzipdata = data 27 | # Write new zip file to memory instead of to disk 28 | with StringIO(newzipdata) as newZip: 29 | with ZipFile(newZip) as zip: 30 | for name in zip.namelist(): 31 | if name == "config.xml": # this is the config in clear 32 | config = zip.read(name) 33 | return config 34 | 35 | 36 | def parse_config(config): 37 | xml = [x for x in config if x in string.printable] 38 | root = ET.fromstring(xml) 39 | raw_config = {} 40 | for child in root: 41 | if child.text.startswith("Unrecom"): 42 | raw_config["Version"] = child.text 43 | else: 44 | raw_config[child.attrib["key"]] = child.text 45 | return { 46 | "Version": raw_config["Version"], 47 | "Delay": raw_config["delay"], 48 | "Domain": raw_config["dns"], 49 | "Extension": raw_config["extensionname"], 50 | "Install": raw_config["install"], 51 | "Port1": raw_config["p1"], 52 | "Port2": raw_config["p2"], 53 | "Password": raw_config["password"], 54 | "PluginFolder": raw_config["pluginfoldername"], 55 | "Prefix": raw_config["prefix"], 56 | } 57 | 58 | 59 | def extract_config(data): 60 | embedded = extract_embedded(data) 61 | if embedded is None: 62 | return None 63 | config = parse_embedded(embedded) 64 | return parse_config(config) if config is not None else None 65 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/Azorult.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Kevin O'Reilly (kevoreilly@gmail.com) 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | import struct 16 | import logging 17 | 18 | import pefile 19 | import yara 20 | 21 | DESCRIPTION = "Azorult configuration parser." 22 | AUTHOR = "kevoreilly" 23 | 24 | YARA_RULES = """ 25 | rule Azorult 26 | { 27 | meta: 28 | author = "kevoreilly" 29 | description = "Azorult Payload" 30 | cape_type = "Azorult Payload" 31 | strings: 32 | $ref_c2 = {6A 00 6A 00 6A 00 6A 00 68 ?? ?? ?? ?? FF 55 F0 8B D8 C7 47 10 ?? ?? ?? ?? 90 C7 45 B0 C0 C6 2D 00 6A 04 8D 45 B0 50 6A 06 53 FF 55 D4} 33 | condition: 34 | uint16(0) == 0x5A4D and all of them 35 | } 36 | """ 37 | 38 | rules = yara.compile(source=YARA_RULES) 39 | log = logging.getLogger() 40 | 41 | 42 | def extract_config(filebuf): 43 | pe = pefile.PE(data=filebuf, fast_load=True) 44 | image_base = pe.OPTIONAL_HEADER.ImageBase 45 | 46 | for match in rules.match(data=filebuf): 47 | for block in match.strings: 48 | for instance in block.instances: 49 | try: 50 | cnc_offset = struct.unpack("i", instance.matched_data[21:25])[0] 51 | cnc = pe.get_data(cnc_offset - image_base, 32).split(b"\x00")[0] 52 | if cnc: 53 | if not cnc.startswith(b"http"): 54 | cnc = b"http://" + cnc 55 | return {"CNCs": [cnc.decode()]} 56 | except Exception as e: 57 | log.error("Error parsing Azorult config: %s", e) 58 | return {} 59 | 60 | 61 | if __name__ == "__main__": 62 | import sys 63 | 64 | with open(sys.argv[1], "rb") as f: 65 | print(extract_config(f.read())) 66 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/Strrat.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 enzok 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import base64 24 | import zipfile 25 | from hashlib import pbkdf2_hmac 26 | from io import BytesIO 27 | 28 | from Cryptodome.Cipher import AES 29 | 30 | 31 | def unpad(s): 32 | return s[: -s[-1]] 33 | 34 | 35 | def unzip_config(content: bytes): 36 | data = "" 37 | try: 38 | with zipfile.ZipFile(BytesIO(content)) as z: 39 | for name in z.namelist(): 40 | if "config.txt" in name: 41 | data = z.read(name) 42 | break 43 | except Exception: 44 | return 45 | return data 46 | 47 | 48 | def aesdecrypt(data, passkey): 49 | iv = data[4:20] 50 | key = pbkdf2_hmac("sha1", passkey, iv, 65536, 16) 51 | aes = AES.new(key, AES.MODE_CBC, iv) 52 | return unpad(aes.decrypt(data[20:])) 53 | 54 | 55 | def decode(data): 56 | decoded = "" 57 | try: 58 | data = base64.b64decode(data) 59 | except Exception as exc: 60 | return exc 61 | if data: 62 | passkey = b"strigoi" 63 | try: 64 | decoded = aesdecrypt(data, passkey) 65 | except Exception: 66 | return 67 | return decoded.decode() 68 | 69 | 70 | def extract_config(data): 71 | raw_config = {} 72 | configdata = unzip_config(data) 73 | 74 | if configdata: 75 | raw_config["raw"] = decode(configdata) 76 | 77 | return raw_config 78 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Hancitor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hancitor config extractor 3 | """ 4 | 5 | import hashlib 6 | import logging 7 | import re 8 | import struct 9 | 10 | import pefile 11 | from Cryptodome.Cipher import ARC4 12 | 13 | DESCRIPTION = "Hancitor config extractor." 14 | AUTHOR = "threathive, cccs-j" 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | def getHashKey(key_data): 20 | # source: https://github.com/OALabs/Lab-Notes/blob/main/Hancitor/hancitor.ipynb 21 | m = hashlib.sha1() 22 | m.update(key_data) 23 | key = m.digest()[:5] 24 | return key 25 | 26 | 27 | def get_key_config_data(filebuf, pe): 28 | # source: https://github.com/OALabs/Lab-Notes/blob/main/Hancitor/hancitor.ipynb 29 | RE_KEY = rb"\x6a(.)\x68(....)\x68\x00\x20\x00\x00" 30 | m = re.search(RE_KEY, filebuf) 31 | if not m: 32 | return 33 | key_len = struct.unpack("b", m.group(1))[0] 34 | key_address = struct.unpack("", "doomedraven "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | include = ["cape_parsers/**"] 10 | keywords = [ "cape", "parsers", "malware", "configuration"] 11 | 12 | packages = [{ include = "cape_parsers" }] 13 | 14 | 15 | [tool.poetry.dependencies] 16 | python = ">=3.10, <4.0" 17 | pefile = "*" 18 | capstone = ">=4.0.2" 19 | pycryptodomex = ">=3.20.0" 20 | # regex = ">=2021.7.6" 21 | netstruct = "1.1.2" 22 | maco = "1.1.8" 23 | yara-python = ">=4.5.1" 24 | dnfile = ">=0.15.1" 25 | dncil = ">=1.0.2" 26 | unicorn = ">=2.1.1" 27 | rat-king-parser = ">=4.1.0" 28 | 29 | ruff = ">=0.7.2" 30 | 31 | [tool.distutils.bdist_wheel] 32 | universal = false 33 | 34 | [tool.poetry.extras] 35 | maco = ["maco"] 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | pytest = "7.2.2" 39 | pytest-pretty = "1.1.0" 40 | pytest-cov = "3.0.0" 41 | pytest-mock = "3.7.0" 42 | pytest_asyncio = "0.18.3" 43 | pytest-xdist = "3.6.1" 44 | pytest-asyncio = "0.18.3" 45 | pytest-freezer = "0.4.8" 46 | tenacity = "8.1.0" 47 | httpretty = "^1.1.4" 48 | func-timeout = "^4.3.5" 49 | pre-commit = "^2.19.0" 50 | 51 | [tool.pytest.ini_options] 52 | pythonpath = ["."] 53 | testpaths = ["tests"] 54 | norecursedirs = "tests/zip_compound" 55 | asyncio_mode = "auto" 56 | 57 | [tool.poetry-dynamic-versioning] 58 | enable = true # Master switch to enable the plugin 59 | 60 | [build-system] 61 | requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] 62 | build-backend = "poetry.core.masonry.api" 63 | 64 | [tool.ruff] 65 | line-length = 132 66 | 67 | [tool.ruff.lint] 68 | select = [ 69 | "F", # pyflakes 70 | "E", # pycodestyle errors 71 | "W", # pycodestyle warnings 72 | # "I", # isort 73 | # "N", # pep8-naming 74 | "G", # flake8-logging-format 75 | ] 76 | 77 | ignore = [ 78 | "E501", # ignore due to conflict with formatter 79 | "N818", # exceptions don't need the Error suffix 80 | "E741", # allow ambiguous variable names 81 | "E402", 82 | "W605", # ToDo to fix - Invalid escape sequence 83 | ] 84 | 85 | fixable = ["ALL"] 86 | 87 | 88 | [tool.ruff.format] 89 | quote-style = "double" 90 | indent-style = "space" 91 | skip-magic-trailing-comma = false 92 | line-ending = "auto" 93 | 94 | [tool.ruff.lint.isort] 95 | known-first-party = ["libqtile", "test"] 96 | default-section = "third-party" 97 | 98 | [tool.mypy] 99 | warn_unused_configs = true 100 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/SparkRAT.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | from contextlib import suppress 5 | 6 | HAVE_PYCYPTODOMEX = False 7 | with suppress(ImportError): 8 | from Cryptodome.Cipher import AES 9 | from Cryptodome.Util import Counter 10 | 11 | HAVE_PYCYPTODOMEX = True 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | DESCRIPTION = "SparkRAT configuration parser." 17 | AUTHOR = "t-mtsmt" 18 | 19 | 20 | def extract_data_before_string(data, search_string, offset): 21 | search_bytes = search_string.encode("utf-8") 22 | 23 | position = data.find(search_bytes) 24 | if position == -1: 25 | return b"" 26 | 27 | start_position = max(position - offset, 0) 28 | return data[start_position:position] 29 | 30 | 31 | def decrypt_config(enc_data, key, iv): 32 | counter = Counter.new(128, initial_value=int.from_bytes(iv, "big")) 33 | cipher = AES.new(key, mode=AES.MODE_CTR, counter=counter) 34 | dec_data = cipher.decrypt(enc_data) 35 | config = dec_data.decode("utf-8") 36 | return json.loads(config) 37 | 38 | 39 | def extract_config(data): 40 | if not HAVE_PYCYPTODOMEX: 41 | log.error("Missed pycryptodomex. Run: poetry install") 42 | return {} 43 | 44 | search_string = "DXGI_ERROR_DRIVER_INTERNAL" 45 | config_buf_size = 0x180 46 | config_buf = extract_data_before_string(data, search_string, offset=config_buf_size) 47 | 48 | if len(config_buf) == 0: 49 | log.error("Configuration is not found.") 50 | return {} 51 | 52 | if config_buf == b"\x19" * config_buf_size: 53 | log.debug("Configuration does not exist because the template data in the ConfigBuffer was not replaced.") 54 | return {} 55 | 56 | try: 57 | with io.BytesIO(config_buf) as f: 58 | data_len = int.from_bytes(f.read(2), "big") 59 | key = f.read(16) 60 | iv = f.read(16) 61 | enc_data = f.read(data_len - 32) 62 | config = decrypt_config(enc_data, key, iv) 63 | if config: 64 | output = {"raw": config} 65 | output["CNCs"] = [ 66 | f"{'http' if not config['secure'] else 'https'}://{config['host']}:{config['port']}{config['path']}" 67 | ] 68 | return output 69 | except Exception as e: 70 | log.error("Configuration decryption failed: %s", e) 71 | return {} 72 | 73 | 74 | if __name__ == "__main__": 75 | import sys 76 | from pathlib import Path 77 | 78 | data = Path(sys.argv[1]).read_bytes() 79 | print(extract_config(data)) 80 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: [created] # TRIGGERS ONCE, WHEN THE GITHUB RELEASE IS PUBLISHED 6 | 7 | # Add concurrency to prevent duplicate runs for the same tag 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | name: Build distribution 📦 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # CRITICAL for dynamic versioning 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | 25 | - name: Install poetry 26 | shell: bash 27 | run: | 28 | PIP_BREAK_SYSTEM_PACKAGES=1 pip install poetry poetry-dynamic-versioning --user 29 | 30 | - name: "Build package" 31 | run: poetry build 32 | 33 | - name: Store the distribution packages 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: python-package-distributions 37 | path: dist/ 38 | 39 | publish-to-pypi: 40 | name: >- 41 | Publish Python 🐍 distribution 📦 to PyPI 42 | needs: 43 | - build 44 | runs-on: ubuntu-latest 45 | environment: 46 | name: pypi 47 | url: https://pypi.org/p/CAPE-parsers/ 48 | permissions: 49 | id-token: write 50 | 51 | steps: 52 | - name: Download all the dists 53 | uses: actions/download-artifact@v4 54 | with: 55 | name: python-package-distributions 56 | path: dist/ 57 | - name: Publish distribution 📦 to PyPI 58 | uses: pypa/gh-action-pypi-publish@release/v1 59 | 60 | github-release-signing: 61 | name: >- 62 | Sign and attach distributions to GitHub Release 63 | needs: 64 | - publish-to-pypi # Runs after PyPI publish is successful 65 | runs-on: ubuntu-latest 66 | 67 | permissions: 68 | contents: write # To upload assets to the release 69 | id-token: write # For sigstore 70 | 71 | steps: 72 | - name: Download all the dists 73 | uses: actions/download-artifact@v4 74 | with: 75 | name: python-package-distributions 76 | path: dist/ 77 | 78 | - name: Sign the dists with Sigstore 79 | uses: sigstore/gh-action-sigstore-python@v3.0.0 80 | with: 81 | inputs: >- 82 | ./dist/*.tar.gz 83 | ./dist/*.whl 84 | 85 | # The release already exists, so we just upload artifacts to it 86 | - name: Upload assets to GitHub Release 87 | env: 88 | GITHUB_TOKEN: ${{ github.token }} 89 | run: >- 90 | gh release upload 91 | '${{ github.ref_name }}' dist/** 92 | --repo '${{ github.repository }}' 93 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | env: 4 | COLUMNS: 132 5 | 6 | on: 7 | push: 8 | branches: [ main, staging ] 9 | pull_request: 10 | branches: [ main, staging ] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 20 16 | strategy: 17 | matrix: 18 | python-version: ["3.10"] 19 | steps: 20 | - name: Check out repository code 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | fetch-depth: 0 # IMPORTANT: This fetches all history and tags 25 | 26 | - name: Checkout test files repo 27 | uses: actions/checkout@v4 28 | with: 29 | repository: CAPESandbox/CAPE-TestFiles 30 | path: tests/data/ 31 | 32 | - uses: ./.github/actions/python-setup/ 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Run Ruff 37 | run: poetry run ruff check . --line-length 132 --ignore E501,E402 38 | 39 | - name: See if any parser changed 40 | uses: dorny/paths-filter@v3 41 | id: changes 42 | with: 43 | filters: | 44 | src: 45 | - 'cape_parsers/CAPE/core/*.py' 46 | - 'cape_parsers/CAPE/community/**.py' 47 | 48 | - name: Test parsers only if any parser changed 49 | if: steps.changes.outputs.src == 'true' 50 | run: | 51 | sudo apt-get install yara 52 | poetry run pip install yara-python 53 | poetry run python -m pytest tests_parsers -s --import-mode=append 54 | 55 | # setuptools-scm runs here automatically and sets the version 56 | # from the Git tag associated with the release. 57 | 58 | format: 59 | runs-on: ubuntu-latest 60 | timeout-minutes: 20 61 | strategy: 62 | matrix: 63 | python-version: ["3.10"] 64 | if: ${{ github.ref == 'refs/heads/master' }} 65 | 66 | steps: 67 | - name: Check out repository code 68 | uses: actions/checkout@v4 69 | 70 | - name: Set up python 71 | uses: ./.github/actions/python-setup 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | 75 | - name: Format with black 76 | run: poetry run black . 77 | 78 | # to be replaced with ruff 79 | - name: Format imports with isort 80 | run: poetry run isort . 81 | 82 | - name: Commit changes if any 83 | # Skip this step if being run by nektos/act 84 | if: ${{ !env.ACT }} 85 | run: | 86 | git config user.name "GitHub Actions" 87 | git config user.email "action@github.com" 88 | if output=$(git status --porcelain) && [ ! -z "$output" ]; then 89 | git commit -m "style: Automatic code formatting" -a 90 | git push 91 | fi 92 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/DoppelPaymer.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Kevin O'Reilly (kevoreilly@gmail.com) 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | DESCRIPTION = "DoppelPaymer configuration parser." 16 | AUTHOR = "kevoreilly" 17 | 18 | import string 19 | 20 | import pefile 21 | from Cryptodome.Cipher import ARC4 22 | 23 | rule_source = """ 24 | rule DoppelPaymer 25 | { 26 | meta: 27 | author = "kevoreilly" 28 | description = "DoppelPaymer Payload" 29 | cape_type = "DoppelPaymer Payload" 30 | 31 | strings: 32 | $getproc32 = {81 FB ?? ?? ?? ?? 74 2D 8B CB E8 ?? ?? ?? ?? 85 C0 74 0C 8B C8 8B D7 E8 ?? ?? ?? ?? 5B 5F C3} 33 | $cmd_string = "Setup run\\n" wide 34 | condition: 35 | uint16(0) == 0x5A4D and all of them 36 | } 37 | """ 38 | 39 | LEN_BLOB_KEY = 40 40 | 41 | 42 | def convert_char(c) -> str: 43 | if isinstance(c, int): 44 | c = chr(c) 45 | if c in string.printable: 46 | return c 47 | return f"\\x{ord(c):02x}" 48 | 49 | 50 | def decrypt_rc4(key, data): 51 | cipher = ARC4.new(key) 52 | return cipher.decrypt(data) 53 | 54 | 55 | def extract_rdata(pe): 56 | for section in pe.sections: 57 | if b".rdata" in section.Name: 58 | return section.get_data(section.VirtualAddress, section.SizeOfRawData) 59 | return None 60 | 61 | 62 | def extract_config(filebuf): 63 | pe = pefile.PE(data=filebuf, fast_load=True) 64 | config = {} 65 | blobs = filter(None, [x.strip(b"\x00\x00\x00\x00") for x in extract_rdata(pe).split(b"\x00\x00\x00\x00")]) 66 | for blob in blobs: 67 | if len(blob) < LEN_BLOB_KEY: 68 | continue 69 | raw = decrypt_rc4(blob[:LEN_BLOB_KEY][::-1], blob[LEN_BLOB_KEY:]) 70 | if not raw: 71 | continue 72 | for item in raw.split(b"\x00"): 73 | data = "".join(convert_char(c) for c in item) 74 | if len(data) == 406: 75 | config.setdefault("cryptokey", data) 76 | # ToDO proper naming here 77 | config.setdefault("raw", {})["cryptokey_type"] = "RSA public key" 78 | elif len(data) > 1 and "\\x" not in data: 79 | config.setdefault("raw", {})["strings"] = data 80 | return config 81 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/WinosStager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Description: Winos 4.0 "OnlineModule" config parser 3 | Author: x.com/YungBinary 4 | """ 5 | 6 | import re 7 | from contextlib import suppress 8 | 9 | CONFIG_KEY_MAP = { 10 | "dd": "execution_delay_seconds", 11 | "cl": "communication_interval_seconds", 12 | "bb": "version", 13 | "bz": "comment", 14 | "jp": "keylogger", 15 | "bh": "end_bluescreen", 16 | "ll": "anti_traffic_monitoring", 17 | "dl": "entrypoint", 18 | "sh": "process_daemon", 19 | "kl": "process_hollowing" 20 | } 21 | 22 | 23 | def find_config(data): 24 | start = ":db|".encode("utf-16le") 25 | end = ":1p|".encode("utf-16le") 26 | pattern = re.compile(re.escape(start) + b".*?" + re.escape(end), re.DOTALL) 27 | match = pattern.search(data) 28 | if match: 29 | return match.group(0).decode("utf-16le") 30 | 31 | 32 | def extract_config(data: bytes) -> dict: 33 | config_dict = {} 34 | final_config = {} 35 | 36 | with suppress(Exception): 37 | config = find_config(data) 38 | if not config: 39 | return config_dict 40 | 41 | # Reverse the config string, which is delimited by '|' 42 | config = config[::-1] 43 | # Remove leading/trailing pipes and split into key/value pairs 44 | elements = [element for element in config.strip('|').split('|') if ':' in element] 45 | # Split each element for key : value in a dictionary 46 | config_dict = dict(element.split(':', 1) for element in elements) 47 | if config_dict: 48 | # Handle extraction and formatting of CNCs 49 | for i in range(1, 4): 50 | p, o, t = config_dict.get(f"p{i}"), config_dict.get(f"o{i}"), config_dict.get(f"t{i}") 51 | if p and p != "127.0.0.1" and o: 52 | protocol = {"0": "udp", "1": "tcp"}.get(t) 53 | if protocol: 54 | cnc = f"{protocol}://{p}:{o}" 55 | final_config.setdefault("CNCs", []).append(cnc) 56 | 57 | if "CNCs" not in final_config: 58 | return {} 59 | 60 | final_config["CNCs"] = list(set(final_config["CNCs"])) 61 | # Extract campaign ID 62 | final_config["campaign"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"] 63 | 64 | # Check if the version has been extracted 65 | if "bb" in config_dict: 66 | final_config["version"] = config_dict["bb"] 67 | 68 | # Map keys, e.g. dd -> execution_delay_seconds 69 | final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict} 70 | 71 | return final_config 72 | 73 | 74 | if __name__ == "__main__": 75 | import sys 76 | 77 | with open(sys.argv[1], "rb") as f: 78 | print(extract_config(f.read())) 79 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/BlackNix.py: -------------------------------------------------------------------------------- 1 | import pefile 2 | 3 | 4 | def extract_raw_config(raw_data): 5 | try: 6 | pe = pefile.PE(data=raw_data) 7 | rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"]) 8 | rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx] 9 | for entry in rt_string_directory.directory.entries: 10 | if str(entry.name) == "SETTINGS": 11 | data_rva = entry.directory.entries[0].data.struct.OffsetToData 12 | size = entry.directory.entries[0].data.struct.Size 13 | data = pe.get_memory_mapped_image()[data_rva : data_rva + size] 14 | return data.split("}") 15 | except Exception: 16 | return None 17 | 18 | 19 | def decode(line): 20 | return "".join(chr(ord(char) - 1) for char in line) 21 | 22 | 23 | def domain_parse(config): 24 | return [domain.split(":", 1)[0] for domain in config["Domains"].split(";")] 25 | 26 | 27 | def extract_config(data): 28 | try: 29 | config_raw = extract_raw_config(data) 30 | if config_raw: 31 | return { 32 | "mutex": decode(config_raw[1])[::-1], 33 | "raw": { 34 | "Anti Sandboxie": decode(config_raw[2])[::-1], 35 | "Max Folder Size": decode(config_raw[3])[::-1], 36 | "Delay Time": decode(config_raw[4])[::-1], 37 | "Password": decode(config_raw[5])[::-1], 38 | "Kernel Mode Unhooking": decode(config_raw[6])[::-1], 39 | "User More Unhooking": decode(config_raw[7])[::-1], 40 | "Melt Server": decode(config_raw[8])[::-1], 41 | "Offline Screen Capture": decode(config_raw[9])[::-1], 42 | "Offline Keylogger": decode(config_raw[10])[::-1], 43 | "Copy To ADS": decode(config_raw[11])[::-1], 44 | "Domain": decode(config_raw[12])[::-1], 45 | "Persistence Thread": decode(config_raw[13])[::-1], 46 | "Active X Key": decode(config_raw[14])[::-1], 47 | "Registry Key": decode(config_raw[15])[::-1], 48 | "Active X Run": decode(config_raw[16])[::-1], 49 | "Registry Run": decode(config_raw[17])[::-1], 50 | "Safe Mode Startup": decode(config_raw[18])[::-1], 51 | "Inject winlogon.exe": decode(config_raw[19])[::-1], 52 | "Install Name": decode(config_raw[20])[::-1], 53 | "Install Path": decode(config_raw[21])[::-1], 54 | "Campaign Name": decode(config_raw[22])[::-1], 55 | "Campaign Group": decode(config_raw[23])[::-1], 56 | } 57 | } 58 | except Exception: 59 | return None 60 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Pandora.py: -------------------------------------------------------------------------------- 1 | import pefile 2 | 3 | 4 | def version_21(raw_config): 5 | if raw_config is None: 6 | return None 7 | return { 8 | "Version": "2.1", 9 | "Domain": raw_config[0], 10 | "Port": raw_config[1], 11 | "Password": raw_config[2], 12 | "Install Path": raw_config[3], 13 | "Install Name": raw_config[4], 14 | "HKCU Key": raw_config[5], 15 | "ActiveX Key": raw_config[6], 16 | "Install Flag": raw_config[7], 17 | "StartupFlag": raw_config[8], 18 | "ActiveXFlag": raw_config[9], 19 | "HKCU Flag": raw_config[10], 20 | "Mutex": raw_config[11], 21 | "userMode Hooking": raw_config[12], 22 | "Melt": raw_config[13], 23 | "Keylogger": raw_config[14], 24 | "Campaign ID": raw_config[15], 25 | "UnknownFlag9": raw_config[16], 26 | } 27 | 28 | 29 | def version_22(raw_config): 30 | if raw_config is None: 31 | return None 32 | return { 33 | "Version": "2.2", 34 | "Domain": raw_config[0], 35 | "Port": raw_config[1], 36 | "Password": raw_config[2], 37 | "Install Path": raw_config[3], 38 | "Install Name": raw_config[4], 39 | "HKCU Key": raw_config[5], 40 | "ActiveX Key": raw_config[6], 41 | "Install Flag": raw_config[7], 42 | "StartupFlag": raw_config[8], 43 | "ActiveXFlag": raw_config[9], 44 | "HKCU Flag": raw_config[10], 45 | "Mutex": raw_config[11], 46 | "userMode Hooking": raw_config[12], 47 | "Melt": raw_config[13], 48 | "Keylogger": raw_config[14], 49 | "Campaign ID": raw_config[15], 50 | "UnknownFlag9": raw_config[16], 51 | } 52 | 53 | 54 | def get_config(data): 55 | try: 56 | pe = pefile.PE(data=data) 57 | rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"]) 58 | rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx] 59 | for entry in rt_string_directory.directory.entries: 60 | if str(entry.name) == "CFG": 61 | data_rva = entry.directory.entries[0].data.struct.OffsetToData 62 | size = entry.directory.entries[0].data.struct.Size 63 | data = pe.get_memory_mapped_image()[data_rva : data_rva + size] 64 | cleaned = data.replace("\x00", "") 65 | return cleaned.split("##") 66 | except Exception: 67 | return 68 | 69 | 70 | def extract_config(data): 71 | raw_config = get_config(data) 72 | if raw_config: 73 | if len(raw_config) == 19: 74 | clean_config = version_21(raw_config) 75 | elif len(raw_config) == 20: 76 | clean_config = version_22(raw_config) 77 | if clean_config: 78 | clean_config = {"raw": clean_config} 79 | return clean_config 80 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/AuroraStealer.py: -------------------------------------------------------------------------------- 1 | # Derived from https://github.com/RussianPanda95/Configuration_extractors/blob/main/aurora_config_extractor.py 2 | # A huge thank you to RussianPanda95 3 | 4 | import base64 5 | import json 6 | import logging 7 | import re 8 | 9 | log = logging.getLogger(__name__) 10 | log.setLevel(logging.INFO) 11 | 12 | patterns = [ 13 | rb"[A-Za-z0-9+/]{4}(?:[A-Za-z0-9+/]{4})*(?=[0-9]+)", 14 | rb"(?:[A-Za-z0-9+/]{4}){2,}(?:[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)", 15 | ] 16 | 17 | 18 | def extract_config(data): 19 | config_dict = {} 20 | matches = [] 21 | for pattern in patterns: 22 | matches.extend(re.findall(pattern, data)) 23 | 24 | matches = [match for match in matches if len(match) > 90] 25 | 26 | # Search for the configuration module in the binary 27 | config_match = re.search(rb"eyJCdWlsZElEI[^&]{0,400}", data) 28 | if config_match: 29 | matched_string = config_match.group(0).decode("utf-8") 30 | decoded_str = base64.b64decode(matched_string).decode() 31 | for item in decoded_str.split(","): 32 | key = item.split(":")[0].strip("{").strip('"') 33 | value = item.split(":")[1].strip('"') 34 | if key == "IP": 35 | config_dict["CNCs"] = [f"tcp://{value}"] 36 | elif key == "BuildID": 37 | config_dict["build"] = value 38 | else: 39 | if value: 40 | config_dict.setdefault("raw", {})[key] = value 41 | 42 | grabber_found = False 43 | 44 | # Extracting the modules 45 | for match in matches: 46 | match_str = match.decode("utf-8") 47 | decoded_str = base64.b64decode(match_str) 48 | 49 | if b"DW" in decoded_str: 50 | data_dict = json.loads(decoded_str) 51 | for elem in data_dict: 52 | if elem["Method"] == "DW": 53 | config_dict.setdefault("raw", {})["Loader module"] = elem 54 | 55 | if b"PS" in decoded_str: 56 | data_dict = json.loads(decoded_str) 57 | for elem in data_dict: 58 | if elem["Method"] == "PS": 59 | config_dict.setdefault("raw", {})["PowerShell module"] = elem 60 | 61 | if b"Path" in decoded_str: 62 | grabber_found = True 63 | break 64 | else: 65 | grabber_match = re.search(b"W3siUGF0aCI6.{116}", data) 66 | if grabber_match: 67 | encoded_string = grabber_match.group(0) 68 | decoded_str = base64.b64decode(encoded_string) 69 | grabber_str = decoded_str[:95].decode("utf-8", errors="ignore") 70 | cleanup_str = grabber_str.split("[")[-1].split("]")[0] 71 | 72 | if not grabber_found: 73 | grabber_found = True 74 | config_dict.setdefault("raw", {})["Grabber"] = cleanup_str 75 | 76 | return config_dict 77 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/_VirusRat.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import database 4 | import ioc 5 | 6 | 7 | def run(md5, data): 8 | config_dict = {} 9 | config = data.split("abccba") 10 | if len(config) > 5: 11 | config_dict = { 12 | "Domain": config[1], 13 | "Port": config[2], 14 | "Campaign Name": config[3], 15 | "Copy StartUp": config[4], 16 | "StartUp Name": config[5], 17 | "Add To Registry": config[6], 18 | "Registry Key": config[7], 19 | "Melt + Inject SVCHost": config[8], 20 | "Anti Kill Process": config[9], 21 | "USB Spread": config[10], 22 | "Kill AVG 2012-2013": config[11], 23 | "Kill Process Hacker": config[12], 24 | "Kill Process Explorer": config[13], 25 | "Kill NO-IP": config[14], 26 | "Block Virus Total": config[15], 27 | "Block Virus Scan": config[16], 28 | "HideProcess": config[17], 29 | } 30 | snortRule(md5, config_dict) 31 | createIOC(md5, config_dict) 32 | database.insertDomain(md5, [config_dict["Domain"]]) 33 | return config_dict 34 | 35 | 36 | def snortRule(md5, config_dict): 37 | rules = [] 38 | domain = config_dict["Domain"] 39 | ipPattern = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}") 40 | ipTest = ipPattern.search(domain) 41 | if len(domain) > 1: 42 | if ipTest: 43 | rules.append( 44 | f"""alert tcp any any -> {domain}""" 45 | f""" any (msg: "VirusRat Beacon Domain: {domain}""" 46 | """"; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""" 47 | ) 48 | else: 49 | rules.append( 50 | f"""alert udp any any -> any 53 (msg: "VirusRat Beacon Domain: {domain}""" 51 | f""""; content:"|0e|{domain}""" 52 | """|00|"; nocase; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""", 53 | f"""alert tcp any any -> any 53 (msg: "VirusRat Beacon Domain: {domain}""" 54 | f""""; content:"|0e|{domain}""" 55 | """|00|"; nocase; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""", 56 | ) 57 | database.insertSnort(md5, rules) 58 | 59 | 60 | # IOC Creator Two elements Domain or install 61 | def createIOC(md5, config_dict): 62 | items = [ 63 | ("contains", "Network", "Network/DNS", "string", config_dict["Domain"]), 64 | ("is", "PortItem", "PortItem/remotePort", "string", config_dict["Port"]), 65 | ("is", "ProcessItem", "ProcessItem/name", "string", config_dict["StartUp Name"]), 66 | ("is", "RegistryItem", "RegistryItem/Value", "string", config_dict["Registry Key"]), 67 | ] 68 | IOC = ioc.main(items) 69 | database.insertIOC(md5, IOC) 70 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/ChChes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Kevin O'Reilly kevin.oreilly@contextis.co.uk 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | DESCRIPTION = "ChChes configuration parser." 15 | AUTHOR = "kevoreilly" 16 | 17 | import yara 18 | 19 | rule_source = """ 20 | rule ChChes 21 | { 22 | meta: 23 | author = "kev" 24 | description = "ChChes Payload" 25 | cape_type = "ChChes Payload" 26 | strings: 27 | $payload1 = {55 8B EC 53 E8 EB FC FF FF E8 DB FF FF FF 05 10 FE 2A 00 33 DB 39 58 44 75 58 56 57 50 E8 57 00 00 00 59 8B F0 E8 AB FF FF FF B9 01 1F 2A 00 BF D0 1C 2A 00 2B CF 03 C1 39 5E 30 76 0F} 28 | $payload2 = {55 8B EC 53 E8 8F FB FF FF E8 DB FF FF FF 05 00 07 FF 00 33 DB 39 58 44 75 58 56 57 50 E8 57 00 00 00 59 8B F0 E8 AB FF FF FF B9 5D 20 FE 00 BF D0 1C FE 00 2B CF 03 C1 39 5E 30 76 0F } 29 | $payload3 = {55 8B EC 53 E8 E6 FC FF FF E8 DA FF FF FF 05 80 FC FC 00 33 DB 39 58 44 75 58 56 57 50 E8 57 00 00 00 59 8B F0 E8 AA FF FF FF B9 05 1F FC 00 BF D0 1C FC 00 2B CF 03 C1 39 5E 30 76 0F} 30 | $payload4 = {55 8B EC E8 ?? ?? FF FF E8 D? FF FF FF 05 ?? ?? ?? 00 83 78 44 00 75 40 56 57 50 E8 3E 00 00 00 59 8B F0 6A 00 FF 76 30 E8 A8 FF FF FF B9 ?? ?? ?? 00 BF 00 1A E1 00 2B CF 03 C1 50 FF 56 70} 31 | condition: 32 | $payload1 or $payload2 or $payload3 or $payload4 33 | } 34 | """ 35 | 36 | MAX_STRING_SIZE = 128 37 | 38 | 39 | def yara_scan(raw_data): 40 | addresses = {} 41 | yara_rules = yara.compile(source=rule_source) 42 | matches = yara_rules.match(data=raw_data) 43 | for match in matches: 44 | if match.rule != "ChChes": 45 | continue 46 | 47 | for block in match.strings: 48 | for instance in block.instances: 49 | addresses[block.identifier] = instance.offset 50 | return addresses 51 | 52 | 53 | def string_from_offset(data, offset): 54 | return data[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 55 | 56 | 57 | def extract_config(filebuf): 58 | config = {} 59 | yara_matches = yara_scan(filebuf) 60 | 61 | c2_offsets = [] 62 | if yara_matches.get("$payload1"): 63 | c2_offsets.append(0xE455) 64 | if yara_matches.get("$payload2"): 65 | c2_offsets.append(0xED55) 66 | if yara_matches.get("$payload3"): 67 | c2_offsets.append(0xE2B9) 68 | # no c2 for type4 69 | 70 | for c2_offset in c2_offsets: 71 | c2_url = string_from_offset(filebuf, c2_offset) 72 | if c2_url: 73 | config.setdefault("CNCs", []).append(c2_url) 74 | 75 | return config 76 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/JavaDropper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import hashlib 3 | import string 4 | import zlib 5 | from base64 import b64decode 6 | from io import StringIO 7 | from zipfile import ZipFile 8 | 9 | # Non Standard Imports 10 | from Cryptodome.Cipher import AES, ARC4, XOR 11 | 12 | # Helper Functions Go Here 13 | 14 | 15 | def string_print(line): 16 | return [x for x in line if x in string.printable] 17 | 18 | 19 | #### Ciphers #### 20 | def decrypt_RC4(enckey, data): 21 | cipher = ARC4.new(enckey) 22 | return cipher.decrypt(data) 23 | 24 | 25 | def decrypt_AES(enckey, data): 26 | cipher = AES.new(enckey) 27 | return cipher.decrypt(data) 28 | 29 | 30 | def decrypt_XOR(enckey, data): 31 | cipher = XOR.new(enckey) 32 | return cipher.decrypt(data) 33 | 34 | 35 | def parse_ek(key, drop): 36 | enc_key = key[:16] 37 | coded = drop 38 | drop_details = key[16:] 39 | decoded = decrypt_AES(enc_key, coded) 40 | for section in drop_details.split(","): 41 | print(b64decode(section).decode("hex")) 42 | return decoded 43 | 44 | 45 | def parse_load(key, drop): 46 | raw_key = f"{key}ALSKEOPQLFKJDUSIKSJAUIE" 47 | enc_key = hashlib.sha256(raw_key).hexdigest() 48 | return decrypt_RC4(enc_key, drop) 49 | 50 | 51 | def parse_stub(drop): 52 | keys = ("0kwi38djuie8oq89", "0B4wCrd5N2OxG93h") 53 | 54 | for key in keys: 55 | decoded = decrypt_AES(key, drop) 56 | if "META-INF" in decoded: 57 | print("Found Embedded Jar") 58 | return decoded 59 | if "Program" in decoded: 60 | print("Found Embedded EXE") 61 | return decoded 62 | 63 | 64 | def parse_xor(key, drop): 65 | key2 = 'FYj&w3bd"m/kSZjD' 66 | decoded = decrypt_XOR(key2, drop) 67 | return zlib.decompress(decoded, 16 + zlib.MAX_WBITS) 68 | 69 | 70 | # Jar Parser 71 | def extract_config(raw_data): 72 | decoded = False 73 | jar_data = StringIO(raw_data) 74 | with ZipFile(jar_data, "r") as jar: 75 | files = jar.namelist() 76 | if "e" in files and "k" in files: 77 | print("Found EK Dropper") 78 | key = jar.read("k") 79 | drop = jar.read("e") 80 | decoded = parse_ek(key, drop) 81 | 82 | if "config.ini" in files and "password.ini" in files: 83 | print("Found LoadStub Dropper") 84 | key = jar.read("password.ini") 85 | drop = jar.read("config.ini") 86 | decoded = parse_load(key, drop) 87 | 88 | if "stub/stub.dll" in files: 89 | print("Found Stub Dropper") 90 | drop = jar.read("stub/stub.dll") 91 | decoded = parse_stub(drop) 92 | 93 | if "c.dat" in files: 94 | print("Found XOR Dropper") 95 | key_file = b64decode(jar.read("c.dat")) 96 | key_text = decrypt_XOR("\xdd", key_file) 97 | drop_file = key_text.split("\n", 2)[1] 98 | key = key_text.split("\n", 6)[5] 99 | print(key) 100 | decoded = parse_xor(key, jar.read(drop_file)) 101 | 102 | if decoded: 103 | return decoded 104 | else: 105 | print("Unable to decode") 106 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/MonsterV2.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from Crypto.Cipher import ChaCha20_Poly1305 3 | from contextlib import suppress 4 | import zlib 5 | import struct 6 | import json 7 | import yara 8 | import pefile 9 | 10 | 11 | RULE_SOURCE = """rule MonsterV2Config 12 | { 13 | meta: 14 | author = "doomedraven,YungBinary" 15 | strings: 16 | $chunk_1 = { 17 | 41 B8 0E 04 00 00 18 | 48 8D 15 ?? ?? ?? 00 19 | 48 8B C? 20 | E8 ?? ?? ?? ?? [3-17] 21 | 4C 8B C? 22 | 48 8D 54 24 28 23 | 48 8B CE 24 | E8 ?? ?? ?? ?? 25 | } 26 | condition: 27 | $chunk_1 28 | }""" 29 | 30 | 31 | def derive_chacha_key_nonce_blake2b(seed: bytes): # -> tuple[bytes, bytes]: 32 | """ 33 | Derives a 32-byte ChaCha20 key and a 24-byte ChaCha20 nonce 34 | using BLAKE2b from a given seed. 35 | """ 36 | output_length = 56 # 32 bytes for key + 24 bytes for nonce 37 | h = hashlib.blake2b(digest_size=output_length) 38 | h.update(seed) 39 | derived_material = h.digest() 40 | chacha20_key = derived_material[0:32] 41 | chacha20_nonce = derived_material[32:56] 42 | return chacha20_key, chacha20_nonce 43 | 44 | 45 | def yara_scan(raw_data, rule_source): 46 | yara_rules = yara.compile(source=rule_source) 47 | matches = yara_rules.match(data=raw_data) 48 | 49 | for match in matches: 50 | for block in match.strings: 51 | for instance in block.instances: 52 | return instance.offset 53 | 54 | def extract_config(data: bytes) -> dict: 55 | config_dict = {} 56 | with suppress(Exception): 57 | pe = pefile.PE(data=data) 58 | offset = yara_scan(data, RULE_SOURCE) 59 | 60 | # image_base = pe.OPTIONAL_HEADER.ImageBase 61 | disp_offset = data[offset + 9 : offset + 13] 62 | disp_offset = struct.unpack('i', disp_offset)[0] 63 | instruction_pointer_va = pe.get_rva_from_offset(offset + 13) 64 | config_offset_va = instruction_pointer_va + disp_offset 65 | config_offset = pe.get_offset_from_rva(config_offset_va) 66 | 67 | 68 | blake_seed = data[config_offset : config_offset + 32] 69 | chacha20_key, chacha20_nonce = derive_chacha_key_nonce_blake2b(blake_seed) 70 | cipher_len = int.from_bytes(data[config_offset + 32 : config_offset + 40], byteorder="big") 71 | cipher_text = data[config_offset + 40 : config_offset + 40 + cipher_len] 72 | 73 | cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=chacha20_nonce) 74 | decrypted_zlib_data = cipher.decrypt(cipher_text) 75 | decompressed_data = zlib.decompress(decrypted_zlib_data) 76 | config_dict = json.loads(decompressed_data) 77 | 78 | if config_dict: 79 | final_config = {"raw": config_dict} 80 | if "ip" in config_dict and "port" in config_dict: 81 | final_config["CNCs"] = [f"tcp://{config_dict['ip']}:{config_dict['port']}"] 82 | if "build_name" in config_dict: 83 | final_config["build"] = config_dict["build_name"] 84 | return final_config 85 | 86 | return {} 87 | 88 | 89 | if __name__ == "__main__": 90 | import sys 91 | 92 | with open(sys.argv[1], "rb") as f: 93 | print(extract_config(f.read())) 94 | -------------------------------------------------------------------------------- /tests_parsers/test_cobaltstrikebeacon.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation. 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | 5 | from cape_parsers.CAPE.community.CobaltStrikeBeacon import extract_config 6 | 7 | 8 | def test_csb(): 9 | with open("tests/data/malware/2588fd3232138f587e294aea5cc9a0611d1e165b199743552c84bfddc1e4c063", "rb") as data: 10 | conf = extract_config(data.read()) 11 | assert conf == { 12 | "raw": { 13 | "BeaconType": ["HTTP"], 14 | "Port": 4848, 15 | "SleepTime": 60000, 16 | "MaxGetSize": 1048576, 17 | "Jitter": 0, 18 | "MaxDNS": "Not Found", 19 | "PublicKey": "30819f300d06092a864886f70d010101050003818d0030818902818100bebe41805d3c15a738caf3e308a992d4d507ce827996a8c9d783c766963e7e73083111729ae0abc1b49af0bcf803efdcaf83ac694fb53d043a88e9333f169e026a3c4e63cc6d4cd1aa5e199cb95eec500f948ac472c0ab2eda385d35fb8592d74b1154a1c671afb310eccb0b139ee1100907bfcdd8dfbf3385803a11bc252995020301000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 20 | "C2Server": "192.144.206.100,/load", 21 | "UserAgent": "Not Found", 22 | "HttpPostUri": "/submit.php", 23 | "Malleable_C2_Instructions": [], 24 | "HttpGet_Metadata": "Not Found", 25 | "HttpPost_Metadata": "Not Found", 26 | "SpawnTo": "d7a9ca15a07f82bfd3b63020da38aa16", 27 | "PipeName": "Not Found", 28 | "DNS_Idle": "Not Found", 29 | "DNS_Sleep": "Not Found", 30 | "SSH_Host": "Not Found", 31 | "SSH_Port": "Not Found", 32 | "SSH_Username": "Not Found", 33 | "SSH_Password_Plaintext": "Not Found", 34 | "SSH_Password_Pubkey": "Not Found", 35 | "HttpGet_Verb": "GET", 36 | "HttpPost_Verb": "POST", 37 | "HttpPostChunk": 0, 38 | "Spawnto_x86": "%windir%\\syswow64\\rundll32.exe", 39 | "Spawnto_x64": "%windir%\\sysnative\\rundll32.exe", 40 | "CryptoScheme": 0, 41 | "Proxy_Config": "Not Found", 42 | "Proxy_User": "Not Found", 43 | "Proxy_Password": "Not Found", 44 | "Proxy_Behavior": "Use IE settings", 45 | "Watermark": 391144938, 46 | "bStageCleanup": "False", 47 | "bCFGCaution": "False", 48 | "KillDate": 0, 49 | "bProcInject_StartRWX": "True", 50 | "bProcInject_UseRWX": "True", 51 | "bProcInject_MinAllocSize": 0, 52 | "ProcInject_PrependAppend_x86": "Empty", 53 | "ProcInject_PrependAppend_x64": "Empty", 54 | "ProcInject_Execute": ["CreateThread", "SetThreadContext", "CreateRemoteThread", "RtlCreateUserThread"], 55 | "ProcInject_AllocationMethod": "VirtualAllocEx", 56 | "bUsesCookies": "True", 57 | "HostHeader": "", 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cape_parsers/utils/blzpack.py: -------------------------------------------------------------------------------- 1 | # included from https://github.com/sysopfb/brieflz 2 | 3 | import binascii 4 | import os 5 | import struct 6 | import zlib 7 | from ctypes import byref, c_int, cdll, create_string_buffer 8 | 9 | CURR_DIR = os.path.abspath(os.path.dirname(__file__)) 10 | LIB_PATH = os.path.join(CURR_DIR, "blzpack_lib.so") 11 | brieflz = cdll.LoadLibrary(LIB_PATH) 12 | 13 | 14 | DEFAULT_BLOCK_SIZE = 1024 * 1024 15 | 16 | 17 | def compress_data(data, blocksize, level): 18 | compressed_data = "" 19 | while len(data) > 0: 20 | buf = create_string_buffer(data[:blocksize]) 21 | cb = c_int(len(buf)) 22 | cbOut = brieflz.blz_max_packed_size(blocksize) 23 | packed = create_string_buffer(cbOut) 24 | workmem = create_string_buffer(brieflz.blz_workmem_size_level(blocksize, 1)) 25 | cbOut = c_int(cbOut) 26 | retval = brieflz.blz_pack_level(byref(buf), byref(packed), cb, byref(workmem), level) 27 | if retval > 0: 28 | temp = packed.raw[:retval] 29 | tempret = ( 30 | struct.pack( 31 | ">IIIIII", 32 | 1651276314, 33 | level, 34 | len(temp), 35 | zlib.crc32(temp) % (1 << 32), 36 | len(buf), 37 | zlib.crc32(data[:blocksize]) % (1 << 32), 38 | ) 39 | + temp 40 | ) 41 | compressed_data += tempret 42 | else: 43 | print("Compression Error") 44 | return None 45 | data = data[blocksize:] 46 | return compressed_data 47 | 48 | 49 | def decompress_data(data, blocksize=DEFAULT_BLOCK_SIZE, level=1): 50 | decompressed_data = b"" 51 | # max_packed_size = brieflz.blz_max_packed_size(blocksize) 52 | 53 | (magic, level, packedsize, crc, hdr_depackedsize, crc2) = struct.unpack_from(">IIIIII", data) 54 | data = data[24:] 55 | while magic == 0x626C7A1A and len(data) > 0: 56 | compressed_data = create_string_buffer(data[:packedsize]) 57 | workdata = create_string_buffer(blocksize) 58 | depackedsize = brieflz.blz_depack(byref(compressed_data), byref(workdata), c_int(hdr_depackedsize)) 59 | if depackedsize != hdr_depackedsize: 60 | print("Decompression error") 61 | print(f"DepackedSize: {depackedsize}\nHdrVal: {hdr_depackedsize}") 62 | return None 63 | decompressed_data += workdata.raw[:depackedsize] 64 | data = data[packedsize:] 65 | if len(data) > 0: 66 | (magic, level, packedsize, crc, hdr_depackedsize, crc2) = struct.unpack_from(">IIIIII", data) 67 | data = data[24:] 68 | else: 69 | break 70 | return decompressed_data 71 | 72 | 73 | def main(): 74 | # blocksize = DEFAULT_BLOCK_SIZE 75 | blocksize = 100 76 | level = 1 77 | data = "This is a test of brieflz compression" * 100 78 | retval = compress_data(data, blocksize, level) 79 | if retval is not None: 80 | print("Compression SUCCESS!\nCompressed Data: ") 81 | print(binascii.hexlify(retval)) 82 | retval = decompress_data(retval, blocksize, level) 83 | if retval is not None and retval == data: 84 | print("Decompress SUCCESS!\nDecompress Data: ") 85 | print(retval) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/REvil.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 R3MRUM (https://twitter.com/R3MRUM) 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | #!/usr/bin/python 16 | 17 | import json 18 | import struct 19 | 20 | import pefile 21 | 22 | 23 | def getSectionNames(sections): 24 | return [section.Name.partition(b"\0")[0] for section in sections] 25 | 26 | 27 | def getREvilKeyAndConfig(pesections, section_name): 28 | for section in pesections: 29 | if section.Name.partition(b"\0")[0] == section_name: 30 | data = section.get_data() 31 | if len(data) > 32: 32 | key = data[:32] 33 | encoded_config = data[32:] 34 | return key, encoded_config 35 | 36 | 37 | def decodeREvilConfig(config_key, config_data): 38 | init255 = list(range(256)) 39 | 40 | key = config_key 41 | config_len = struct.unpack(". 14 | 15 | DESCRIPTION = "BitPaymer configuration parser." 16 | AUTHOR = "kevoreilly" 17 | 18 | import string 19 | 20 | import pefile 21 | from Cryptodome.Cipher import ARC4 22 | 23 | import yara 24 | 25 | rule_source = """ 26 | rule BitPaymer 27 | { 28 | meta: 29 | author = "kevoreilly" 30 | description = "BitPaymer Payload" 31 | cape_type = "BitPaymer Payload" 32 | 33 | strings: 34 | $decrypt32 = {6A 40 58 3B C8 0F 4D C1 39 46 04 7D 50 53 57 8B F8 81 E7 3F 00 00 80 79 05 4F 83 CF C0 47 F7 DF 99 1B FF 83 E2 3F 03 C2 F7 DF C1 F8 06 03 F8 C1 E7 06 57} 35 | $antidefender = "TouchMeNot" wide 36 | condition: 37 | uint16(0) == 0x5A4D and all of them 38 | } 39 | """ 40 | 41 | LEN_BLOB_KEY = 40 42 | 43 | 44 | def convert_char(c): 45 | if c in (string.letters + string.digits + string.punctuation + " \t\r\n"): 46 | # ToDo gonna break as its int 47 | return c 48 | return f"\\x{ord(c):02x}" 49 | 50 | 51 | def decrypt_rc4(key, data): 52 | cipher = ARC4.new(key) 53 | return cipher.decrypt(data) 54 | 55 | 56 | def yara_scan(raw_data, rule_name): 57 | yara_rules = yara.compile(source=rule_source) 58 | matches = yara_rules.match(data=raw_data) 59 | for match in matches: 60 | if match.rule != "BitPaymer": 61 | continue 62 | 63 | for block in match.strings: 64 | for instance in block.instances: 65 | if block.identifier == rule_name: 66 | return {block.identifier: instance.offset} 67 | 68 | 69 | def extract_rdata(pe): 70 | for section in pe.sections: 71 | if ".rdata" in section.Name: 72 | return section.get_data(section.VirtualAddress, section.SizeOfRawData) 73 | return None 74 | 75 | 76 | def extract_config(file_data): 77 | pe = pefile.PE(data=file_data, fast_load=True) 78 | config = {} 79 | blobs = filter(None, [x.strip(b"\x00\x00\x00\x00") for x in extract_rdata(pe).split(b"\x00\x00\x00\x00")]) 80 | for blob in blobs: 81 | if len(blob) < LEN_BLOB_KEY: 82 | continue 83 | raw = decrypt_rc4(blob[:LEN_BLOB_KEY][::-1], blob[LEN_BLOB_KEY:]) 84 | if not raw: 85 | continue 86 | for item in raw.split(b"\x00"): 87 | data = "".join(convert_char(c) for c in item) 88 | if len(data) == 760: 89 | config.setdefault("cryptokey", data) 90 | # ToDO proper naming here 91 | config.setdefault("raw", {})["cryptokey_type"] = "RSA public key" 92 | 93 | elif len(data) > 1 and "\\x" not in data: 94 | config.setdefault("raw", {})["strings"] = data 95 | return config 96 | -------------------------------------------------------------------------------- /cape_parsers/utils/strings.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2015 Cuckoo Foundation, Optiv, Inc. (brad.spengler@optiv.com) 2 | # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 3 | # See the file 'docs/LICENSE' for copying permission. 4 | import logging 5 | from pathlib import Path 6 | 7 | try: 8 | import re2 as re 9 | 10 | HAVE_RE2 = True 11 | except ImportError: 12 | import re 13 | 14 | HAVE_RE2 = False 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | def bytes2str(convert): 21 | """Converts bytes to string 22 | @param convert: string as bytes. 23 | @return: string. 24 | """ 25 | if isinstance(convert, bytes): 26 | try: 27 | convert = convert.decode() 28 | except UnicodeDecodeError: 29 | convert = "".join(chr(_) for _ in convert) 30 | 31 | return convert 32 | 33 | if isinstance(convert, bytearray): 34 | try: 35 | convert = convert.decode() 36 | except UnicodeDecodeError: 37 | convert = "".join(chr(_) for _ in convert) 38 | 39 | return convert 40 | 41 | items = [] 42 | if isinstance(convert, dict): 43 | tmp_dict = {} 44 | items = convert.items() 45 | for k, v in items: 46 | if isinstance(v, bytes): 47 | try: 48 | tmp_dict[k] = v.decode() 49 | except UnicodeDecodeError: 50 | tmp_dict[k] = "".join(str(ord(_)) for _ in v) 51 | elif isinstance(v, str): 52 | tmp_dict[k] = v 53 | return tmp_dict 54 | elif isinstance(convert, list): 55 | converted_list = [] 56 | items = enumerate(convert) 57 | for k, v in items: 58 | if isinstance(v, bytes): 59 | try: 60 | converted_list.append(v.decode()) 61 | except UnicodeDecodeError: 62 | converted_list.append("".join(str(ord(_)) for _ in v)) 63 | 64 | return converted_list 65 | 66 | return convert 67 | 68 | 69 | def extract_strings(filepath: str = False, data: bytes = False, on_demand: bool = False, dedup: bool = False, minchars: int = 0): 70 | """Extract strings from analyzed file. 71 | @return: list of printable strings. 72 | """ 73 | 74 | nulltermonly = False 75 | if minchars == 0: 76 | minchars = 5 77 | 78 | if filepath: 79 | p = Path(filepath) 80 | if not p.exists(): 81 | log.error("Sample file doesn't exist: %s", filepath) 82 | return 83 | try: 84 | data = p.read_bytes() 85 | except (IOError, OSError) as e: 86 | log.error("Error reading file: %s", e) 87 | return 88 | 89 | if not data: 90 | return 91 | 92 | endlimit = b"8192" if not HAVE_RE2 else b"" 93 | if nulltermonly: 94 | apat = b"([\x20-\x7e]{" + str(minchars).encode() + b"," + endlimit + b"})\x00" 95 | upat = b"((?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + endlimit + b"})\x00\x00" 96 | else: 97 | apat = b"[\x20-\x7e]{" + str(minchars).encode() + b"," + endlimit + b"}" 98 | upat = b"(?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + endlimit + b"}" 99 | 100 | strings = [bytes2str(string) for string in re.findall(apat, data)] 101 | strings.extend(str(ws.decode("utf-16le")) for ws in re.findall(upat, data)) 102 | 103 | if dedup: 104 | strings = list(set(strings)) 105 | 106 | return strings 107 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/AuraStealer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import struct 3 | from contextlib import suppress 4 | from typing import Any, Dict, Tuple 5 | 6 | import pefile 7 | from Cryptodome.Cipher import AES 8 | from Cryptodome.Util.Padding import unpad 9 | 10 | # Define the format for the fixed-size header part. 11 | # < : little-endian 12 | # 32s : 32-byte string (for aes_key) 13 | # 16s : 16-byte string (for iv) 14 | # I : 4-byte unsigned int (for dword1) 15 | # I : 4-byte unsigned int (for dword2) 16 | HEADER_FORMAT = "<32s16sII" 17 | HEADER_SIZE = struct.calcsize(HEADER_FORMAT) # This will be 32 + 16 + 4 + 4 = 56 bytes 18 | 19 | def parse_blob(data: bytes): 20 | """ 21 | Parse the blob according to the scheme: 22 | - 32 bytes = AES key 23 | - Next 16 bytes = IV 24 | - Next 2 DWORDs (8 bytes total) = XOR to get cipher data size 25 | - Remaining bytes = cipher data of that size 26 | """ 27 | aes_key, iv, dword1, dword2 = struct.unpack_from(HEADER_FORMAT, data, 0) 28 | ciphertext_size = dword1 ^ dword2 29 | cipher_data = data[HEADER_SIZE : HEADER_SIZE + ciphertext_size] 30 | return aes_key, iv, cipher_data 31 | 32 | 33 | def decrypt(data: bytes) -> Tuple[bytes, bytes, bytes]: 34 | aes_key, iv, cipher_data = parse_blob(data) 35 | cipher = AES.new(aes_key, AES.MODE_CBC, iv) 36 | plaintext_padded = cipher.decrypt(cipher_data) 37 | return aes_key, iv, unpad(plaintext_padded, AES.block_size) 38 | 39 | 40 | def extract_config(data: bytes) -> Dict[str, Any]: 41 | cfg: Dict[str, Any] = {} 42 | plaintext = b"" 43 | data_section = None 44 | 45 | pe = pefile.PE(data=data, fast_load=True) 46 | for s in pe.sections: 47 | name = s.Name.decode("utf-8", errors="ignore").rstrip("\x00") 48 | if name in ("UPX1", ".data"): 49 | data_section = s 50 | break 51 | 52 | if data_section is None: 53 | return cfg 54 | 55 | data = data_section.get_data() 56 | block_size = 4096 57 | zeros = b"\x00" * block_size 58 | offset = data.find(zeros) 59 | if offset == -1: 60 | return cfg 61 | 62 | while offset > 0: 63 | with suppress(Exception): 64 | aes_key, iv, plaintext = decrypt(data[offset : offset + block_size]) 65 | if plaintext and b"conf" in plaintext: 66 | break 67 | 68 | offset -= 1 69 | 70 | if plaintext: 71 | try: 72 | parsed = json.loads(plaintext.decode("utf-8", errors="ignore").rstrip("\x00")) 73 | except json.JSONDecodeError: 74 | return cfg 75 | 76 | conf = parsed.get("conf", {}) 77 | build = parsed.get("build", {}) 78 | if conf: 79 | cfg = { 80 | "CNCs": conf.get("hosts"), 81 | "user_agent": conf.get("useragents"), 82 | "version": build.get("ver"), 83 | "build": build.get("build_id"), 84 | "cryptokey": aes_key.hex(), 85 | "cryptokey_type": "AES", 86 | "raw": { 87 | "iv": iv.hex(), 88 | "anti_vm": conf.get("anti_vm"), 89 | "anti_dbg": conf.get("anti_dbg"), 90 | "self_del": conf.get("self_del"), 91 | "run_delay": conf.get("run_delay"), 92 | } 93 | } 94 | 95 | return cfg 96 | 97 | 98 | if __name__ == "__main__": 99 | import sys 100 | 101 | with open(sys.argv[1], "rb") as f: 102 | print(extract_config(f.read())) 103 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/SquirrelWaffle.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Kevin O'Reilly (kevoreilly@gmail.com) 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | import struct 15 | from itertools import cycle 16 | 17 | import pefile 18 | 19 | import yara 20 | 21 | rule_source = """ 22 | rule SquirrelWaffle 23 | { 24 | strings: 25 | $config = {83 C2 04 83 C1 04 83 EE 04 73 EF 83 FE FC 74 34 8A 02 3A 01 75 27 83 FE FD 74 29 8A 42 01 3A 41 01 75 1A 83 FE FE 74 1C 8A 42 02 3A 41 02 75 0D} 26 | $decode = {F7 75 ?? 83 7D ?? 10 8D 4D ?? 8D 45 ?? C6 45 ?? 00 0F 43 4D ?? 83 7D ?? 10 0F 43 45 ?? 8A 04 10 32 04 39} 27 | $c2key = {83 EC 18 8B CC 89 A5 [4] 6A 05 C7 41 ?? 00 00 00 00 C7 41 ?? 0F 00 00 00 68} 28 | condition: 29 | uint16(0) == 0x5A4D and any of them 30 | } 31 | """ 32 | 33 | yara_rules = yara.compile(source=rule_source) 34 | 35 | MAX_STRING_SIZE = 32 36 | 37 | 38 | def string_from_offset(data, offset): 39 | return data[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 40 | 41 | 42 | def extract_rdata(pe): 43 | for section in pe.sections: 44 | if b".rdata" in section.Name: 45 | return section.get_data(section.VirtualAddress, section.SizeOfRawData) 46 | return None 47 | 48 | 49 | def xor_data(data, key): 50 | return bytes(c ^ k for c, k in zip(data, cycle(key))) 51 | 52 | 53 | def extract_config(data): 54 | config = {} 55 | pe = None 56 | try: 57 | pe = pefile.PE(data=data, fast_load=False) 58 | except Exception: 59 | return config 60 | 61 | if pe is not None: 62 | rdata = extract_rdata(pe) 63 | if len(rdata) == 0: 64 | return config 65 | chunks = [x for x in rdata.split(b"\x00") if x != b""] 66 | for i, line in enumerate(chunks): 67 | if len(line) > 100: 68 | try: 69 | decrypted = xor_data(line, chunks[i + 1]).decode() 70 | if "\r\n" in decrypted and "|" not in decrypted: 71 | config.setdefault("raw", {})["IP Blocklist"] = list(filter(None, decrypted.split("\r\n"))) 72 | elif "|" in decrypted and "." in decrypted and "\r\n" not in decrypted: 73 | config["CNCs"] = list(filter(None, decrypted.split("|"))) 74 | except Exception: 75 | continue 76 | matches = yara_rules.match(data=data) 77 | if not matches: 78 | return config 79 | for match in matches: 80 | if match.rule != "SquirrelWaffle": 81 | continue 82 | for item in match.strings: 83 | if "$c2key" in item.identifier: 84 | c2key_offset = item.instances[0].offset 85 | key_rva = struct.unpack("i", data[c2key_offset + 28 : c2key_offset + 32])[0] - pe.OPTIONAL_HEADER.ImageBase 86 | key_offset = pe.get_offset_from_rva(key_rva) 87 | config["cryptokey"] = string_from_offset(data, key_offset).decode() 88 | return config 89 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/BlackDropper.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 enzok 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | from datetime import datetime 16 | import re 17 | from contextlib import suppress 18 | 19 | import pefile 20 | 21 | 22 | def get_year(pe: pefile.PE) -> str: 23 | try: 24 | pe_timestamp = pe.FILE_HEADER.TimeDateStamp 25 | except AttributeError: 26 | return "" 27 | return datetime.fromtimestamp(pe_timestamp).strftime("%Y") 28 | 29 | 30 | def decrypt_string(encoded_string: str, key: str) -> str: 31 | encoded_bytes = bytearray.fromhex(encoded_string) 32 | key_bytes = bytearray(ord(char) for char in key) 33 | encoded_length = len(encoded_bytes) 34 | key_length = len(key_bytes) 35 | decoded_bytes = bytearray(encoded_length) 36 | 37 | for i in range(encoded_length): 38 | new_byte = (encoded_bytes[i] ^ key_bytes[i % key_length]) & 0xFF 39 | decoded_bytes[i] = new_byte 40 | 41 | decoded_string = decoded_bytes.decode("ascii", errors="ignore") 42 | 43 | return decoded_string 44 | 45 | 46 | def extract_config(data: bytes) -> dict: 47 | pe = pefile.PE(data=data, fast_load=True) 48 | rdata_section = None 49 | for section in pe.sections: 50 | if b".rdata" in section.Name: 51 | rdata_section = section 52 | break 53 | 54 | if not rdata_section: 55 | return {} 56 | 57 | rdata_data = rdata_section.get_data() 58 | patterns = [rb"Builder\.dll\x00", rb"Builder\.exe\x00"] 59 | matches = [] 60 | for pattern in patterns: 61 | matches.extend(re.finditer(pattern, rdata_data)) 62 | 63 | found_strings = set() 64 | for match in matches: 65 | start = max(0, match.start() - 1024) 66 | end = min(len(rdata_data), match.end() + 1024) 67 | found_strings.update(re.findall(b"[\x20-\x7E]{4,}?\x00", rdata_data[start:end])) 68 | 69 | config = {} 70 | urls = [] 71 | directories = [] 72 | campaign = "" 73 | 74 | if found_strings: 75 | key = get_year(pe) 76 | if not key: 77 | return {} 78 | for string in found_strings: 79 | with suppress(UnicodeDecodeError): 80 | decoded_string = string.decode("utf-8").rstrip("\x00") 81 | 82 | if re.match(r"^[0-9A-Fa-f]+$", decoded_string): 83 | url = decrypt_string(decoded_string, key) 84 | if url: 85 | urls.append(url) 86 | elif decoded_string.count("\\") > 1: 87 | directories.append(decoded_string) 88 | elif re.match(r"^(?![A-Z]{6,}$)[a-zA-Z0-9\-=]{6,}$", decoded_string): 89 | campaign = decoded_string 90 | 91 | if urls: 92 | config["CNCs"] = sorted(urls) 93 | if campaign: 94 | config["campaign"] = campaign 95 | if directories: 96 | config["raw"] = {"directories": directories} 97 | 98 | return config 99 | 100 | 101 | if __name__ == "__main__": 102 | import sys 103 | 104 | with open(sys.argv[1], "rb") as f: 105 | print(extract_config(f.read())) 106 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Greame.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | import pefile 4 | 5 | 6 | def get_config(data): 7 | try: 8 | pe = pefile.PE(data=data) 9 | rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"]) 10 | rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx] 11 | for entry in rt_string_directory.directory.entries: 12 | if str(entry.name) == "GREAME": 13 | data_rva = entry.directory.entries[0].data.struct.OffsetToData 14 | size = entry.directory.entries[0].data.struct.Size 15 | data = pe.get_memory_mapped_image()[data_rva : data_rva + size] 16 | return data.split("####@####") 17 | except Exception: 18 | return None 19 | 20 | 21 | def xor_decode(data): 22 | key = 0xBC 23 | encoded = bytearray(data) 24 | for i in range(len(encoded)): 25 | encoded[i] ^= key 26 | return [x for x in str(encoded) if x in string.printable] 27 | 28 | 29 | def parse_config(raw_config): 30 | if len(raw_config) <= 20: 31 | return None 32 | domains = "" 33 | ports = "" 34 | # Config sections 0 - 19 contain a list of Domains and Ports 35 | for x in range(19): 36 | if len(raw_config[x]) > 1: 37 | domains += xor_decode(raw_config[x]).split(":", 1)[0] 38 | domains += "|" 39 | ports += xor_decode(raw_config[x]).split(":", 2)[1] 40 | ports += "|" 41 | config_dict = { 42 | "Domain": domains[:-1], 43 | "Port": ports[:-1], 44 | "ServerID": xor_decode(raw_config[20]), 45 | "Password": xor_decode(raw_config[21]), 46 | "Install Flag": xor_decode(raw_config[22]), 47 | "Install Directory": xor_decode(raw_config[25]), 48 | "Install File Name": xor_decode(raw_config[26]), 49 | "Active X Startup": xor_decode(raw_config[27]), 50 | "REG Key HKLM": xor_decode(raw_config[28]), 51 | "REG Key HKCU": xor_decode(raw_config[29]), 52 | "Enable Message Box": xor_decode(raw_config[30]), 53 | "Message Box Icon": xor_decode(raw_config[31]), 54 | "Message Box Button": xor_decode(raw_config[32]), 55 | "Install Message Title": xor_decode(raw_config[33]), 56 | "Install Message Box": xor_decode(raw_config[34]).replace("\r\n", " "), 57 | "Activate Keylogger": xor_decode(raw_config[35]), 58 | "Keylogger Backspace = Delete": xor_decode(raw_config[36]), 59 | "Keylogger Enable FTP": xor_decode(raw_config[37]), 60 | "FTP Address": xor_decode(raw_config[38]), 61 | "FTP Directory": xor_decode(raw_config[39]), 62 | "FTP UserName": xor_decode(raw_config[41]), 63 | "FTP Password": xor_decode(raw_config[42]), 64 | "FTP Port": xor_decode(raw_config[43]), 65 | "FTP Interval": xor_decode(raw_config[44]), 66 | "Persistance": xor_decode(raw_config[59]), 67 | "Hide File": xor_decode(raw_config[60]), 68 | "Change Creation Date": xor_decode(raw_config[61]), 69 | "Mutex": xor_decode(raw_config[62]), 70 | "Melt File": xor_decode(raw_config[63]), 71 | "Startup Policies": xor_decode(raw_config[69]), 72 | "USB Spread": xor_decode(raw_config[70]), 73 | "P2P Spread": xor_decode(raw_config[71]), 74 | "Google Chrome Passwords": xor_decode(raw_config[73]), 75 | } 76 | if xor_decode(raw_config[57]) == 0: 77 | config_dict["Process Injection"] = "Disabled" 78 | elif xor_decode(raw_config[57]) == 1: 79 | config_dict["Process Injection"] = "Default Browser" 80 | elif xor_decode(raw_config[57]) == 2: 81 | config_dict["Process Injection"] = xor_decode(raw_config[58]) 82 | else: 83 | config_dict["Process Injection"] = "None" 84 | return config_dict 85 | 86 | 87 | def extract_config(data): 88 | raw_config = get_config(data) 89 | if raw_config: 90 | config = parse_config(raw_config) 91 | if config: 92 | return {"raw": config} 93 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/AdaptixBeacon.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from contextlib import suppress 4 | 5 | import pefile 6 | from Cryptodome.Cipher import ARC4 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | DESCRIPTION = "Adaptix beacon configuration parser." 11 | AUTHOR = "enzok" 12 | 13 | 14 | def parse_http_config(rc4_key: bytes, data: bytes) -> dict: 15 | config = {} 16 | offset = 0 17 | servers = [] 18 | ports = [] 19 | 20 | def read(fmt: str): 21 | nonlocal offset 22 | size = struct.calcsize(fmt) 23 | value = struct.unpack_from(fmt, data, offset) 24 | offset += size 25 | return value if len(value) > 1 else value[0] 26 | 27 | def read_str(length: int): 28 | nonlocal offset 29 | value = data[offset : offset + length].decode("utf-8", errors="replace") 30 | offset += length 31 | return value 32 | 33 | config["cryptokey"] = rc4_key.hex() 34 | config["cryptokey_type"] = "RC4" 35 | config["agent_type"] = f"{read(' dict: 78 | pe = pefile.PE(data=filebuf, fast_load=True) 79 | data_sections = [s for s in pe.sections if b".rdata" in s.Name] 80 | if not data_sections: 81 | return 82 | 83 | data = data_sections[0].get_data() 84 | data_len = len(data) 85 | pos = 0 86 | while pos + 4 <= data_len: 87 | start_offset = pos 88 | key_offset = struct.unpack_from(" data_len: 92 | pos = start_offset + 1 93 | continue 94 | 95 | encrypted_data = data[pos : pos + key_offset] 96 | pos += key_offset 97 | rc4_key = data[pos : pos + 16] 98 | 99 | if key_offset == 787: 100 | pass 101 | 102 | with suppress(Exception): 103 | decrypted = ARC4.new(rc4_key).decrypt(encrypted_data) 104 | if b"User-Agent" in decrypted: 105 | return parse_http_config(rc4_key, decrypted) 106 | 107 | pos = start_offset + 1 108 | 109 | return None 110 | 111 | 112 | if __name__ == "__main__": 113 | import sys 114 | 115 | with open(sys.argv[1], "rb") as f: 116 | print(extract_config(f.read())) 117 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/IcedID.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Kevin O'Reilly (kevoreilly@gmail.com) 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | # 15 | # Updates to handle stage 1 Based on initial work referenced here and modified to work with python3 16 | # https://sysopfb.github.io/malware,/icedid/2020/04/28/IcedIDs-updated-photoloader.html 17 | # https://gist.github.com/sysopfb/93eb0090ef47c08e4e516cb045b48b96 18 | # https://www.group-ib.com/blog/icedid 19 | 20 | import logging 21 | import struct 22 | 23 | import yara 24 | import pefile 25 | from Cryptodome.Cipher import ARC4 26 | 27 | 28 | log = logging.getLogger(__name__) 29 | DESCRIPTION = "IcedID Stage 2 configuration parser." 30 | AUTHOR = "kevoreilly,threathive,sysopfb" 31 | 32 | yara_rules = yara.compile( 33 | source=""" 34 | rule IcedID 35 | { 36 | meta: 37 | author = "kevoreilly, threathive" 38 | description = "IcedID Payload" 39 | cape_type = "IcedID Payload" 40 | strings: 41 | $crypt1 = {8A 04 ?? D1 C? F7 D? D1 C? 81 E? 20 01 00 00 D1 C? F7 D? 81 E? 01 91 00 00 32 C? 88} 42 | $crypt2 = {8B 44 24 04 D1 C8 F7 D0 D1 C8 2D 20 01 00 00 D1 C0 F7 D0 2D 01 91 00 00 C3} 43 | $crypt3 = {41 00 8B C8 C1 E1 08 0F B6 C4 66 33 C8 66 89 4? 24 A1 ?? ?? 41 00 89 4? 20 A0 ?? ?? 41 00 D0 E8 32 4? 32} 44 | $download1 = {8D 44 24 40 50 8D 84 24 44 03 00 00 68 04 21 40 00 50 FF D5 8D 84 24 4C 01 00 00 C7 44 24 28 01 00 00 00 89 44 24 1C 8D 4C 24 1C 8D 84 24 4C 03 00 00 83 C4 0C 89 44 24 14 8B D3 B8 BB 01 00 00 66 89 44 24 18 57} 45 | $download2 = {8B 75 ?? 8D 4D ?? 8B 7D ?? 8B D6 57 89 1E 89 1F E8 [4] 59 3D C8 00 00 00 75 05 33 C0 40 EB} 46 | $major_ver = {0F B6 05 ?? ?? ?? ?? 6A ?? 6A 72 FF 75 0C 6A 70 50 FF 35 ?? ?? ?? ?? 8D 45 80 FF 35 ?? ?? ?? ?? 6A 63 FF 75 08 6A 67 50 FF 75 10 FF 15 ?? ?? ?? ?? 83 C4 38 8B E5 5D C3} 47 | $stage_2_request_binary = "id=" 48 | $stage_2_request_img = ".png" 49 | condition: 50 | any of ($crypt*, $download*, $major_ver) and all of ($stage_2_request_*) 51 | } 52 | 53 | """ 54 | ) 55 | 56 | 57 | def yara_scan(raw_data): 58 | try: 59 | return yara_rules.match(data=raw_data) 60 | except Exception as e: 61 | print(e) 62 | 63 | 64 | def extract_config(filebuf): 65 | yara_hit = yara_scan(filebuf) 66 | 67 | for hit in yara_hit: 68 | if hit.rule == "IcedID": # can be either a dll or a exe 69 | enc_data = None 70 | try: 71 | pe = pefile.PE(data=filebuf, fast_load=True) 72 | for section in pe.sections: 73 | if section.Name == b".data\x00\x00\x00": 74 | enc_data = section.get_data() 75 | key = enc_data[:8] 76 | enc_config = enc_data[8:592] 77 | decrypted_data = ARC4.new(key).decrypt(enc_config) 78 | config = list(filter(None, decrypted_data.split(b"\x00"))) 79 | return { 80 | "version": str(struct.unpack("I", decrypted_data[4:8])[0]), 81 | "botnet": str(struct.unpack("I", decrypted_data[:4])[0]), 82 | "raw": { 83 | "family": "IcedID", 84 | "paths": [{"path": config[1].decode(), "usage": "other"}], 85 | "http": [{"uri": controller[1:].decode()} for controller in config[2:]], 86 | }, 87 | } 88 | except Exception as e: 89 | log.error("Error: %s", e) 90 | 91 | return {} 92 | 93 | 94 | if __name__ == "__main__": 95 | import sys 96 | 97 | with open(sys.argv[1], "rb") as f: 98 | print(extract_config(f.read())) 99 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/WarzoneRAT.py: -------------------------------------------------------------------------------- 1 | # This program is free software: you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation, either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | import struct 15 | from contextlib import suppress 16 | 17 | import pefile 18 | 19 | DESCRIPTION = "WarzoneRAT configuration extractor." 20 | AUTHOR = "enzo" 21 | 22 | 23 | def ksa(key: bytearray) -> bytearray: 24 | sbox = bytearray(256) 25 | for i in range(256): 26 | sbox[i] = i 27 | 28 | j = 0 29 | for i in range(256): 30 | j = (j + key[i % 250] + sbox[i]) & 0xFF 31 | sbox[i] ^= sbox[j] & 0xFF 32 | sbox[j] ^= sbox[i] & 0xFF 33 | sbox[i] ^= sbox[j] & 0xFF 34 | return sbox 35 | 36 | 37 | def decrypt(sbox: bytearray, src_buf: bytearray) -> bytes: 38 | i, j, k = 0, 0, 0 39 | dst_buf = bytearray(len(src_buf)) 40 | 41 | while k < len(src_buf): 42 | i += 1 43 | uc = sbox[i % 256] & 0xFF 44 | c = uc - 256 if uc > 127 else uc 45 | j = j + c - 256 if j + c > 256 else j + c 46 | d = sbox[j % 256] 47 | sbox[i % 256] = d 48 | sbox[j % 256] = uc 49 | e1 = (i >> 3) ^ (32 * j) 50 | e = sbox[e1 % 256] 51 | g1 = ((int.from_bytes(struct.pack(">i", j), "big") >> 3) ^ (32 * i)) & 0xFF 52 | g2 = sbox[g1 % 256] 53 | g = (e + g2) & 0xFF 54 | e = sbox[(j + d) % 256] 55 | h = sbox[(g ^ 0xAA) % 256] 56 | xor_key = (e ^ (h + sbox[(d + uc) % 256])) & 0xFF 57 | dst_buf[k] = src_buf[k] ^ xor_key 58 | i += 1 59 | k += 1 60 | 61 | return bytes(dst_buf) 62 | 63 | 64 | def extract_bss_data(pe): 65 | for section in pe.sections: 66 | if b".bss" in section.Name: 67 | return section.get_data(section.VirtualAddress, section.SizeOfRawData) 68 | return None 69 | 70 | 71 | def extract_config(data): 72 | cfg = {} 73 | pe = None 74 | with suppress(Exception): 75 | pe = pefile.PE(data=data, fast_load=False) 76 | if not pe: 77 | return 78 | try: 79 | key = bytearray(250) 80 | bss_data = extract_bss_data(pe) 81 | if not bss_data: 82 | return cfg 83 | key_size = struct.unpack("i", bss_data[:4])[0] 84 | key_bytes = bss_data[4 : 4 + key_size] 85 | for k in range(len(key_bytes)): 86 | key[k] = key_bytes[k] 87 | etxt = bss_data[4 + key_size : 260 + key_size] 88 | dtxt = decrypt(ksa(key), bytearray(etxt)) 89 | 90 | offset = 4 91 | c2_size = struct.unpack("i", dtxt[:offset])[0] 92 | c2_host = dtxt[offset : offset + c2_size].decode("utf-16") 93 | offset += c2_size 94 | c2_port = struct.unpack("H", dtxt[offset : offset + 2])[0] 95 | # ToDo missed schema 96 | cfg["CNCs"] = [f"{c2_host}:{c2_port}"] 97 | offset += 2 98 | # unk1 = dtxt[offset : offset + 7] 99 | offset += 7 100 | unk2_size = struct.unpack("i", dtxt[offset : offset + 4])[0] 101 | offset += 4 102 | # unk2 = dtxt[offset : offset + unk2_size] 103 | offset += unk2_size 104 | # unk3 = dtxt[offset : offset + 2] 105 | offset += 2 106 | runkey_size = struct.unpack("i", dtxt[offset : offset + 4])[0] 107 | offset += 4 108 | cfg.setdefault("raw", {})["Run Key Name"] = dtxt[offset : offset + runkey_size].decode("utf-16") 109 | except struct.error: 110 | # there is a lot of failed data validation muting it 111 | return 112 | except Exception as e: 113 | print("warzone", e) 114 | 115 | return cfg 116 | 117 | 118 | if __name__ == "__main__": 119 | import sys 120 | from pathlib import Path 121 | 122 | data = Path(sys.argv[1]).read_bytes() 123 | print(extract_config(data)) 124 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/Enfal.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Kevin O'Reilly kevin.oreilly@contextis.co.uk 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | DESCRIPTION = "Enfal configuration parser." 16 | AUTHOR = "kevoreilly" 17 | 18 | import yara 19 | 20 | rule_source = """ 21 | rule Enfal 22 | { 23 | meta: 24 | author = "kev" 25 | description = "Enfal configuration blob" 26 | cape_type = "Enfal Config" 27 | strings: 28 | $config = {BF 49 ?? 75 22 12 ?? 75 4B 65 72 6E 65 6C 33 32 2E 64 6C 6C} 29 | 30 | condition: 31 | $config 32 | } 33 | """ 34 | 35 | MAX_STRING_SIZE = 128 36 | 37 | 38 | def yara_scan(raw_data, rule_name): 39 | addresses = {} 40 | yara_rules = yara.compile(source=rule_source) 41 | matches = yara_rules.match(data=raw_data) 42 | for match in matches: 43 | if match.rule == "Enfal": 44 | for item in match.strings: 45 | if item.identifier == rule_name: 46 | addresses[item.identifier] = item.instances[0].offset 47 | return addresses 48 | 49 | 50 | def string_from_offset(data, offset): 51 | return data[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 52 | 53 | 54 | def list_from_offset(data, offset): 55 | string = data[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 56 | return string.split(b",") 57 | 58 | 59 | def extract_config(filebuf): 60 | config = yara_scan(filebuf, "$config") 61 | return_conf = {} 62 | if config: 63 | yara_offset = int(config["$config"]) 64 | 65 | c2_address = string_from_offset(filebuf, yara_offset + 0x2E8) 66 | if c2_address: 67 | return_conf["CNCs"] = c2_address 68 | 69 | c2_url = string_from_offset(filebuf, yara_offset + 0xE8) 70 | if c2_url: 71 | return_conf["c2_url"] = c2_url 72 | 73 | if filebuf[yara_offset + 0x13B0 : yara_offset + 0x13B1] == "S": 74 | registrypath = string_from_offset(filebuf, yara_offset + 0x13B0) 75 | elif filebuf[yara_offset + 0x13C0 : yara_offset + 0x13C1] == "S": 76 | registrypath = string_from_offset(filebuf, yara_offset + 0x13C0) 77 | elif filebuf[yara_offset + 0x13D0 : yara_offset + 0x13D1] == "S": 78 | registrypath = string_from_offset(filebuf, yara_offset + 0x13D0) 79 | else: 80 | registrypath = "" 81 | 82 | if registrypath: 83 | return_conf["registrypath"] = registrypath 84 | 85 | if filebuf[yara_offset + 0x14A2 : yara_offset + 0x14A3] == "C": 86 | servicename = "" 87 | filepaths = list_from_offset(filebuf, yara_offset + 0x14A2) 88 | filepaths[0] = filepaths[0].split(b" ", 1)[0] 89 | elif filebuf[yara_offset + 0x14B0 : yara_offset + 0x14B1] != "\0": 90 | servicename = string_from_offset(filebuf, yara_offset + 0x14B0) 91 | filepaths = list_from_offset(filebuf, yara_offset + 0x14C0) 92 | elif filebuf[yara_offset + 0x14C0 : yara_offset + 0x14C1] != "\0": 93 | servicename = string_from_offset(filebuf, yara_offset + 0x14C0) 94 | filepaths = list_from_offset(filebuf, yara_offset + 0x14D0) 95 | elif filebuf[yara_offset + 0x14D0 : yara_offset + 0x14D1] != "\0": 96 | servicename = string_from_offset(filebuf, yara_offset + 0x14D0) 97 | filepaths = list_from_offset(filebuf, yara_offset + 0x14E0) 98 | else: 99 | servicename = "" 100 | filepaths = [] 101 | 102 | if servicename: 103 | return_conf["servicename"] = servicename 104 | if filepaths: 105 | for path in filepaths: 106 | return_conf.setdefault("filepath", []).append(path) 107 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/Arkei.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import pefile 3 | import yara 4 | from contextlib import suppress 5 | 6 | # Hash = 69ba4e2995d6b11bb319d7373d150560ea295c02773fe5aa9c729bfd2c334e1e 7 | 8 | RULE_SOURCE = """rule Arkei 9 | { 10 | meta: 11 | author = "Yung Binary" 12 | strings: 13 | $decode_1 = { 14 | 6A ?? 15 | 68 ?? ?? ?? ?? 16 | 68 ?? ?? ?? ?? 17 | E8 ?? ?? ?? ?? 18 | } 19 | $decode_2 = { 20 | 6A ?? 21 | 68 ?? ?? ?? ?? 22 | 68 ?? ?? ?? ?? 23 | [0-5] 24 | E8 ?? ?? ?? ?? 25 | } 26 | condition: 27 | any of them 28 | }""" 29 | 30 | 31 | def yara_scan(raw_data): 32 | yara_rules = yara.compile(source=RULE_SOURCE) 33 | matches = yara_rules.match(data=raw_data) 34 | 35 | for match in matches: 36 | for block in match.strings: 37 | for instance in block.instances: 38 | yield block.identifier, instance.offset 39 | 40 | 41 | def xor_data(data, key): 42 | decoded = bytearray() 43 | for i in range(len(data)): 44 | decoded.append(data[i] ^ key[i]) 45 | return decoded 46 | 47 | 48 | def extract_config(data): 49 | config = {} 50 | 51 | # Attempt to extract via old method 52 | with suppress(Exception): 53 | domain = "" 54 | uri = "" 55 | lines = data.decode().split("\n") 56 | for line in lines: 57 | if line.startswith("http") and "://" in line: 58 | domain = line 59 | if line.startswith("/") and line[-4] == ".": 60 | uri = line 61 | if domain and uri: 62 | config.setdefault("CNCs", []).append(f"{domain}{uri}") 63 | return config 64 | 65 | # Try with new method 66 | 67 | # config_dict["Strings"] = [] 68 | pe = pefile.PE(data=data, fast_load=True) 69 | image_base = pe.OPTIONAL_HEADER.ImageBase 70 | domain = "" 71 | uri = "" 72 | botnet_id = "" 73 | last_str = "" 74 | for match in yara_scan(data): 75 | try: 76 | rule_str_name, str_decode_offset = match 77 | str_size = int(data[str_decode_offset + 1]) 78 | # Ignore size 0 strings 79 | if not str_size: 80 | continue 81 | 82 | if rule_str_name.startswith("$decode"): 83 | key_rva = data[str_decode_offset + 3 : str_decode_offset + 7] 84 | encoded_str_rva = data[str_decode_offset + 8 : str_decode_offset + 12] 85 | # dword_rva = data[str_decode_offset + 21 : str_decode_offset + 25] 86 | 87 | key_offset = pe.get_offset_from_rva(struct.unpack("i", key_rva)[0] - image_base) 88 | encoded_str_offset = pe.get_offset_from_rva(struct.unpack("i", encoded_str_rva)[0] - image_base) 89 | # dword_offset = struct.unpack("i", dword_rva)[0] 90 | # dword_name = f"dword_{hex(dword_offset)[2:]}" 91 | 92 | key = data[key_offset : key_offset + str_size] 93 | encoded_str = data[encoded_str_offset : encoded_str_offset + str_size] 94 | decoded_str = xor_data(encoded_str, key).decode() 95 | # config_dict["Strings"].append({dword_name : decoded_str}) 96 | 97 | if last_str in ("http://", "https://"): 98 | domain += decoded_str 99 | elif decoded_str in ("http://", "https://"): 100 | domain = decoded_str 101 | elif "http" in decoded_str and "://" in decoded_str: 102 | domain = decoded_str 103 | elif uri == "" and decoded_str.startswith("/") and decoded_str[-4] == ".": 104 | uri = decoded_str 105 | elif last_str.startswith("/") and last_str[-4] == ".": 106 | botnet_id = decoded_str 107 | break 108 | 109 | last_str = decoded_str 110 | 111 | except Exception: 112 | continue 113 | 114 | if domain and uri: 115 | config.setdefault("CNCs", []).append(f"{domain}{uri}") 116 | 117 | if botnet_id: 118 | config.setdefault("botnet", botnet_id) 119 | 120 | return config 121 | 122 | 123 | if __name__ == "__main__": 124 | import sys 125 | 126 | with open(sys.argv[1], "rb") as f: 127 | print(extract_config(f.read())) 128 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/core/Quickbind.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import struct 4 | from contextlib import suppress 5 | 6 | import pefile 7 | from Cryptodome.Cipher import ARC4 8 | 9 | log = logging.getLogger(__name__) 10 | log.setLevel(logging.INFO) 11 | 12 | 13 | def is_hex(hex_string): 14 | if len(hex_string) % 2 != 0: 15 | return False 16 | 17 | if not re.fullmatch(r"[0-9a-fA-F]+", hex_string): 18 | return False 19 | 20 | return True 21 | 22 | 23 | def extract_config(filebuf): 24 | cfg = {} 25 | pe = pefile.PE(data=filebuf, fast_load=True) 26 | 27 | section_data = { 28 | "data": "", 29 | "rdata": "", 30 | } 31 | 32 | data_section = [s for s in pe.sections if s.Name.find(b".data") != -1][0] 33 | rdata_section = [s for s in pe.sections if s.Name.find(b".rdata") != -1][0] 34 | 35 | if data_section: 36 | section_data["data"] = data_section.get_data() 37 | 38 | if rdata_section: 39 | section_data["rdata"] = rdata_section.get_data() 40 | 41 | entries = [] 42 | 43 | for section in section_data: 44 | data = section_data[section] 45 | offset = 0 46 | 47 | while offset < len(data): 48 | decrypted_result = "" 49 | if offset + 8 > len(data): 50 | break 51 | size, key = struct.unpack_from("I4s", data, offset) 52 | if b"\x00\x00\x00" in key or size > 256 or size == 0: 53 | offset += 1 54 | continue 55 | offset += 8 56 | data_format = f"{size}s" 57 | encrypted_string = struct.unpack_from(data_format, data, offset)[0] 58 | 59 | with suppress(IndexError, UnicodeDecodeError, ValueError): 60 | decrypted_result = ARC4.new(key).decrypt(encrypted_string).replace(b"\x00", b"").decode("utf-8") 61 | 62 | if decrypted_result and all(32 <= ord(char) <= 127 for char in decrypted_result): 63 | if len(decrypted_result) > 2: 64 | entries.append(decrypted_result) 65 | offset += size 66 | pad_start = offset 67 | pad_end = pad_start 68 | while pad_end < len(data) and data[pad_end] == 0: 69 | pad_end += 1 70 | padding = pad_end - pad_start 71 | offset += padding 72 | else: 73 | offset += 1 74 | 75 | if entries: 76 | c2s = [] 77 | mutexes = [] 78 | campaign = entries[0] 79 | campaign_found = False 80 | known_campaigns = ( 81 | "aws", 82 | "adobe.com", 83 | "traf", 84 | ) 85 | 86 | for i, item in enumerate(entries): 87 | if item.count(".") == 3 and re.fullmatch(r"\d+", item.replace(".", "")): 88 | c2s.append(item) 89 | if i == 1: 90 | campaign_found = True 91 | 92 | elif "http" in item: 93 | c2s.append(item) 94 | if i == 1: 95 | campaign_found = True 96 | 97 | elif item.count("-") == 4 and "{" not in item: 98 | mutexes.append(item) 99 | if i == 1: 100 | campaign_found = True 101 | 102 | elif is_hex(item): 103 | cfg["cryptokey"] = item 104 | cfg["cryptokey_type"] = "RC4" 105 | if i == 1: 106 | campaign_found = True 107 | 108 | elif "Mozilla" in item: 109 | cfg["user_agent"] = item 110 | if i == 1: 111 | campaign_found = True 112 | 113 | if item in known_campaigns: 114 | campaign = item 115 | campaign_found = True 116 | 117 | if campaign_found: 118 | cfg["campaign"] = campaign 119 | 120 | if c2s: 121 | cfg["CNCs"] = [f"http://{c2}" for c2 in c2s] 122 | 123 | if mutexes: 124 | mutexes = list(set(mutexes)) 125 | cfg["mutex"] = mutexes[0] if len(mutexes) == 1 else mutexes 126 | 127 | return cfg 128 | 129 | 130 | if __name__ == "__main__": 131 | import sys 132 | from pathlib import Path 133 | 134 | log.setLevel(logging.DEBUG) 135 | data = Path(sys.argv[1]).read_bytes() 136 | print(extract_config(data)) 137 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/SmallNet.py: -------------------------------------------------------------------------------- 1 | def ver_52(data): 2 | config_parts = data.split("!!<3SAFIA<3!!") 3 | config_dict = { 4 | "Domain": config_parts[1], 5 | "Port": config_parts[2], 6 | "Disbale Registry": config_parts[3], 7 | "Disbale TaskManager": config_parts[4], 8 | "Install Server": config_parts[5], 9 | "Registry Key": config_parts[8], 10 | "Install Name": config_parts[9], 11 | "Disbale UAC": config_parts[10], 12 | "Anti-Sandboxie": config_parts[13], 13 | "Anti-Anubis": config_parts[14], 14 | "Anti-VirtualBox": config_parts[15], 15 | "Anti-VmWare": config_parts[16], 16 | "Anti-VirtualPC": config_parts[17], 17 | "ServerID": config_parts[18], 18 | "USB Spread": config_parts[19], 19 | "P2P Spread": config_parts[20], 20 | "RAR Spread": config_parts[21], 21 | "MSN Spread": config_parts[22], 22 | "Yahoo Spread": config_parts[23], 23 | "LAN Spread": config_parts[24], 24 | "Disbale Firewall": config_parts[25], 25 | "Delay Execution MiliSeconds": config_parts[26], 26 | "Attribute Read Only": config_parts[27], 27 | "Attribute System File": config_parts[28], 28 | "Attribute Hidden": config_parts[29], 29 | "Attribute Compressed": config_parts[30], 30 | "Attribute Temporary": config_parts[31], 31 | "Attribute Archive": config_parts[32], 32 | "Modify Creation Date": config_parts[33], 33 | "Modified Creation Data": config_parts[34], 34 | "Thread Persistance": config_parts[35], 35 | "Anti-ZoneAlarm": config_parts[36], 36 | "Anti-SpyTheSpy": config_parts[37], 37 | "Anti-NetStat": config_parts[38], 38 | "Anti-TiGeRFirewall": config_parts[39], 39 | "Anti-TCPview": config_parts[40], 40 | "Anti-CurrentPorts": config_parts[41], 41 | "Anti-RogueKiller": config_parts[42], 42 | "Enable MessageBox": config_parts[43], 43 | "MessageBox Message": config_parts[44], 44 | "MessageBox Icon": config_parts[45], 45 | "MessageBox Buttons": config_parts[46], 46 | "MessageBox Title": config_parts[47], 47 | } 48 | 49 | if config_parts[6] == 1: 50 | config_dict["Install Path"] = "Temp" 51 | if config_parts[7] == 1: 52 | config_dict["Install Path"] = "Windows" 53 | if config_parts[11] == 1: 54 | config_dict["Install Path"] = "System32" 55 | if config_parts[12] == 1: 56 | config_dict["Install Path"] = "Program Files" 57 | return config_dict 58 | 59 | 60 | def ver_5(data): 61 | config_parts = data.split("!!ElMattadorDz!!") 62 | config_dict = { 63 | "Domain": config_parts[1], 64 | "Port": config_parts[2], 65 | "Disable Registry": config_parts[3], 66 | "Disbale TaskManager": config_parts[4], 67 | "Install Server": config_parts[5], 68 | "Registry Key": config_parts[8], 69 | "Install Name": config_parts[9], 70 | "Disbale UAC": config_parts[10], 71 | "Anti-Sandboxie": config_parts[13], 72 | "Anti-Anubis": config_parts[14], 73 | "Anti-VirtualBox": config_parts[15], 74 | "Anti-VmWare": config_parts[16], 75 | "Anti-VirtualPC": config_parts[17], 76 | "ServerID": config_parts[18], 77 | "USB Spread": config_parts[19], 78 | "P2P Spread": config_parts[20], 79 | "RAR Spread": config_parts[21], 80 | "MSN Spread": config_parts[22], 81 | "Yahoo Spread": config_parts[23], 82 | "LAN Spread": config_parts[24], 83 | "Disbale Firewall": config_parts[25], 84 | "Delay Execution MiliSeconds": config_parts[26], 85 | } 86 | 87 | if config_parts[6] == 1: 88 | config_dict["Install Path"] = "Temp" 89 | if config_parts[7] == 1: 90 | config_dict["Install Path"] = "Windows" 91 | if config_parts[11] == 1: 92 | config_dict["Install Path"] = "System32" 93 | if config_parts[12] == 1: 94 | config_dict["Install Path"] = "Program Files" 95 | return [config_dict, [config_dict["Domain"]]] 96 | 97 | 98 | def extract_config(data): 99 | config = {} 100 | if "!!<3SAFIA<3!!" in data: 101 | config = ver_52(data) 102 | 103 | elif "!!ElMattadorDz!!" in data: 104 | config = ver_5(data) 105 | 106 | if config: 107 | return {"raw": config} 108 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/RedLeaf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Kevin O'Reilly kevin.oreilly@contextis.co.uk 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | DESCRIPTION = "RedLeaf configuration parser." 16 | AUTHOR = "kevoreilly" 17 | 18 | import struct 19 | import pefile 20 | import yara 21 | 22 | rule_source = """ 23 | rule RedLeaf 24 | { 25 | meta: 26 | author = "kev" 27 | description = "RedLeaf configuration parser." 28 | cape_type = "RedLeaf Payload" 29 | strings: 30 | $crypto = {6A 10 B8 ?? ?? ?? 10 E8 ?? ?? 01 00 8B F1 89 75 E4 8B 7D 08 83 CF 07 81 FF FE FF FF 7F 76 05 8B 7D 08 EB 29 8B 4E 14 89 4D EC D1 6D EC 8B C7 33 D2 6A 03 5B F7 F3 8B 55 EC 3B D0 76 10 BF FE FF FF} 31 | $decrypt_config = {55 8B EC 83 EC 20 A1 98 9F 03 10 33 C5 89 45 FC 56 33 F6 33 C0 80 B0 ?? ?? ?? ?? ?? 40 3D ?? ?? ?? ?? 72 F1 68 70 99 03 10 56 56 FF 15 2C 11 03 10 FF 15 B8 11 03 10 3D B7 00 00 00 75 06 56 E8 5F 9E} 32 | condition: 33 | //check for MZ Signature at offset 0 34 | uint16(0) == 0x5A4D 35 | 36 | and 37 | 38 | $crypto and $decrypt_config 39 | } 40 | """ 41 | 42 | MAX_STRING_SIZE = 64 43 | MAX_IP_STRING_SIZE = 16 # aaa.bbb.ccc.ddd\0 44 | 45 | 46 | def yara_scan(raw_data, rule_name): 47 | addresses = {} 48 | yara_rules = yara.compile(source=rule_source) 49 | matches = yara_rules.match(data=raw_data) 50 | for match in matches: 51 | if match.rule == "RedLeaf": 52 | for item in match.strings: 53 | if item.identifier == rule_name: 54 | addresses[item.identifier] = item.instances[0].offset 55 | return addresses 56 | 57 | 58 | def pe_data(pe, va, size): 59 | image_base = pe.OPTIONAL_HEADER.ImageBase 60 | rva = va - image_base 61 | return pe.get_data(rva, size) 62 | 63 | 64 | def string_from_offset(buffer, offset): 65 | return buffer[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 66 | 67 | 68 | def unicode_string_from_offset(buffer, offset): 69 | return buffer[offset : offset + MAX_STRING_SIZE].split(b"\x00\x00", 1)[0] 70 | 71 | 72 | def extract_config(filebuf): 73 | pe = pefile.PE(data=filebuf, fast_load=False) 74 | image_base = pe.OPTIONAL_HEADER.ImageBase 75 | 76 | decrypt_config = yara_scan(filebuf, "$decrypt_config") 77 | 78 | if decrypt_config: 79 | yara_offset = int(decrypt_config["$decrypt_config"]) 80 | else: 81 | return 82 | 83 | config_rva = struct.unpack("i", filebuf[yara_offset + 23 : yara_offset + 27])[0] - image_base 84 | config_offset = pe.get_offset_from_rva(config_rva) 85 | xor_key = struct.unpack("b", filebuf[yara_offset + 27 : yara_offset + 28])[0] 86 | config_size = struct.unpack("i", filebuf[yara_offset + 30 : yara_offset + 34])[0] 87 | tmp_config = "".join([chr(xor_key ^ ord(x)) for x in filebuf[config_offset : config_offset + config_size]]) 88 | end_config = {} 89 | c2_address = tmp_config[8 : 8 + MAX_IP_STRING_SIZE] 90 | if c2_address: 91 | end_config.setdefault("CNCs", []).append(c2_address) 92 | c2_address = tmp_config[0x48 : 0x48 + MAX_IP_STRING_SIZE] 93 | if c2_address: 94 | end_config.setdefault("CNCs", []).append(c2_address) 95 | c2_address = tmp_config[0x88 : 0x88 + MAX_IP_STRING_SIZE] 96 | if c2_address: 97 | end_config.setdefault("CNCs", []).append(c2_address) 98 | missionid = string_from_offset(tmp_config, 0x1EC) 99 | if missionid: 100 | end_config.setdefault("raw", {})["missionid"] = missionid 101 | mutex = unicode_string_from_offset(tmp_config, 0x508) 102 | if mutex: 103 | end_config["mutex"] = mutex 104 | key = string_from_offset(tmp_config, 0x832) 105 | if key: 106 | end_config["cryptokey"] = key 107 | 108 | return end_config 109 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/_ShadowTech.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ShadowTech Config Extractor 4 | """ 5 | 6 | import re 7 | import string 8 | 9 | import createIOC 10 | import database 11 | 12 | new_line = "#-@NewLine@-#" 13 | split_string = "ESILlzCwXBSrQ1Vb72t6bIXtKRzHJkolNNL94gD8hIi9FwLiiVlrznTz68mkaaJQQSxJfdLyE4jCnl5QJJWuPD4NeO4WFYURvmkth8" 14 | enc_key = "pSILlzCwXBSrQ1Vb72t6bIXtKRzAHJklNNL94gD8hIi9FwLiiVlr" # Actual key is "KeY11PWD24" 15 | 16 | 17 | # Helper Functions Go Here 18 | 19 | 20 | def string_print(line): 21 | return [x for x in line if x in string.printable] 22 | 23 | 24 | def get_config(data): 25 | config_list = [] 26 | config_string = data.split(split_string) 27 | for x in range(1, len(config_string)): 28 | try: 29 | output = "" 30 | hex_pairs = [config_string[x][i : i + 2] for i in range(0, len(config_string[x]), 2)] 31 | for i in range(len(config_string[x]) // 2): 32 | data_slice = int(hex_pairs[i], 16) # get next hex value 33 | key_slice = ord(enc_key[i + 1]) # get next Char For Key 34 | output += chr(data_slice ^ key_slice) # xor Hex and Key Char 35 | print(output) 36 | except Exception: 37 | output = "DecodeError" 38 | config_list.append(output) 39 | return config_list 40 | 41 | 42 | # returns pretty config 43 | def parse_config(config_list): 44 | return { 45 | "Domain": config_list[0], 46 | "Port": config_list[1], 47 | "CampaignID": config_list[2], 48 | "Password": config_list[3], 49 | "InstallFlag": config_list[4], 50 | "RegistryKey": config_list[5], 51 | "Melt": config_list[6], 52 | "Persistance": config_list[7], 53 | "Mutex": config_list[8], 54 | "ShowMsgBox": config_list[9], 55 | # "Flag5": config_list[10] # MsgBox Icon, 56 | # "Flag6": config_list[11] # MsgBox Buttons, 57 | "MsgBoxTitle": config_list[12], 58 | "MsgBoxText": config_list[13], 59 | } 60 | 61 | 62 | """ 63 | def decrypt_XOR(enckey, data): 64 | # ToDo fix it yourself, XOR not defined 65 | cipher = XOR.new(enckey) # set the cipher 66 | return cipher.decrypt(data) # decrpyt the data 67 | """ 68 | 69 | 70 | def snortRule(md5, config_dict): 71 | rules = [] 72 | domain = config_dict["Domain"] 73 | ipPattern = re.compile(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}") 74 | ipTest = ipPattern.search(domain) 75 | if len(domain) > 1: 76 | if ipTest: 77 | rules.append( 78 | f"""alert tcp any any -> {domain}""" 79 | f""" any (msg: "ShadowTech Beacon Domain: {domain}""" 80 | """"; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""" 81 | ) 82 | else: 83 | rules.append( 84 | f"""alert udp any any -> any 53 (msg: "ShadowTech Beacon Domain: {domain}""" 85 | f""""; content:"|0e|{domain}""" 86 | """|00|"; nocase; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""" 87 | ) 88 | rules.append( 89 | f"""alert tcp any any -> any 53 (msg: "ShadowTech Beacon Domain: {domain}""" 90 | f""""; content:"|0e|{domain}""" 91 | """|00|"; nocase; classtype:trojan-activity; sid:5000000; rev:1; priority:1; reference:url,http://malwareconfig.com;)""" 92 | ) 93 | database.insertSnort(md5, rules) 94 | 95 | 96 | # IOC Creator Two elements Domain or install 97 | def generateIOC(md5, config_dict): 98 | items = [ 99 | [ 100 | ("is", "PortItem", "PortItem/remotePort", "string", config_dict["Port"]), 101 | ("contains", "Network", "Network/DNS", "string", config_dict["Domain"]), 102 | ] 103 | ] 104 | IOC = createIOC.main(items, "ShadowTech", md5) 105 | database.insertIOC(md5, IOC) 106 | 107 | 108 | def run(md5, data): 109 | raw_config = get_config(data) 110 | 111 | # lets Process this and format the config 112 | config_dict = parse_config(raw_config) 113 | if len(config_dict["Domain"]) > 0: 114 | snortRule(md5, config_dict) 115 | generateIOC(md5, config_dict) 116 | database.insertDomain(md5, [config_dict["Domain"]]) 117 | return config_dict 118 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/EvilGrab.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Kevin O'Reilly kevin.oreilly@contextis.co.uk 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation, either version 3 of the License, or 5 | # (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program. If not, see . 14 | 15 | DESCRIPTION = "EvilGrab configuration parser." 16 | AUTHOR = "kevoreilly" 17 | 18 | import struct 19 | import pefile 20 | import yara 21 | 22 | rule_source = """ 23 | rule EvilGrab 24 | { 25 | meta: 26 | author = "kev" 27 | description = "EvilGrab configuration function" 28 | cape_type = "EvilGrab Payload" 29 | strings: 30 | $configure1 = {8D 44 24 ?? 50 6A 01 E8 ?? ?? ?? ?? 85 C0 74 07 33 C0 E9 9? 00 00 00 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 83 F8 07 59 73 ?? 68 ?? ?? ?? ?? 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 68} 31 | $configure2 = {8D 44 24 ?? 50 6A 01 E8 ?? ?? ?? ?? 85 C0 74 07 33 C0 E9 9? 00 00 00 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 83 F8 07 59 73 ?? 68 ?? ?? ?? ?? 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 83} 32 | $configure3 = {8D 95 60 ?? ?? ?? 52 6A 01 E8 ?? ?? ?? ?? 85 C0 74 13 33 C0 8B 4D F4 64 89 0D 00 00 00 00 5F 5E 5B 8B E5 5D C3 BF ?? ?? ?? ?? 83 C9 FF 33 C0 F2 AE} 33 | 34 | condition: 35 | //check for MZ Signature at offset 0 36 | uint16(0) == 0x5A4D 37 | 38 | and 39 | 40 | $configure1 or $configure2 or $configure3 41 | } 42 | """ 43 | 44 | MAX_STRING_SIZE = 65 45 | 46 | 47 | def yara_scan(raw_data): 48 | addresses = {} 49 | yara_rules = yara.compile(source=rule_source) 50 | matches = yara_rules.match(data=raw_data) 51 | for match in matches: 52 | if match.rule == "EvilGrab": 53 | for item in match.strings: 54 | addresses[item.identifier] = item.instances[0].offset 55 | return addresses 56 | 57 | 58 | def pe_data(pe, va, size): 59 | image_base = pe.OPTIONAL_HEADER.ImageBase 60 | rva = va - image_base 61 | return pe.get_data(rva, size) 62 | 63 | 64 | def string_from_va(pe, offset): 65 | image_base = pe.OPTIONAL_HEADER.ImageBase 66 | string_rva = struct.unpack("i", pe.__data__[offset : offset + 4])[0] - image_base 67 | string_offset = pe.get_offset_from_rva(string_rva) 68 | return pe.__data__[string_offset : string_offset + MAX_STRING_SIZE].split(b"\0", 1)[0] 69 | 70 | 71 | map_offset = { 72 | "$configure1": [24, 71, 60, 90, 132, 186], 73 | "$configure2": [27, 78, 67, 91, 133, 188], 74 | "$configure3": [38, 99, 132, 167, 195], 75 | } 76 | 77 | 78 | def extract_config(filebuf): 79 | pe = pefile.PE(data=filebuf, fast_load=False) 80 | # image_base = pe.OPTIONAL_HEADER.ImageBase 81 | yara_matches = yara_scan(filebuf) 82 | end_config = {} 83 | for key, values in map_offset.keys(): 84 | if not yara_matches.get(key): 85 | continue 86 | 87 | yara_offset = int(yara_matches[key]) 88 | 89 | # ToDo missed schema 90 | c2_address = string_from_va(pe, yara_offset + values[0]) 91 | if c2_address: 92 | end_config["CNCs"] = c2_address 93 | port = str(struct.unpack("h", filebuf[yara_offset + values[1] : yara_offset + values[1] + 2])[0]) 94 | if port: 95 | end_config.setdefault("raw", {})["port"] = [port, "tcp"] 96 | missionid = string_from_va(pe, yara_offset + values[3]) 97 | if missionid: 98 | end_config.setdefault("raw", {})["missionid"] = missionid 99 | version = string_from_va(pe, yara_offset + values[4]) 100 | if version: 101 | end_config["version"] = version 102 | injectionprocess = string_from_va(pe, yara_offset + values[5]) 103 | if injectionprocess: 104 | end_config.setdefault("raw", {})["injectionprocess"] = injectionprocess 105 | if key != "$configure3": 106 | mutex = string_from_va(pe, yara_offset - values[6]) 107 | if mutex: 108 | end_config["mutex"] = mutex 109 | 110 | return end_config 111 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/KoiLoader.py: -------------------------------------------------------------------------------- 1 | import re 2 | import struct 3 | from contextlib import suppress 4 | from itertools import cycle 5 | 6 | import pefile 7 | 8 | import yara 9 | 10 | # Hash = b462e3235c7578450b2b56a8aff875a3d99d22f6970a01db3ba98f7ecb6b01a0 11 | 12 | RULE_SOURCE = """ 13 | rule KoiLoaderResources 14 | { 15 | meta: 16 | author = "YungBinary" 17 | description = "Find KoiLoader XOR key and payload resource ids" 18 | strings: 19 | $payload_resource = {8D [2] 50 68 [4] E8} 20 | $xor_key_resource = {8D [2] 51 68 [4] E8} 21 | condition: 22 | uint16(0) == 0x5A4D and $payload_resource and $xor_key_resource 23 | } 24 | """ 25 | 26 | 27 | def yara_scan(raw_data): 28 | yara_rules = yara.compile(source=RULE_SOURCE) 29 | matches = yara_rules.match(data=raw_data) 30 | payload_resource_id = None 31 | xor_key_resource_id = None 32 | 33 | for match in matches: 34 | if match.rule != "KoiLoaderResources": 35 | continue 36 | for item in match.strings: 37 | if "$payload_resource" in item.identifier: 38 | payload_offset = item.instances[0].offset 39 | payload_resource_id = struct.unpack("i", raw_data[payload_offset + 5 : payload_offset + 9])[0] 40 | 41 | elif "$xor_key_resource" in item.identifier: 42 | xor_key_offset = item.instances[0].offset 43 | xor_key_resource_id = struct.unpack("i", raw_data[xor_key_offset + 5 : xor_key_offset + 9])[0] 44 | 45 | return (payload_resource_id, xor_key_resource_id) 46 | 47 | 48 | def remove_nulls(buffer, buffer_size): 49 | """ 50 | Modify a buffer removing null bytes 51 | """ 52 | num_nulls = count_nulls(buffer) 53 | result = skip_nth(buffer, num_nulls + 1) 54 | return bytearray(result) 55 | 56 | 57 | def count_nulls(buffer): 58 | """ 59 | Count null separation in a buffer 60 | """ 61 | num_nulls = 0 62 | idx = 1 63 | while True: 64 | cur_byte = buffer[idx] 65 | if cur_byte == 0: 66 | num_nulls += 1 67 | idx += 1 68 | continue 69 | else: 70 | break 71 | 72 | return num_nulls 73 | 74 | 75 | def skip_nth(buffer, n): 76 | iterable = list(buffer) 77 | yield from (value for index, value in enumerate(iterable) if (index + 1) % n and (index - 1) % n) 78 | 79 | 80 | def find_c2(decoded_buffer): 81 | decoded_buffer = bytearray(skip_nth(decoded_buffer, 2)) 82 | url_regex = re.compile(rb"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") 83 | urls = [url.lower().decode() for url in url_regex.findall(decoded_buffer)] 84 | return urls 85 | 86 | 87 | def xor_data(data, key): 88 | return bytes(c ^ k for c, k in zip(data, cycle(key))) 89 | 90 | 91 | def extract_config(data): 92 | config = {} 93 | 94 | xor_key = b"" 95 | encoded_payload = b"" 96 | 97 | payload_resource_id, xor_key_resource_id = yara_scan(data) 98 | 99 | if payload_resource_id is None or xor_key_resource_id is None: 100 | return 101 | 102 | with suppress(Exception): 103 | pe = pefile.PE(data=data) 104 | for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries: 105 | resource_type = pefile.RESOURCE_TYPE.get(entry.struct.Id) 106 | for directory in entry.directory.entries: 107 | for resource in directory.directory.entries: 108 | if resource_type != "RT_RCDATA": 109 | continue 110 | if directory.struct.Id == xor_key_resource_id: 111 | offset = resource.data.struct.OffsetToData 112 | xor_phrase_size = resource.data.struct.Size 113 | xor_key = pe.get_memory_mapped_image()[offset : offset + xor_phrase_size] 114 | elif directory.struct.Id == payload_resource_id: 115 | offset = resource.data.struct.OffsetToData 116 | encoded_payload_size = resource.data.struct.Size 117 | encoded_payload = pe.get_memory_mapped_image()[offset : offset + encoded_payload_size] 118 | 119 | encoded_payload = remove_nulls(encoded_payload, encoded_payload_size) 120 | decoded_payload = xor_data(encoded_payload, xor_key) 121 | cncs = find_c2(decoded_payload) 122 | if cncs: 123 | config["CNCs"] = cncs 124 | 125 | return config 126 | 127 | 128 | if __name__ == "__main__": 129 | import sys 130 | 131 | with open(sys.argv[1], "rb") as f: 132 | print(extract_config(f.read())) 133 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/xRAT.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | from base64 import b64decode 4 | 5 | import pefile 6 | from Cryptodome.Cipher import AES, XOR 7 | 8 | 9 | def string_print(line): 10 | return "".join((char for char in line if 32 < ord(char) < 127)) 11 | 12 | 13 | def parse_config(config_list, ver): 14 | config_dict = {} 15 | if ver == "V1": 16 | config_dict["Version"] = "1.0.x" 17 | config_dict["Domain"] = config_list[1] 18 | config_dict["Port"] = config_list[2] 19 | config_dict["Password"] = config_list[3] 20 | config_dict["CampaignID"] = config_list[4] 21 | config_dict["InstallName"] = config_list[5] 22 | config_dict["HKCUKey"] = config_list[6] 23 | config_dict["InstallDir"] = config_list[7] 24 | config_dict["Flag1"] = config_list[8] 25 | config_dict["Flag2"] = config_list[9] 26 | config_dict["Mutex"] = config_list[10] 27 | if ver == "V2": 28 | config_dict["Version"] = config_list[0] 29 | config_dict["Domain"] = config_list[1] 30 | config_dict["Password"] = config_list[2] 31 | config_dict["InstallSub"] = config_list[3] 32 | config_dict["InstallName"] = config_list[4] 33 | config_dict["Mutex"] = config_list[5] 34 | config_dict["RegistryKey"] = config_list[6] 35 | return config_dict 36 | 37 | 38 | def get_long_line(data): 39 | try: 40 | raw_config = None 41 | pe = pefile.PE(data=data) 42 | for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries: 43 | if str(entry.name) == "RT_RCDATA": 44 | new_dirs = entry.directory 45 | for entry in new_dirs.entries: 46 | if str(entry.name) == "0": 47 | data_rva = entry.directory.entries[0].data.struct.OffsetToData 48 | size = entry.directory.entries[0].data.struct.Size 49 | data = pe.get_memory_mapped_image()[data_rva : data_rva + size] 50 | raw_config = data 51 | except Exception: 52 | raw_config = None 53 | if raw_config is not None: 54 | return raw_config, "V1" 55 | try: 56 | m = re.search("\x69\x00\x6F\x00\x6E\x00\x00\x59(.*)\x6F\x43\x00\x61\x00\x6E", data) 57 | raw_config = m.group(0)[4:-12] 58 | return raw_config, "V2" 59 | except Exception: 60 | return None, None 61 | 62 | 63 | def decrypt_XOR(enckey, data): 64 | cipher = XOR.new(enckey) # set the cipher 65 | return cipher.decrypt(data) # decrpyt the data 66 | 67 | 68 | # decrypt function 69 | def decrypt_aes(enckey, data): 70 | iv = data[:16] 71 | cipher = AES.new(enckey, AES.MODE_CBC, iv) # set the cipher 72 | return cipher.decrypt(data[16:]) # decrpyt the data 73 | 74 | 75 | # converts the enc key to an md5 key 76 | def aes_key(enc_key): 77 | return hashlib.md5(enc_key).hexdigest().decode("hex") 78 | 79 | 80 | # This will split all the b64 encoded strings and the encryption key 81 | def get_parts(long_line): 82 | coded_config = [] 83 | raw_line = long_line 84 | small_lines = raw_line.split("\x00\x00") 85 | for line in small_lines: 86 | new_line = line[1:] if len(line) % 2 == 0 else line[2:] 87 | coded_config.append(new_line.replace("\x00", "")) 88 | return coded_config 89 | 90 | 91 | def extract_config(data): 92 | long_line, ver = get_long_line(data) 93 | if ver is None: 94 | return 95 | config_list = [] 96 | if ver == "V1": 97 | # The way the XOR Cypher was implemented the keys are off by 1. 98 | key1 = "RAT11x" # Used for First level of encryption actual key is 'xRAT11' 99 | key2 = "eY11K" # used for individual sections, actual key is 'KeY11' 100 | key3 = "eY11PWD24K" # used for password section only. Actual key is 'KeY11PWD24' 101 | config = long_line.decode("hex") 102 | first_decode = decrypt_XOR(key1, config) 103 | sections = first_decode.split("|//\\\\|") # Split is |//\\| the extra \\ are for escaping. 104 | for i, section in enumerate(sections): 105 | enc_key = key3 if i == 3 else key2 106 | config_list.append(decrypt_XOR(enc_key, section.decode("hex"))) 107 | elif ver == "V2": 108 | coded_lines = get_parts(long_line) 109 | enc_key = aes_key(coded_lines[-1]) 110 | for i in range(1, (len(coded_lines) - 1)): 111 | decoded_line = b64decode(coded_lines[i]) 112 | decrypt_line = decrypt_aes(enc_key, decoded_line) 113 | config_list.append(string_print(decrypt_line)) 114 | return parse_config(config_list, ver) 115 | -------------------------------------------------------------------------------- /cape_parsers/utils/lznt1.py: -------------------------------------------------------------------------------- 1 | # Rekall Memory Forensics 2 | # Copyright 2014 Google Inc. All Rights Reserved. 3 | # 4 | # Author: Michael Cohen scudette@google.com. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or (at 9 | # your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | # 20 | 21 | """Decompression support for the LZNT1 compression algorithm. 22 | 23 | Reference: 24 | http://msdn.microsoft.com/en-us/library/jj665697.aspx 25 | (2.5 LZNT1 Algorithm Details) 26 | 27 | https://github.com/libyal/reviveit/ 28 | https://github.com/sleuthkit/sleuthkit/blob/develop/tsk/fs/ntfs.c 29 | """ 30 | import array 31 | import struct 32 | from io import BytesIO 33 | 34 | __all__ = ["Lznt1", "lznt1"] 35 | 36 | 37 | def get_displacement(offset: int) -> int: 38 | """Calculate the displacement.""" 39 | result = 0 40 | while offset >= 0x10: 41 | offset >>= 1 42 | result += 1 43 | 44 | return result 45 | 46 | 47 | DISPLACEMENT_TABLE = array.array("B", [get_displacement(x) for x in range(8192)]) 48 | 49 | COMPRESSED_MASK = 1 << 15 50 | SIGNATURE_MASK = 3 << 12 51 | SIZE_MASK = (1 << 12) - 1 52 | TAG_MASKS = [(1 << i) for i in range(0, 8)] 53 | 54 | 55 | def decompress_data(cdata: bytes) -> bytes: 56 | """Decompresses the data.""" 57 | block_end = 0 58 | 59 | with BytesIO(cdata) as in_fd, BytesIO() as output_fd: 60 | while in_fd.tell() < len(cdata): 61 | block_offset = in_fd.tell() 62 | uncompressed_chunk_offset = output_fd.tell() 63 | 64 | block_header = struct.unpack("= block_end: 79 | break 80 | 81 | if header & mask: 82 | pointer = struct.unpack("> (12 - displacement)) + 1 86 | symbol_length = (pointer & (0xFFF >> displacement)) + 3 87 | 88 | output_fd.seek(-symbol_offset, 2) 89 | data = output_fd.read(symbol_length) 90 | 91 | # Pad the data to make it fit. 92 | if 0 < len(data) < symbol_length: 93 | data = data * (symbol_length // len(data) + 1) 94 | data = data[:symbol_length] 95 | 96 | output_fd.seek(0, 2) 97 | 98 | output_fd.write(data) 99 | 100 | else: 101 | data = in_fd.read(1) 102 | 103 | output_fd.write(data) 104 | 105 | else: 106 | # Block is not compressed 107 | data = in_fd.read(size + 1) 108 | output_fd.write(data) 109 | 110 | result = output_fd.getvalue() 111 | 112 | return result 113 | 114 | 115 | class Lznt1: 116 | """ 117 | Implementation of LZNT1 decompression. Allows to decompress data compressed by RtlCompressBuffer 118 | .. code-block:: python 119 | from malduck import lznt1 120 | lznt1(b"\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot") 121 | :param buf: Buffer to decompress 122 | :type buf: bytes 123 | :rtype: bytes 124 | """ 125 | 126 | def decompress(self, buf: bytes) -> bytes: 127 | return decompress_data(buf) 128 | 129 | __call__ = decompress 130 | 131 | 132 | lznt1 = Lznt1() 133 | -------------------------------------------------------------------------------- /cape_parsers/CAPE/community/AgentTesla.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | 3 | try: 4 | from cape_parsers.utils.strings import extract_strings 5 | except ImportError as e: 6 | print(f"Problem to import extract_strings: {e}") 7 | 8 | 9 | def extract_config(data: bytes): 10 | config = {} 11 | config_dict = {} 12 | is_c2_found = False 13 | with suppress(Exception): 14 | if data[:2] == b"MZ": 15 | lines = extract_strings(data=data, on_demand=True, minchars=3) 16 | if not lines: 17 | return 18 | else: 19 | lines = data.decode().split("\n") 20 | base = next(i for i, line in enumerate(lines) if "Mozilla/5.0" in line) 21 | if not base: 22 | return 23 | for x in range(1, 32): 24 | # Data Exfiltration via Telegram 25 | if "api.telegram.org" in lines[base + x]: 26 | config_dict["Protocol"] = "Telegram" 27 | config["CNCs"] = lines[base + x] 28 | config_dict["Password"] = lines[base + x + 1] 29 | is_c2_found = True 30 | break 31 | # Data Exfiltration via Discord 32 | elif "discord" in lines[base + x]: 33 | config_dict["Protocol"] = "Discord" 34 | config["CNCs"] = [lines[base + x]] 35 | is_c2_found = True 36 | break 37 | # Data Exfiltration via FTP 38 | elif "ftp:" in lines[base + x]: 39 | config_dict["Protocol"] = "FTP" 40 | hostname = lines[base + x] 41 | username = lines[base + x + 1] 42 | password = lines[base + x + 2] 43 | config["CNCs"] = [f"ftp://{username}:{password}@{hostname}"] 44 | is_c2_found = True 45 | break 46 | # Data Exfiltration via SMTP 47 | elif "@" in lines[base + x]: 48 | config_dict["Protocol"] = "SMTP" 49 | if lines[base + x - 2].isdigit() and len(lines[base + x - 2]) <= 5: # check if length <= highest Port 65535 50 | # minchars 3 so Ports < 100 do not appear in strings / TBD: michars < 3 51 | config_dict["Port"] = lines[base + x - 2] 52 | elif lines[base + x - 2] in {"true", "false"} and lines[base + x - 3].isdigit() and len(lines[base + x - 3]) <= 5: 53 | config_dict["Port"] = lines[base + x - 3] 54 | config_dict["CNCs"] = [lines[base + +x - 1]] 55 | config_dict["Username"] = lines[base + x] 56 | config_dict["Password"] = lines[base + x + 1] 57 | if "@" in lines[base + x + 2]: 58 | config_dict["EmailTo"] = lines[base + x + 2] 59 | is_c2_found = True 60 | break 61 | # Get Persistence Payload Filename 62 | for x in range(2, 22): 63 | # Only extract Persistence Filename when a C2 is detected. 64 | if ".exe" in lines[base + x] and is_c2_found: 65 | config_dict["Persistence_Filename"] = lines[base + x] 66 | break 67 | # Get External IP Check Services 68 | externalipcheckservices = [] 69 | for x in range(-4, 19): 70 | if "ipify.org" in lines[base + x] or "ip-api.com" in lines[base + x]: 71 | externalipcheckservices.append(lines[base + x]) 72 | if externalipcheckservices: 73 | config_dict["ExternalIPCheckServices"] = externalipcheckservices 74 | 75 | # Data Exfiltration via HTTP(S) 76 | temp_match = ["http://", "https://"] # TBD: replace with a better url validator (Regex) 77 | if "Protocol" not in config_dict.keys(): 78 | for index, string in enumerate(lines[base:]): 79 | if string == "Win32_BaseBoard": 80 | for x in range(1, 8): 81 | if any(s in lines[base + index + x] for s in temp_match): 82 | config_dict["Protocol"] = "HTTP(S)" 83 | config["CNCs"] = lines[base + index + x] 84 | break 85 | if config or config_dict: 86 | config.setdefault("raw", config_dict) 87 | 88 | # If the data exfiltration is done through SMTP, then patch the extracted CNCs to include SMTP credentials 89 | if config_dict.get("Protocol") == "SMTP": 90 | config['CNCs'] = [f"smtp://{config_dict.get('Username')}:{config_dict.get('Password')}@{domain}:{config_dict.get('Port','587')}" for domain in config_dict.get('CNCs', [])] 91 | 92 | return config 93 | 94 | if __name__ == "__main__": 95 | import sys 96 | 97 | with open(sys.argv[1], "rb") as f: 98 | print(extract_config(f.read())) 99 | -------------------------------------------------------------------------------- /cape_parsers/deprecated/PoisonIvy.py: -------------------------------------------------------------------------------- 1 | import string 2 | from struct import unpack 3 | 4 | 5 | def calc_length(byte_str): 6 | try: 7 | return unpack("