1356 lines
40 KiB
C
1356 lines
40 KiB
C
/*
|
|
* Author: Silas Dunsmore aka 0x517A5D vim:ts=4:sw=4
|
|
*
|
|
* Released under the MIT X11 license; feel free to use some or all of this
|
|
* code, as long as you include the copyright and license statement (below)
|
|
* in all copies of the source code. In fact, I truly encourage reuse.
|
|
*
|
|
* If you do use large portions of this code, I suggest but do not require
|
|
* that you keep this code in a seperate file (such as this hexsearch.c file)
|
|
* so that it is clear that the terms of the license do not also apply to
|
|
* your code.
|
|
*
|
|
* Should you make fundamental changes, or bugfixes, to this code, I would
|
|
* appreciate it if you would give me a copy of your changes.
|
|
*
|
|
*
|
|
* Be advised that I use several advanced idioms of the C language:
|
|
* macro expansion, stringification, and variable argument functions.
|
|
* You do not need to understand them. Usage should be obvious.
|
|
*
|
|
*
|
|
* Lots of logging output is sent to OutputDebugString().
|
|
* The Sysinternals' DebugView program is very useful in monitering this.
|
|
*
|
|
*
|
|
* Copyright (C) 2007-2008 Silas Dunsmore
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* See also: http://www.opensource.org/licenses/mit-license.php
|
|
* and http://en.wikipedia.org/wiki/MIT_License
|
|
*/
|
|
|
|
#include <stdarg.h> // va_list and friends
|
|
#include <stdio.h> // vsnprintf()
|
|
#include <stdlib.h> // atexit()
|
|
#include <signal.h>
|
|
#define WINVER 0x0500 // OpenThread()
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <tlhelp32.h>
|
|
#include "hexsearch2.h"
|
|
|
|
|
|
#define SKIPME 0xFEDCBA98 // token for internal use only.
|
|
|
|
|
|
// exported globals
|
|
HANDLE df_h_process, df_h_thread;
|
|
DWORD df_pid, df_main_win_tid;
|
|
DWORD here[16];
|
|
DWORD target[16];
|
|
char *errormessage;
|
|
DWORD df_memory_base, df_memory_start, df_memory_end;
|
|
|
|
|
|
// local globals
|
|
static BOOL change_page_permissions;
|
|
static BOOL suspended;
|
|
static BYTE *copy_of_df_memory;
|
|
static int nexthere;
|
|
static int nexttarget;
|
|
static DWORD searchmemstart, searchmemend;
|
|
|
|
|
|
#define dump(x) d_printf("%-32s == %08X\n", #x, (x));
|
|
#define dumps(x) d_printf("%-32s == '%s'\n", #x, (x));
|
|
|
|
|
|
// ============================================================================
|
|
// send info to KERNEL32:OutputDebugString(), useful for non-console programs.
|
|
// you can watch this with SysInternals' DebugView.
|
|
// http://www.microsoft.com/technet/sysinternals/Miscellaneous/DebugView.mspx
|
|
//
|
|
void d_printf(const char *format, ...)
|
|
{
|
|
va_list va;
|
|
char debugstring[4096]; // debug strings can be up to 4K.
|
|
|
|
va_start(va, format);
|
|
_vsnprintf(debugstring, sizeof(debugstring) - 2, format, va);
|
|
va_end(va);
|
|
|
|
OutputDebugString(debugstring);
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
BOOL isvalidaddress(DWORD address)
|
|
{
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
return (VirtualQueryEx(df_h_process, (void *)address, &mbi, sizeof(mbi))
|
|
== sizeof(mbi) && mbi.State == MEM_COMMIT);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL peekarb(DWORD address, OUT void *data, DWORD len)
|
|
{
|
|
BOOL ok = FALSE;
|
|
DWORD succ = 0;
|
|
DWORD OldProtect;
|
|
MEMORY_BASIC_INFORMATION mbi = {0, 0, 0, 0, 0, 0, 0};
|
|
|
|
errormessage = "";
|
|
|
|
// do {} while(0) is a construct that lets you abort a piece of code
|
|
// in the middle without using gotos.
|
|
|
|
do
|
|
{
|
|
|
|
if ((succ = VirtualQueryEx(df_h_process, (BYTE *)address + len - 1,
|
|
&mbi, sizeof(mbi))) != sizeof(mbi))
|
|
{
|
|
dump(address + len - 1);
|
|
dump(succ);
|
|
errormessage = "peekarb(): VirtualQueryEx() on end address failed";
|
|
break;
|
|
}
|
|
if (mbi.State != MEM_COMMIT)
|
|
{
|
|
dump(mbi.State);
|
|
errormessage = "peekarb(): VirtualQueryEx() says end address "
|
|
"is not MEM_COMMIT";
|
|
break;
|
|
}
|
|
|
|
if ((succ = VirtualQueryEx(df_h_process, (void *)address, &mbi,
|
|
sizeof(mbi))) != sizeof(mbi))
|
|
{
|
|
dump(address);
|
|
dump(succ);
|
|
errormessage ="peekarb(): VirtualQueryEx() on start address failed";
|
|
break;
|
|
}
|
|
if (mbi.State != MEM_COMMIT)
|
|
{
|
|
dump(mbi.State);
|
|
errormessage = "peekarb(): VirtualQueryEx() says start address is "
|
|
"not MEM_COMMIT";
|
|
break;
|
|
}
|
|
|
|
if (change_page_permissions)
|
|
{
|
|
if (!VirtualProtectEx(df_h_process, mbi.AllocationBase,
|
|
mbi.RegionSize, PAGE_READWRITE, &OldProtect))
|
|
{
|
|
errormessage = "peekarb(): VirtualProtectEx() failed";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ReadProcessMemory(df_h_process, (void *)address, data, len, &succ))
|
|
{
|
|
errormessage = "peekarb(): ReadProcessMemory() failed";
|
|
|
|
// note that we do NOT break here, as we want to restore the
|
|
// page protection.
|
|
}
|
|
else if (len != succ)
|
|
{
|
|
errormessage = "peekarb(): ReadProcessMemory() returned "
|
|
"partial read";
|
|
}
|
|
else ok = TRUE;
|
|
|
|
if (change_page_permissions)
|
|
{
|
|
if (!VirtualProtectEx(df_h_process, mbi.AllocationBase,
|
|
mbi.RegionSize, OldProtect, &OldProtect))
|
|
{
|
|
errormessage = "peekarb(): undo VirtualProtectEx() failed";
|
|
break;
|
|
}
|
|
}
|
|
} while (0);
|
|
|
|
if (errormessage != NULL && strlen(errormessage) != 0)
|
|
{
|
|
d_printf("%s\n", errormessage);
|
|
dump(address);
|
|
//dump(len);
|
|
//dump(succ);
|
|
//dump(mbi.AllocationBase);
|
|
}
|
|
|
|
return(ok);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BYTE peekb(DWORD address)
|
|
{
|
|
BYTE data;
|
|
return(peekarb(address, &data, sizeof(data)) ? data : 0);
|
|
// pop quiz: why don't we set errormessage?
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
WORD peekw(DWORD address)
|
|
{
|
|
WORD data;
|
|
return(peekarb(address, &data, sizeof(data)) ? data : 0);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
DWORD peekd(DWORD address)
|
|
{
|
|
DWORD data;
|
|
return(peekarb(address, &data, sizeof(data)) ? data : 0);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
char *peekstr(DWORD address, OUT char *data, DWORD maxlen)
|
|
{
|
|
BYTE c;
|
|
int i = 0;
|
|
|
|
data[0] = '\0';
|
|
if (!isvalidaddress(address)) return(data);
|
|
|
|
while (--maxlen && (c = peekb(address++)) >= ' ' && c <= '~')
|
|
{
|
|
if (!isvalidaddress(address)) return(data);
|
|
data[i++] = c;
|
|
data[i] = '\0';
|
|
}
|
|
// for convenience
|
|
return(data);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
char *peekwstr(DWORD address, OUT char *data, DWORD maxlen)
|
|
{
|
|
BYTE c;
|
|
int i = 0;
|
|
|
|
data[0] = '\0';
|
|
if (!isvalidaddress(address)) return(data);
|
|
|
|
while (--maxlen && (c = peekb(address++)) >= ' ' && c <= '~' && peekb(address++) == 0)
|
|
{
|
|
if (!isvalidaddress(address))
|
|
{
|
|
return(data);
|
|
}
|
|
data[i++] = c;
|
|
data[i] = '\0';
|
|
}
|
|
// for convenience
|
|
return(data);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL pokearb(DWORD address, const void *data, DWORD len)
|
|
{
|
|
BOOL ok = FALSE;
|
|
DWORD succ = 0;
|
|
DWORD OldProtect;
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
|
|
errormessage = "";
|
|
|
|
do
|
|
{
|
|
if (!isvalidaddress(address))
|
|
{
|
|
errormessage = "pokearb() failed: invalid address";
|
|
break;
|
|
}
|
|
|
|
if (!isvalidaddress(address + len - 1))
|
|
{
|
|
errormessage = "pokearb() failed: invalid end address";
|
|
break;
|
|
}
|
|
|
|
if (change_page_permissions)
|
|
{
|
|
if (VirtualQueryEx(df_h_process, (void *)address, &mbi, sizeof(mbi)) != sizeof(mbi))
|
|
{
|
|
errormessage = "pokearb(): VirtualQueryEx() failed";
|
|
break;
|
|
}
|
|
|
|
if (!VirtualProtectEx(df_h_process, mbi.AllocationBase, mbi.RegionSize, PAGE_READWRITE, &OldProtect))
|
|
{
|
|
errormessage = "pokearb(): VirtualProtectEx() failed";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!WriteProcessMemory(df_h_process, (void *)address, data, len, &succ))
|
|
{
|
|
errormessage = "pokearb(): WriteProcessMemory() failed";
|
|
// note that we do NOT break here, as we want to restore the
|
|
// page protection.
|
|
}
|
|
else if (len != succ)
|
|
{
|
|
errormessage = "pokearb(): WriteProcessMemory() did partial write";
|
|
}
|
|
else
|
|
{
|
|
ok = TRUE;
|
|
}
|
|
|
|
if (change_page_permissions)
|
|
{
|
|
if (!VirtualProtectEx(df_h_process, mbi.AllocationBase, mbi.RegionSize, OldProtect, &OldProtect))
|
|
{
|
|
errormessage = "pokearb(): undo VirtualProtectEx() failed";
|
|
break;
|
|
}
|
|
}
|
|
} while (0);
|
|
|
|
if (errormessage != NULL && strlen(errormessage) != 0)
|
|
{
|
|
d_printf("%s\n", errormessage);
|
|
dump(address);
|
|
//dump(len);
|
|
//dump(succ);
|
|
//dump(mbi.AllocationBase);
|
|
}
|
|
|
|
return(ok);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL pokeb(DWORD address, BYTE data)
|
|
{
|
|
return(pokearb(address, &data, sizeof(data)));
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL pokew(DWORD address, WORD data)
|
|
{
|
|
return(pokearb(address, &data, sizeof(data)));
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL poked(DWORD address, DWORD data)
|
|
{
|
|
return(pokearb(address, &data, sizeof(data)));
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL pokestr(DWORD address, const BYTE *data)
|
|
{
|
|
// can't include a "\x00" in the string, obviously.
|
|
return(pokearb(address, data, strlen((const char *)data)));
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// helper function for hexsearch. recursive, with backtracking.
|
|
// this checks if a particular memory offset matches the given pattern.
|
|
// returns location of start of match (in the cached copy of DF memory).
|
|
// returns NULL on mismatch.
|
|
//
|
|
// TODO: there is a harmless bug in the recursion related to the very first
|
|
// recursive call, probably on each level of recursion.
|
|
static BYTE *hexsearch_match2(BYTE *p, DWORD token1, va_list va)
|
|
{
|
|
static DWORD recursion_level = 0;
|
|
DWORD tokensprocessed = 0;
|
|
DWORD token = 0x4DECADE5, b1, b2, lo, hi;
|
|
BYTE *retval = p;
|
|
BOOL ok = FALSE, lookedahead;
|
|
int savenexthere = nexthere;
|
|
int savenexttarget = nexttarget;
|
|
|
|
// TODO token is being used without being inited on recursion.
|
|
|
|
//if (recursion_level) dump(recursion_level);
|
|
//if (recursion_level) dump(p);
|
|
//if (recursion_level) dump(tokensprocessed);
|
|
|
|
if (token1 != SKIPME)
|
|
{
|
|
lookedahead = TRUE;
|
|
token = token1;
|
|
tokensprocessed = 1;
|
|
}
|
|
|
|
while (1)
|
|
{
|
|
// if the previous argument looked ahead, token is already set.
|
|
// peekahead is currently unused.
|
|
if (!lookedahead)
|
|
{
|
|
token = va_arg(va, unsigned int);
|
|
tokensprocessed++;
|
|
}
|
|
lookedahead = FALSE;
|
|
|
|
// exact-match a byte, advance.
|
|
if (token <= 0xFF)
|
|
{
|
|
if (token != *p++) break;
|
|
}
|
|
|
|
// the remaining tokens (the metas) ought to be a switch.
|
|
// but that would make it hard to break out of the while(1).
|
|
|
|
// if we hit an EOL, the match succeeded.
|
|
else if (token == EOL)
|
|
{
|
|
ok = TRUE;
|
|
break;
|
|
}
|
|
|
|
// match any byte, advance.
|
|
else if (token == ANYBYTE)
|
|
{
|
|
p++;
|
|
}
|
|
|
|
// return the address of the next matching byte instead of the
|
|
// address of the start of the pattern. don't advance.
|
|
else if (token == HERE)
|
|
{
|
|
retval = p;
|
|
here[nexthere++] = (DWORD)p; // needs postprocessing.
|
|
}
|
|
|
|
// accept either of the next two parameters as a match. advance.
|
|
// note that this does not count as peeking.
|
|
else if (token == EITHER)
|
|
{
|
|
if ((b1 = va_arg(va, unsigned int)) > 0xFF)
|
|
{
|
|
d_printf("EITHER: not followed by a legal token (byte1): %08X\n", b1);
|
|
break;
|
|
}
|
|
tokensprocessed++;
|
|
if ((b2 = va_arg(va, unsigned int)) > 0xFF)
|
|
{
|
|
d_printf("EITHER: not followed by a legal token (byte2): %08X\n", b2);
|
|
break;
|
|
}
|
|
tokensprocessed++;
|
|
if (!(*p == b1 || *p == b2)) break;
|
|
p++;
|
|
}
|
|
|
|
#ifdef FF_OR_00 //DEPRECATED
|
|
// accept either 0x00 or 0xFF. advance.
|
|
else if (token == FF_OR_00)
|
|
{
|
|
if (!(*p == 0x00 || *p == 0xFF))
|
|
{
|
|
break;
|
|
}
|
|
p++;
|
|
}
|
|
#endif
|
|
|
|
// set low value for range comparison. don't advance. DEPRECATED.
|
|
else if (token == RANGE_LO)
|
|
{
|
|
if ((lo = va_arg(va, unsigned int)) > 0xFF)
|
|
{
|
|
d_printf("RANGE_LO: not followed by a legal token: %08X\n", lo);
|
|
break;
|
|
}
|
|
tokensprocessed++;
|
|
// Q: peek here to ensure next token is RANGE_HI ?
|
|
}
|
|
|
|
// set high value for range comparison, and do comparison. advance.
|
|
// DEPRECATED.
|
|
else if (token == RANGE_HI)
|
|
{
|
|
if ((hi = va_arg(va, unsigned int)) > 0xFF) {
|
|
d_printf("RANGE_HI: not followed by a legal token: %08X\n", hi);
|
|
break;
|
|
}
|
|
if (*p < lo || *p > hi)
|
|
{
|
|
break;
|
|
}
|
|
p++;
|
|
tokensprocessed++;
|
|
}
|
|
|
|
// do a byte-size range comparison
|
|
else if (token == BYTERANGE)
|
|
{
|
|
if ((lo = va_arg(va, unsigned int)) > 0xFF)
|
|
{
|
|
d_printf("BYTERANGE: not followed by a legal token: %08X\n", lo);
|
|
break;
|
|
}
|
|
tokensprocessed++;
|
|
|
|
if ((hi = va_arg(va, unsigned int)) > 0xFF)
|
|
{
|
|
d_printf("BYTERANGE: not followed by a legal token: %08X\n", hi);
|
|
break;
|
|
}
|
|
if (*p < lo || *p > hi)
|
|
{
|
|
break;
|
|
}
|
|
p++;
|
|
tokensprocessed++;
|
|
}
|
|
|
|
// do a dword-size range comparison
|
|
else if (token == DWORDRANGE)
|
|
{
|
|
lo = va_arg(va, unsigned int);
|
|
tokensprocessed++;
|
|
|
|
hi = va_arg(va, unsigned int);
|
|
if (*(DWORD *)p < lo || *(DWORD *)p > hi)
|
|
{
|
|
break;
|
|
}
|
|
p++;
|
|
tokensprocessed++;
|
|
}
|
|
|
|
// this is the fun one. this is where we recurse.
|
|
else if (token == SKIP_UP_TO)
|
|
{
|
|
DWORD len;
|
|
BYTE * subretval;
|
|
|
|
len = va_arg(va, unsigned int) + 1;
|
|
tokensprocessed++;
|
|
|
|
while (len)
|
|
{
|
|
// um. This is a kludge. It ought to work to not set any
|
|
// heres or targets if we're in a recursion. (not tested)
|
|
int savenexthere;
|
|
int savenexttarget;
|
|
|
|
// I think it's not technically legal to copy va_lists;
|
|
// but it should work on any stack-based machine, which is
|
|
// everything except old Crays and weird dataflow processors.
|
|
|
|
savenexthere = nexthere;
|
|
savenexttarget = nexttarget;
|
|
//dump(tokensprocessed);
|
|
//dump(recursion_level);
|
|
//dumps("-->");
|
|
recursion_level++;
|
|
subretval = hexsearch_match2(p, SKIPME, va);
|
|
recursion_level--;
|
|
//dumps("<--");
|
|
//dump(recursion_level);
|
|
//dump(tokensprocessed);
|
|
nexthere = savenexthere;
|
|
nexttarget = savenexttarget;
|
|
if (subretval != NULL) {
|
|
// okay, we now know that the bytes starting at p
|
|
// match the remainder of the pattern.
|
|
// Nonetheless, for ease of programming, we will
|
|
// go through the motions instead of trying to
|
|
// early-out. (Basically done for HERE tokens.)
|
|
break;
|
|
}
|
|
|
|
p++;
|
|
len--;
|
|
}
|
|
if (subretval != NULL)
|
|
{
|
|
continue;
|
|
}
|
|
// no match within nn bytes, abort.
|
|
break;
|
|
}
|
|
|
|
// exact-match a dword. advance 4.
|
|
else if (token == DWORD_)
|
|
{
|
|
DWORD d = va_arg(va, unsigned int);
|
|
if (*(DWORD *)p != d) break;
|
|
p += 4;
|
|
tokensprocessed++;
|
|
}
|
|
|
|
// match any dword. advance 4.
|
|
else if (token == ANYDWORD)
|
|
{
|
|
p += 4;
|
|
}
|
|
|
|
// match any legal address in
|
|
else if (token == ADDRESS)
|
|
{
|
|
// program text. advance 4.
|
|
if (*(DWORD *)p < df_memory_start)
|
|
{
|
|
break;
|
|
}
|
|
if (*(DWORD *)p > df_memory_end)
|
|
{
|
|
break;
|
|
}
|
|
p += 4;
|
|
}
|
|
|
|
// match any call. advance 5.
|
|
else if (token == CALL)
|
|
{
|
|
if (*p++ != 0xE8)
|
|
{
|
|
break;
|
|
}
|
|
target[nexttarget++] = *(DWORD *)p + (DWORD)p + 4;
|
|
#if 0
|
|
if (*(DWORD *)p > DF_CODE_SIZE
|
|
&& *(DWORD *)p < (DWORD)-DF_CODE_SIZE)
|
|
break;
|
|
#endif
|
|
p += 4;
|
|
}
|
|
|
|
// match any short or near jump.
|
|
else if (token == JUMP)
|
|
{
|
|
// advance 2 or 5 respectively.
|
|
if (*p == 0xEB)
|
|
{
|
|
target[nexttarget++] = *(signed char *)(p+1) + (DWORD)p + 1;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
|
|
if (*p++ != 0xE9)
|
|
{
|
|
break;
|
|
}
|
|
|
|
target[nexttarget++] = *(DWORD *)p + (DWORD)p + 4;
|
|
#if 0
|
|
if (*(DWORD *)p > DF_CODE_SIZE
|
|
&& *(DWORD *)p < (DWORD)-DF_CODE_SIZE)
|
|
break;
|
|
#endif
|
|
p += 4;
|
|
}
|
|
|
|
// match a JZ instruction
|
|
else if (token == JZ)
|
|
{
|
|
if (*p == 0x74)
|
|
{
|
|
target[nexttarget++] = *(signed char *)(p+1) + (DWORD)p + 2;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
else if (*p == 0x0F && *(p+1) == 0x84
|
|
&& (*(p+4) == 0x00 || *(p+4) == 0xFF) // assume dist < 64k
|
|
&& (*(p+5) == 0x00 || *(p+5) == 0xFF))
|
|
{
|
|
target[nexttarget++] = *(DWORD *)(p+2) + (DWORD)p + 6;
|
|
p += 6;
|
|
continue;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
// match a JNZ instruction
|
|
else if (token == JNZ)
|
|
{
|
|
if (*p == 0x75) {
|
|
target[nexttarget++] = *(signed char *)(p+1) + (DWORD)p + 2;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
else if (*p == 0x0F && *(p+1) == 0x85
|
|
&& (*(p+4) == 0x00 || *(p+4) == 0xFF) // assume dist < 64k
|
|
&& (*(p+5) == 0x00 || *(p+5) == 0xFF)
|
|
) {
|
|
target[nexttarget++] = *(DWORD *)(p+2) + (DWORD)p + 6;
|
|
p += 6;
|
|
continue;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
// match any conditional jump
|
|
else if (token == JCC)
|
|
{
|
|
if (*p >= 0x70 && *p <= 0x7F)
|
|
{
|
|
target[nexttarget++] = *(signed char *)(p+1) + (DWORD)p + 2;
|
|
p += 2;
|
|
continue;
|
|
}
|
|
else if (*p == 0x0F && *(p+1) >= 0x80 && *(p+1) <= 0x8F
|
|
&& (*(p+4) == 0x00 || *(p+4) == 0xFF) // assume dist < 64k
|
|
&& (*(p+5) == 0x00 || *(p+5) == 0xFF))
|
|
{
|
|
target[nexttarget++] = *(DWORD *)(p+2) + (DWORD)p + 6;
|
|
p += 6;
|
|
continue;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
// unknown token, abort
|
|
else
|
|
{
|
|
d_printf("unknown token: %08X\n", token);
|
|
dump(p);
|
|
dump(recursion_level);
|
|
break;
|
|
} // end of huge if-else
|
|
|
|
} // end of while(1)
|
|
|
|
if (!ok)
|
|
{
|
|
retval = NULL;
|
|
nexthere = savenexthere;
|
|
nexttarget = savenexttarget;
|
|
}
|
|
return (retval);
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// The name has changed because the API HAS CHANGED!
|
|
//
|
|
// Now instead of returning HERE, it always returns the start of the match.
|
|
//
|
|
// However, there is an array, here[], that is filled with the locations of
|
|
// the HERE tokens.
|
|
// (Starting at 1. here[0] is a copy of the start of the match.)
|
|
//
|
|
// The here[] array, starting at 1, is filled with the locations of the
|
|
// HERE token.
|
|
// here[0] is a copy of the start of the match.
|
|
//
|
|
// Also, the target[] array, starting at 1, is filled with the target
|
|
// addresses of all CALL, JMP, JZ, JNZ, and JCC tokens.
|
|
//
|
|
// Finally, you no longer pass it a search length. Instead, each set of
|
|
// search terms must end with an EOL.
|
|
//
|
|
//
|
|
//
|
|
// Okay, I admit this one is complicated. Treat it as a black box.
|
|
//
|
|
// Search Dwarf Fortress's code and initialized data segments for a pattern.
|
|
// (Does not search stack, heap, or thread-local memory.)
|
|
//
|
|
// Parameters: any number of search tokens, all unsigned ints.
|
|
// The last token must be EOL.
|
|
//
|
|
// 0x00 - 0xFF: Match this byte.
|
|
// EOL: End-of-list. The match succeeds when this token is reached.
|
|
// ANYBYTE: Match any byte.
|
|
// DWORD_: Followed by a dword. Exact-match the dword.
|
|
// Equivalent to 4 match-this-byte tokens.
|
|
// ANYDWORD: Match any dword. Equivalant to 4 ANYBYTEs.
|
|
// HERE: Put the current address into the here[] array.
|
|
// SKIP_UP_TO, nn: Allow up to nn bytes between the previous match
|
|
// and the next match. The next token must be a match-this-byte
|
|
// token. There is sweet, sweet backtracking.
|
|
// EITHER: Accept either of the next two tokens as a match.
|
|
// Both must be match-this-byte tokens.
|
|
// RANGE_LO, nn: Set low byte for a range comparison. DEPRECATED.
|
|
// RANGE_HI, nn: Set high byte for a range comparison, and do the
|
|
// comparison. Should immediately follow a RANGE_LO. DEPRECATED.
|
|
// BYTERANGE, nn, mm: followed by two bytes, the low and high limits
|
|
// of the range. This is the new way to do ranges.
|
|
// DWORDRANGE, nnnnnnnn, mmmmmmmm: works like BYTERANGE.
|
|
// ADDRESS: Accept any legal address in the program's text.
|
|
// DOES NOT accept pointers into heap space and such.
|
|
// CALL: Match a near call instruction to a reasonable address.
|
|
// JUMP: Match a short or near unconditional jump to a reasonable
|
|
// address.
|
|
// JZ: Match a short or long jz (jump if zero) instruction.
|
|
// JNZ: Match a short or long jnz (jump if not zero) instruction.
|
|
// JCC: Match any short or long conditional jump instruction.
|
|
// More tokens can easily be added.
|
|
//
|
|
// Returns the offset in Dwarf Fortress of the first match of the pattern.
|
|
// Also sets global variables here[] and target[].
|
|
//
|
|
// Note: starting a pattern with ANYBYTE, ANYDWORD, SKIP_UP_TO, or EOL
|
|
// is explicitly illegal.
|
|
//
|
|
//
|
|
// implementation detail: search() uses a cached copy of the memory, so
|
|
// poke()s and patch()s will not be reflected in the searches.
|
|
//
|
|
// implementation detail: Q: why don't we discard count and always use EOL?
|
|
// A: because we need a fixed parameter to start the va_list.
|
|
// (Hmm, could we grab address of a local variable, walk the stack
|
|
// until we see &hexsearch, and use that address to init va_list?)
|
|
// (No, dummy, because &hexsearch is not on the stack. The stack
|
|
// holds the return address.)
|
|
// (Could we grab the EBP register and treat it as a stack frame?)
|
|
// (Maybe, but the compiler decides if EBP really is a stack frame,
|
|
// so that would be an implementation dependency. Don't do it.)
|
|
//
|
|
// TODO: should be a way to repeat a search in case we want the second or
|
|
// eleventh occurance.
|
|
//
|
|
// TODO: allow EITHER to accept arbitrary (sets of) tokens.
|
|
// (How would that work? Have two sub-patterns, each terminating
|
|
// with EOL?)
|
|
//
|
|
DWORD hexsearch2(DWORD token1, ...)
|
|
{
|
|
DWORD size;
|
|
BYTE *nextoffset, *foundit;
|
|
DWORD token;
|
|
int i;
|
|
va_list va;
|
|
|
|
for (i = 0; i < 16; i++) here[i] = 0;
|
|
for (i = 0; i < 16; i++) target[i] = 0;
|
|
|
|
nexthere = 1;
|
|
nexttarget = 1;
|
|
|
|
if ( token1 == ANYBYTE
|
|
|| token1 == ANYDWORD
|
|
|| token1 == SKIP_UP_TO
|
|
|| token1 == HERE
|
|
|| token1 == EOL )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
dump(searchmemstart);
|
|
dump(searchmemend);
|
|
dump(df_memory_start);
|
|
dump(copy_of_df_memory);
|
|
nextoffset = copy_of_df_memory;
|
|
nextoffset += (searchmemstart - df_memory_start);
|
|
dump(nextoffset);
|
|
size = searchmemend - searchmemstart;
|
|
dump(size);
|
|
|
|
while (1)
|
|
{
|
|
// for speed, if we start with a raw byte, use a builtin function
|
|
// to skip ahead to the next actual occurance.
|
|
if (token1 <= 0xFF)
|
|
{
|
|
nextoffset = (BYTE *)memchr(nextoffset, token1,
|
|
size - (nextoffset - copy_of_df_memory));
|
|
if (nextoffset == NULL) break;
|
|
}
|
|
|
|
va_start(va, token1);
|
|
foundit = hexsearch_match2(nextoffset, token1, va);
|
|
va_end(va);
|
|
|
|
if (foundit)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if ((DWORD)(++nextoffset - copy_of_df_memory - (searchmemstart - df_memory_start)) >= size)
|
|
{
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (!foundit)
|
|
{
|
|
d_printf("hexsearch2(%X", token1);
|
|
i = 0;
|
|
va_start(va, token1);
|
|
do
|
|
{
|
|
d_printf(",%X", token = va_arg(va, DWORD));
|
|
} while (EOL != token && i++ < 64);
|
|
va_end(va);
|
|
if (i >= 64) d_printf("...");
|
|
d_printf(")\n");
|
|
d_printf("search failed!\n");
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
here[i] = 0;
|
|
target[i] = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
here[0] = (DWORD)foundit;
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
if (i < nexthere && here[i] != 0)
|
|
{
|
|
here[i] += df_memory_start - (DWORD)copy_of_df_memory;
|
|
}
|
|
else here[i] = 0;
|
|
}
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
if (i < nexttarget && target[i] != 0)
|
|
{
|
|
target[i] += df_memory_start - (DWORD)copy_of_df_memory;
|
|
}
|
|
else target[i] = 0;
|
|
}
|
|
|
|
return df_memory_start + (foundit - copy_of_df_memory);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
void set_hexsearch2_limits(DWORD start, DWORD end)
|
|
{
|
|
if (end < start) end = start;
|
|
|
|
searchmemstart = start >= df_memory_start ? start : df_memory_start;
|
|
searchmemend = end >= df_memory_start && end <= df_memory_end ?
|
|
end : df_memory_end;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// helper function for patch and verify. does the actual work.
|
|
static BOOL vpatchengine(BOOL mode, DWORD address, va_list va)
|
|
{
|
|
// mode: TRUE == patch, FALSE == verify
|
|
DWORD next = address, a, c;
|
|
// a is token, c is helper value, b is asm instruction byte to use.
|
|
BYTE b;
|
|
BOOL ok = FALSE;
|
|
|
|
while (1)
|
|
{
|
|
//if (length == 0) { ok = TRUE; break; }
|
|
//length--;
|
|
a = va_arg(va, unsigned int);
|
|
|
|
if (a == EOL)
|
|
{
|
|
ok = TRUE;
|
|
break;
|
|
}
|
|
|
|
if (a == DWORD_)
|
|
{
|
|
//if (length-- == 0) break;
|
|
c = va_arg(va, unsigned int);
|
|
if (mode ? !poked(next, c) : c != peekd(next)) break;
|
|
next += 4;
|
|
continue;
|
|
}
|
|
|
|
if (a == CALL || a == JUMP)
|
|
{
|
|
b = (a == CALL ? 0xE8 : 0xE9);
|
|
//if (length-- == 0) break;
|
|
c = va_arg(va, unsigned int) - (next + 5);
|
|
if (mode ? !pokeb(next, b) : b != peekb(next)) break;
|
|
// do NOT merge the next++ into the previous if statement.
|
|
next++;
|
|
if (mode ? !poked(next, c) : c != peekd(next)) break;
|
|
next += 4;
|
|
continue;
|
|
}
|
|
|
|
if (a == JZ || a == JNZ)
|
|
{
|
|
b = (a == JZ ? 0x84 : 0x85);
|
|
//if (length-- == 0) break;
|
|
c = va_arg(va, unsigned int) - (next + 6);
|
|
if (mode ? !pokeb(next, 0x0F) : 0x0F != peekb(next)) break;
|
|
next++;
|
|
if (mode ? !pokeb(next, b) : b != peekb(next)) break;
|
|
next++;
|
|
if (mode ? !poked(next, c) : c != peekd(next)) break;
|
|
next += 4;
|
|
continue;
|
|
}
|
|
|
|
if (a <= 0xFF)
|
|
{
|
|
if (mode ? !pokeb(next, a) : a != peekb(next)) break;
|
|
next++;
|
|
continue;
|
|
}
|
|
|
|
d_printf("vpatchengine: unsupported token: %08X\n", a);
|
|
break; // unsupported token
|
|
}
|
|
|
|
// d_printf("vpatchengine returning %d, length is %d, next is %08X\n",
|
|
// ok, length, next);
|
|
return(ok);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// patch() and verify() support a modified subset of hex_search() tokens:
|
|
//
|
|
// 0x00 - 0xFF: poke the byte.
|
|
// EOL: end-of-list. terminate when this token is reached.
|
|
// DWORD_: Followed by a DWORD, not a byte. Poke the DWORD.
|
|
// CALL: given an _address_; pokes near call with the proper _delta_.
|
|
// JUMP: given an _address_; pokes near jump with the proper _delta_.
|
|
// JZ: given an _address_; assembles a near (not short) jz & delta.
|
|
// JNZ: given an _address_; assembles a near jnz & delta.
|
|
//
|
|
// Particularly note that, unlike hex_search(), CALL, JUMP, JZ, and JNZ
|
|
// are followed by a dword-sized target address.
|
|
//
|
|
// Note that patch() does its own verify(), so you don't have to.
|
|
//
|
|
// TODO: doing so many individual pokes and peeks is slow. Consider
|
|
// building a copy, then doing a pokearb/peekarb.
|
|
|
|
// Make an offset in Dwarf Fortress have certain bytes.
|
|
BOOL patch2(DWORD address, ...)
|
|
{
|
|
va_list va;
|
|
BOOL ok;
|
|
|
|
va_start(va, address);
|
|
ok = vpatchengine(TRUE, address, va);
|
|
va_end(va);
|
|
|
|
va_start(va, address);
|
|
if (ok) ok = vpatchengine(FALSE, address, va);
|
|
va_end(va);
|
|
return(ok);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Check that an offset in Dwarf Fortress has certain bytes.
|
|
//
|
|
// See patch() documentation.
|
|
BOOL verify2(DWORD address, ...)
|
|
{
|
|
BOOL ok;
|
|
va_list va;
|
|
va_start(va, address);
|
|
ok = vpatchengine(FALSE, address, va);
|
|
va_end(va);
|
|
return(ok);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Get the exe name of the DF executable
|
|
static DWORD get_exe_base(DWORD pid)
|
|
{
|
|
HANDLE snap = INVALID_HANDLE_VALUE;
|
|
PROCESSENTRY32 process = {0};
|
|
MODULEENTRY32 module = {0};
|
|
|
|
do
|
|
{
|
|
snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap == INVALID_HANDLE_VALUE)
|
|
{
|
|
errormessage = "CreateToolhelp32Snapshot(Process) failed.";
|
|
break;
|
|
}
|
|
|
|
// Get the process exe name
|
|
process.dwSize = sizeof(PROCESSENTRY32);
|
|
if (!Process32First(snap, &process))
|
|
{
|
|
errormessage = "Process32First(snapshot) failed.";
|
|
break;
|
|
}
|
|
do
|
|
{
|
|
if (process.th32ProcessID == pid)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
//d_printf("Process: %d \"%.*s\"", process.th32ProcessID, MAX_PATH, process.szExeFile);
|
|
}
|
|
} while (Process32Next(snap, &process));
|
|
if (process.th32ProcessID != pid)
|
|
{
|
|
errormessage = "Process32List(snapshot) Couldn't find the target process in the snapshot?";
|
|
break;
|
|
}
|
|
|
|
d_printf("Target Process: %d \"%.*s\"", process.th32ProcessID, MAX_PATH, process.szExeFile);
|
|
CloseHandle(snap);
|
|
|
|
snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
|
|
if (snap == INVALID_HANDLE_VALUE)
|
|
{
|
|
errormessage = "CreateToolhelp32Snapshot(Modules) failed.";
|
|
break;
|
|
}
|
|
|
|
// Now find the module
|
|
module.dwSize = sizeof(MODULEENTRY32);
|
|
if (!Module32First(snap, &module))
|
|
{
|
|
errormessage = "Module32First(snapshot) failed.";
|
|
break;
|
|
}
|
|
do
|
|
{
|
|
if (!stricmp(module.szModule, process.szExeFile))
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
//d_printf("Module: %08X \"%.*s\" \"%.*s\"", (DWORD)(SIZE_T)module.modBaseAddr, MAX_MODULE_NAME32 + 1, module.szModule, MAX_PATH, module.szExePath);
|
|
}
|
|
} while (Module32Next(snap, &module));
|
|
if (stricmp(module.szModule, process.szExeFile))
|
|
{
|
|
errormessage = "Module32List(snapshot) Couldn't find the target module in the snapshot?";
|
|
break;
|
|
}
|
|
d_printf("Target Module: %08X \"%.*s\" \"%.*s\"", (DWORD)(SIZE_T)module.modBaseAddr, MAX_MODULE_NAME32 + 1, module.szModule, MAX_PATH, module.szExePath);
|
|
} while (0);
|
|
|
|
if (snap != INVALID_HANDLE_VALUE)
|
|
{
|
|
CloseHandle(snap);
|
|
}
|
|
|
|
if (errormessage != NULL && strlen(errormessage) != 0)
|
|
{
|
|
d_printf("%s\n", errormessage);
|
|
return 0;
|
|
}
|
|
|
|
return (DWORD)(SIZE_T)module.modBaseAddr;
|
|
}
|
|
|
|
// ============================================================================
|
|
void cleanup(void)
|
|
{
|
|
|
|
if (copy_of_df_memory != NULL)
|
|
{
|
|
GlobalFree(copy_of_df_memory);
|
|
copy_of_df_memory = NULL;
|
|
}
|
|
|
|
if (suspended)
|
|
{
|
|
if ((DWORD)-1 == ResumeThread(df_h_thread))
|
|
{
|
|
// do what? apologise?
|
|
}
|
|
else suspended = FALSE;
|
|
}
|
|
#if 0
|
|
// do this incase earlier runs crashed without executing the atexit handler.
|
|
// TODO: trap exceptions?
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
ResumeThread(df_h_thread);
|
|
#endif
|
|
|
|
if (df_h_thread != NULL)
|
|
{
|
|
CloseHandle(df_h_thread);
|
|
df_h_thread = NULL;
|
|
}
|
|
if (df_h_process != NULL)
|
|
{
|
|
CloseHandle(df_h_process);
|
|
df_h_process = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
BOOL open_dwarf_fortress(void)
|
|
{
|
|
HANDLE df_main_wh;
|
|
char *state;
|
|
|
|
change_page_permissions = FALSE;
|
|
|
|
atexit(cleanup);
|
|
|
|
df_memory_base = 0x400000; // executables start here unless
|
|
// explicitly relocated.
|
|
errormessage = "";
|
|
|
|
|
|
// trigger a GPF
|
|
//*(char *)(0) = '!';
|
|
|
|
// trigger an invalid instruction (or DEP on newer processors).
|
|
// 0F 0B is the UD1 pseudo-instruction, explicitly reserved as an invalid
|
|
// instruction to trigger faults.
|
|
//asm(".fill 1, 1, 0x0F; .fill 1, 1, 0x0B");
|
|
|
|
state = "calling FindWindow()... ";
|
|
d_printf(state);
|
|
df_main_wh = FindWindow("OpenGL","Dwarf Fortress");
|
|
d_printf("done\n");
|
|
dump(df_main_wh);
|
|
if (df_main_wh == 0)
|
|
{
|
|
df_main_wh = FindWindow("SDL_app","Dwarf Fortress");
|
|
if (df_main_wh == 0)
|
|
{
|
|
errormessage = "FindWindow(Dwarf Fortress) failed.\nIs the game running?";
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
state = "calling GetWindowThreadProcessId()... ";
|
|
d_printf(state);
|
|
df_main_win_tid = GetWindowThreadProcessId(df_main_wh, &df_pid);
|
|
d_printf("done\n");
|
|
dump(df_pid);
|
|
dump(df_main_win_tid);
|
|
|
|
if (df_main_win_tid == 0) {
|
|
errormessage =
|
|
"GetWindowThreadProcessId(Dwarf Fortress) failed.\n"
|
|
"That should not have happened!";
|
|
return FALSE;
|
|
}
|
|
|
|
state = "calling OpenProcess()... ";
|
|
d_printf(state);
|
|
df_h_process = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_VM_OPERATION | PROCESS_SUSPEND_RESUME | PROCESS_QUERY_INFORMATION, FALSE, df_pid);
|
|
d_printf("done\n");
|
|
dump(df_h_process);
|
|
if (df_h_process == NULL)
|
|
{
|
|
errormessage = "OpenProcess(Dwarf Fortress) failed.\n"
|
|
"Are you the same user that started the game?";
|
|
return(FALSE);
|
|
}
|
|
|
|
state = "calling OpenThread()... ";
|
|
d_printf(state);
|
|
df_h_thread = OpenThread(THREAD_ALL_ACCESS, FALSE, df_main_win_tid);
|
|
d_printf("done\n");
|
|
dump(df_h_thread);
|
|
if (df_h_thread == NULL)
|
|
{
|
|
errormessage = "OpenThread(Dwarf Fortress) failed.";
|
|
return(FALSE);
|
|
}
|
|
|
|
// TODO enum and suspend all threads?
|
|
state = "calling SuspendThread()... ";
|
|
d_printf(state);
|
|
if ((DWORD)-1 == SuspendThread(df_h_thread))
|
|
{
|
|
errormessage = "SuspendThread(Dwarf Fortress) failed.";
|
|
return(FALSE);
|
|
}
|
|
d_printf("done\n");
|
|
suspended = TRUE;
|
|
|
|
// get the base of the df executable
|
|
df_memory_base = get_exe_base(df_pid);
|
|
if (!df_memory_base)
|
|
{
|
|
errormessage = "GetModuleBase(Dwarf Fortress) failed.";
|
|
return(FALSE);
|
|
}
|
|
dump(df_memory_base);
|
|
|
|
// we could get this from the PE header, but why bother?
|
|
df_memory_start = df_memory_base + 0x1000;
|
|
dump(df_memory_start);
|
|
|
|
// df_memory_end is based on the SizeOfImage field from the in-memory
|
|
// copy of the PE header.
|
|
df_memory_end = df_memory_base + peekd(df_memory_base+peekd(df_memory_base+0x3C)+0x50)-1;
|
|
dump(df_memory_end);
|
|
|
|
state = "calling GlobalAlloc(huge)... ";
|
|
d_printf(state);
|
|
dump(df_memory_end - df_memory_start + 0x100);
|
|
if (NULL == (copy_of_df_memory = (BYTE *)GlobalAlloc(GPTR, df_memory_end - df_memory_start + 0x100)))
|
|
{
|
|
errormessage = "GlobalAlloc() of copy_of_df_memory failed";
|
|
return FALSE;
|
|
}
|
|
d_printf("done\n");
|
|
|
|
state = "copying memory... ";
|
|
if (!peekarb(df_memory_start, copy_of_df_memory, df_memory_end-df_memory_start))
|
|
{
|
|
d_printf("peekarb(entire program) for search()'s use failed\n");
|
|
return FALSE;
|
|
}
|
|
|
|
set_hexsearch2_limits(0, 0);
|
|
|
|
return(TRUE);
|
|
}
|
|
|
|
|
|
// not (normally) necessary because it is done at exit time by atexit().
|
|
// however you can call this to resume DF before putting up a dialog box.
|
|
void close_dwarf_fortress(void)
|
|
{
|
|
cleanup();
|
|
}
|
|
|