Write a standalone patch - Part 2
 
1. Extra features for patcher


Our previous patch program was a bit basic, so in this next part we are going to add a couple of features. :)

First of all, we don't want to be bothered with the offset anymore. (In the previous patch tut, we had to find the file-offset of the bytes we wanted to patch) So we will use a string of opcodes to search for now. This has the advantage, that minor changes in the code, won't mess up our patch! We don't depend on the fileoffset any more :)

Next, we would like to make our patch program 'usable', so we will make the 'FileToOpen' and 'OpcodeString' variables.
To open the file, we will use a Common DialogBox.
To get the OpcodeStrings, we display 2 edit-boxes.

So, this is how I'm going to program this patcher :

=> Open File with GetOpenFileName

=> Get the size of the file (GetFileSize)

=> Reserve the neccessary memory to load the file into memory (GlobalAlloc)

=> Load file in memory (ReadFile)

=> Search for the given string when button is pressed, and if a string has been inserted.

=> Output message (MessageBox)

=> Close file, free the used memory again, and close program.

2. API Theory


If we want a user-friendly way to open a file, we don't have to look far! Windows has these 'common' dialogboxes predefined. Before we can start using it, a little bit of theory is neccessary :


The API we need for calling an Open-DialogBox is :

BOOL GetOpenFileName(

LPOPENFILENAME lpofn // address of structure with initialization data
);

There is only one parameter, a pointer to an OPENFILENAME structure.
Let's take a look at this struct :

typedef struct tagOFN { // ofn
DWORD lStructSize;
HWND hwndOwner;
HINSTANCE hInstance;
LPCTSTR lpstrFilter;
LPTSTR lpstrCustomFilter;
DWORD nMaxCustFilter;
DWORD nFilterIndex;
LPTSTR lpstrFile;
DWORD nMaxFile;
LPTSTR lpstrFileTitle;
DWORD nMaxFileTitle;
LPCTSTR lpstrInitialDir;
LPCTSTR lpstrTitle;
DWORD Flags;

WORD nFileOffset;
WORD nFileExtension;
LPCTSTR lpstrDefExt;
DWORD lCustData;
LPOFNHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} OPENFILENAME;

Again, if you want to know all the details about the parameters, check the win32.hlp reference. It's very easy to understand.
I will use only a few members of this struct. Look at the code to see which ones :)

The most important one is lpstrFile, because it will contain the File name and Path of the chosen file. And we will need this parameter for the CreateFile API. 'if you don't know how to use this API, check the previous tutorial)


Next API we need is GetFileSize

DWORD GetFileSize(

HANDLE hFile, // handle of file to get size of
LPDWORD lpFileSizeHigh // address of high-order word for file size
);

The filesize is returned in eax.

hFile is the Handle CreateFile has returned to us.
lpFileSizeHigh can be NULL, if you ever want to patch files larger than 4Gb, then you should use this parameter too! But I assume that you won't try to open a File so big. If you try it anyway, you'd better have a lot of memory! :)


If we want to load the file in memory, we have to reserve enough memory first. We can do that with the API GlobalAlloc

HGLOBAL GlobalAlloc(

UINT uFlags, // object allocation attributes
DWORD dwBytes // number of bytes to allocate
);

uFlags = GMEM_FIXED
Allocates fixed memory
dwBytes
= FileSize
we got the FileSize from GetFileSize API.
The API returns a pointer to the memoryblock just created.

At the end of the program, we will free this memory again :

HGLOBAL GlobalFree(

HGLOBAL hMem // handle to the global memory object
);

Only one parameter, the pointer you got from GlobalAlloc.


The last API we haven't discussed yet is the SetFilePointer. U can guess for what we will use this one, no ? :)

DWORD SetFilePointer(

HANDLE hFile, // handle of file
LONG lDistanceToMove, // number of bytes to move file pointer
PLONG lpDistanceToMoveHigh, // address of high-order word of distance to move
DWORD dwMoveMethod // how to move
);

Indeed this API is used to place our Filepointer at the right position in the File.

The hFile handle is the one we got from CreateFile
lDistanceToMove = We will have to store this value in our search Algorithm
dw MoveMethod = FILE_BEGIN ; We start counting from the beginning of the File.

3. The serach algorithm


The most important part of the program is the string-search-part.
The easiest way to check the entire file for a string, is to use the REPZ SCASB. To completely understand how to use this command, read the followin theory first :

REPNZ SCAS m32 ; Find EAX, starting at ES:[(E)DI]

Description

Repeats a string instruction the number of times specified in the count register ((E)CX) or until the indicated condition of the ZF flag is no longer met. The REP (repeat), REPE (repeat while equal), REPNE (repeat while not equal), REPZ (repeat while zero), and REPNZ (repeat while not zero) mnemonics are prefixes that can be added to one of the string instructions. The REP prefix can be added to the INS, OUTS, MOVS, LODS, and STOS instructions, and the REPE, REPNE, REPZ, and REPNZ prefixes can be added to the CMPS and SCAS instructions. (The REPZ and REPNZ prefixes are synonymous forms of the REPE and REPNE prefixes, respectively.) The behavior of the REP prefix is undefined when used with non-string instructions.

The REP prefixes apply only to one string instruction at a time. To repeat a block of instructions, use the LOOP instruction or another looping construct.
All of these repeat prefixes cause the associated instruction to be repeated until the count in register (E)CX is decremented to 0 (see the following table). (If the current address-size attribute is 32, register ECX is used as a counter, and if the address-size attribute is 16, the CX register is used.) The REPE, REPNE, REPZ, and REPNZ prefixes also check the state of the ZF flag after each iteration and terminate the repeat loop if the ZF flag is not in the specified state. When both termination conditions are tested, the cause of a repeat termination can be determined either by testing the (E)CX register with a JECXZ instruction or by testing the ZF flag with a JZ, JNZ, and JNE instruction.

 

Did you get all this? Let's quickly summarize this :

ECX = FileSize
EDI = Pointer to the data that needs to be examined (our File)
ESI = The string we are looking for
AL = First byte of data in EDI

The command continues until ECX = 0 (Entire file has been searched) or until the Zero-flag is set. (when we have a match)
the pointer in EDI is incremented every cycle.


Here is the Search Algorithm:

        mov edi,pointer-to-memoryblock   ;move pointer to memory to edi
        mov ecx,FileSize                 ;move Filesize to ecx
        mov esi,offset string            ;set ESI to the Opcode string we search
        mov al, byte ptr [esi]           ;move the first byte of the string to AL

SEARCH :
        repnz scasb                      ;repeat until ECX=0 or AL equals
                                         ;the value of the byte [EDI], EDI is 
                                         ;incremented by 1 every run
cmp ecx,0 ;If ECX=0, no matching string is found
jz NOT_IN_HERE
FOUND_A_MATCH : ;found matching byte pushad ;save registers dec edi ;EDI-1 because REPZ added 1 byte to much mov ecx,StrLen ;ECX = length of the string repz cmpsb ;repeat until the values in the memory of ;[EDI] and [ESI] are not equal, or ecx=0
cmp ecx,0 ;If ecx = 0, we have found the correct string
jz PATCH_IT popad ;Restore registers for continuing search
jmp SEARCH ;go on with search PATCH_IT : dec edi ;EDI - 1
inc ecx ;ECX + 1
mov eax,fsize
sub eax,ecx ;compute the File Offset to patch ...

NOT_IN_HERE : ...
4. The code...

Get the code and binaries here :

.386
.model flat,stdcall
option casemap:none
include \masm32\include\comdlg32.inc
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
Convert proto :DWORD,:DWORD

 

.const
MAXSTR equ 100
MAXSIZE equ 260
ButtonOpenID equ 100
ButtonPatchID equ 101
StringID equ 200
NStringID equ 201

.data
ClassName db "WinClass",0

NotEqual db "Length of strings must be equal",0
Error db "Error!",0
NoMem db "Error allocating memory",0
FilterString db "All Files",0,"*.*",0
db "Executable Files",0,"*.exe",0,0
FilePath db 260 dup (0)
Caption db "Choose the file to patch :",0
AppName db "Patcher by Detten",0
Done db "File patched succesfully !",0
NoFile db "Can't find the file !",0
WrFile db "Error writing to file !",0

ofn OPENFILENAME <>

FSize dd 0
NString db 50 dup (0)
OString db 50 dup (0)
StrLenA dd 0
StrLen dd 0

ButtonOpen db "Open Target",0
ButtonPatch db "Patch Target",0
EditClassName db "edit",0
ButtonClassName db "button",0


StringBuf db MAXSTR dup (0)
NStringBuf db MAXSTR dup (0)
NotFound db "The given string is not Found !",0


.data?
hwndOpen HWND ?
hwndPatch HWND ?
hwndString HWND ?
hwndNString HWND ?
hInstance HINSTANCE ?
hwndname HWND ?
hFile HANDLE ?
Numb dd ?
FPointer dd ?

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

LOCAL wc:WNDCLASSEX ; create local variables on stack
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax

invoke RegisterClassEx, addr wc ; register our window class

invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,
CW_USEDEFAULT,280,150,NULL,NULL,hInstance,NULL ; Create the window
mov hwnd,eax

invoke ShowWindow, hwnd,CmdShow ; display our window
invoke UpdateWindow, hwnd

.WHILE TRUE ; The MessageLoop
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW


mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

.IF uMsg==WM_CREATE

; Create the 2 buttons, and 2 editboxes
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
30,15,210,20,hWnd,StringID,hInstance,NULL
mov hwndString,eax

invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
30,40,210,20,hWnd,NStringID,hInstance,NULL
mov hwndNString,eax

invoke SetFocus, hwndString
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonOpen,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
50,70,85,20,hWnd,ButtonOpenID,hInstance,NULL
mov hwndOpen,eax

invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonPatch,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
150,70,85,20,hWnd,ButtonPatchID,hInstance,NULL
mov hwndPatch,eax



.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED

.IF ax==ButtonOpenID ; Open button clicked?
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFile, OFFSET FilePath
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET Caption

invoke GetOpenFileName, ADDR ofn

.IF eax == TRUE
; Open the file
invoke CreateFile, ofn.lpstrFile, GENERIC_READ OR GENERIC_WRITE, FILE_SHARE_READ OR FILE_SHARE_WRITE, NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL

.IF eax!=INVALID_HANDLE_VALUE
mov hFile, eax ; Store handle of file
Invoke GetFileSize, hFile, NULL
mov FSize, eax ; Save FileSize
Invoke GlobalAlloc, GMEM_FIXED, FSize
mov FPointer, eax
.IF eax == NULL ; If no pointer, error message
push MB_OK OR MB_ICONINFORMATION
push OFFSET NoMem
push OFFSET Error
JMP MESSAGE
.ENDIF

Invoke ReadFile, hFile,FPointer,FSize, ADDR Numb, NULL

.ELSE ;If no valid handle, error message
push MB_OK OR MB_ICONINFORMATION
push OFFSET NoFile
push OFFSET Error
jmp MESSAGE
.ENDIF

.ENDIF

.ELSEIF ax == ButtonPatchID ; Patch Button Pressed?
Invoke GetDlgItemText,hWnd,StringID,ADDR StringBuf,MAXSTR ; Get String1
mov StrLenA, eax ; Save string length
Invoke Convert,ADDR StringBuf, ADDR OString ; Convert to bytes
Invoke GetDlgItemText,hWnd,NStringID,ADDR NStringBuf,MAXSTR ;Get string2
.IF eax != StrLenA ; both strings equal?
push MB_OK OR MB_ICONINFORMATION ; If not, error message
push OFFSET Error
push OFFSET NotEqual
JMP MESSAGE
.ENDIF
Invoke Convert,ADDR NStringBuf, ADDR NString ; Convert to bytes


mov edi,FPointer ;move pointer to memory to edi
mov ecx,FSize ;move Filesize to ecx
mov esi,offset OString ;set ESI to the Opcode string we search
mov al, byte ptr [esi] ;move the first byte of the string to AL

SEARCH :
repnz scasb ;repeat until ECX=0 or AL equals
;the value of the byte [EDI], EDI is
;incremented by 1 every run

cmp ecx,0 ;If ECX=0, no matching string is found

jz NOT_FOUND


FOUND_A_MATCH : ;found matching byte
push ecx ;save registers
push edi
push esi
dec edi ;EDI-1 because REPZ added 1 byte to much
mov ecx,StrLen ;ECX = length of the string
repz cmpsb ;repeat until the values in the memory o ;[EDI] and [ESI] are not equal, or ecx=0
cmp ecx,0 ;If ecx = 0, we have found the correct string

jz PATCH_IT

pop esi ;Restore registers for continuing search
pop edi
pop ecx
jmp SEARCH ;go on with search

PATCH_IT :
pop esi
pop edi
pop ecx
dec edi ;EDI - 1
inc ecx ;ECX + 1
mov eax,FSize
sub eax,ecx ;compute the File Offset to patch (FileSize - Remaining bytes (ecx) = Offset to patch)

Invoke SetFilePointer, hFile, eax, NULL, FILE_BEGIN
Invoke WriteFile, hFile,offset NString, StrLen, ADDR Numb, NULL
mov eax, Numb
.IF eax == StrLen ; bytes written = Bytes to write ?
push MB_OK ; If so, success-message
push OFFSET AppName
push OFFSET Done
JMP MESSAGE
.ELSE
push MB_OK OR MB_ICONINFORMATION ; If not, error message
push OFFSET Error
push OFFSET WrFile
.ENDIF

NOT_FOUND :
push MB_OK OR MB_ICONINFORMATION ; If no handle, error message
push OFFSET Error
push OFFSET NotFound

MESSAGE :

push NULL
Call MessageBox
.ENDIF

.ENDIF


.ELSEIF uMsg==WM_DESTROY ; Close program
invoke CloseHandle, hFile ; Release handle
invoke GlobalFree, Fpointer ; Release memory block
invoke ExitProcess,eax ; Exit
invoke PostQuitMessage,NULL

.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret

WndProc endp


; This routine converts the ascii strings to their byte equivalent. eg
string 7415FF -> 74h,15h,FFh
Use only numbers, and uppercase letters (A,B,C,D,E,F)

Convert proc LpBuffer:DWORD,LpString:DWORD

push eax
push esi
push ecx
push edx
mov esi, LpBuffer
mov edx, LpString
xor ecx, ecx

MORE :

MOV al, [esi]

.IF al>29h
.IF al<40h
AND al, 0Fh
IMUL eax, 10h
.ELSE
.IF al>64
.IF al<71
SUB al, 55
IMUL eax, 10h
.ENDIF
.ENDIF
.ENDIF
.ENDIF

MOV byte ptr [edx+ecx], al
INC esi
mov al, [esi]


.IF al >29h
.IF al < 40h
AND al, 0Fh
ADD byte ptr [edx+ecx], al
.ELSE
.IF al > 64
.IF al < 71
SUB al, 55
ADD byte ptr [edx+ecx], al
.ENDIF
.ENDIF
.ENDIF
.ENDIF

.IF byte ptr [edx+ecx] != NULL
INC esi
INC ecx
JMP MORE
.ENDIF
mov StrLen, ecx

pop edx
pop ecx
pop esi
pop eax
ret
Convert endp

end start

 


That's it for the patcher. Of course, a couple of features could be added. Like the choice if you want to enter opcodes or text, or a feature that displays the opcode of the patched bytes,...

Feel free to mail me if you have questions or remarks about this tutorial.

Greetings,

Detten
Detn@hotmail.com