Hands on tutorial for inline patching packed targets.
Target: Zoom Player Professional (inmatrix.com)
Tools used:
OllyDbg 1.09d
PE iDentifier v0.91
Note. A product version (as the product itself) doesn't matter, but it's important that all worked and you understand what you do. The second thing that you have to keep in mind is what all the addresses, values and numbers, which corresponds to a disassembled code are represented in hexadimal system, the rest numbers are decimal.
Author: Wizard
Date: 16.04.2004
Difficulty: beginner
Origin: Don't wish it were easier, wish you were better. Michelangelo
Introduction
Zoom Player Professional is one of the most advanced Media Players and DVD Front-End on the PC today, Designed to be simple at first glance while being remarkably dynamic and flexible when used to its full potential.
Zoom Player works in two modes. A Media mode which can play any file supported by DirectShow (any file which plays in MediaPlayer) and a DVD mode which uses pre-installed DirectShow DVD filters to play DVD content.
Reconnoitre
First of all, I downloaded file zp331pro.exe from the official site and then installed it. Once I've done that, I checked out the main executable (zplayer.exe) file. It was sorta packed. To be sure in that I used PEiD to detect the exact packer. PEid detected some old UPX version, maybe because it was modified a bit, but maybe it was so indeed. Anyway PEid said this: "UPX 0.89.6 - 1.02 / 1.05 - 1.24 (Delphi) stub -> Markus & Laszlo".
Now run the program to see the startup NAG.
You have to wait for 3 seconds and then press the appropriate button. Once you've pressed the button - program starts and runs fine. Check what do we have. Go to the options menu by pressing Ctrl+P or by clicking the appropriate button on the player's panel (at the bottom-right corner).
Click information bar in the left column. We see the strings: "Registered to: This copy is unregistered". You may also notice the register button at the bottom of the form. If you press it, it will bring you to the internet register page. That means, if the program hasn't a password/name dialog it should use some other registration way.
Usually it's a registration file (keyfile). Why? Simple, why does it need inet for? Of course to register you as a legal user for money. After that the registered user will receive some file with key and he'll just put it into the program's folder (usually in the root). It's 99% of cases. Nobody (I mean "any" programs' authors) won't do such tricks as hidden password dialog somewhere by pressing the magic keys combinations or something else even more tricky. That's because usual users don't need a haemorrhoids in their ass, which they couldn't heal. Other words, end-users need simple registration procedure. Of course, it's only my subjective point of view and I can be wrong, but I just confide in my intuition and rely on my experience.
Get a spade and dig
Fire up Olly, press F3 and open file zplayer.exe from your ZoomPlayer directory (e.g. "C:Program FilesZoom Player"). You'll see this dialog, telling that the content (of our executable) could be compressed with some packer.
Press "Yes" button to start analyzing the code. Once it's done you will be directly at UPX's entry point (00683ED0).
00683ED0 : 60 PUSHAD ; <= you're here
00683ED1 : BE00C05C00 MOV ESI,zplayer.005CC000
00683ED6 : 8DBE0050E3FF LEA EDI,DWORD PTR DS:[ESI+FFE35000]
00683EDC : C787C4B01C00 MOV DWORD PTR DS:[EDI+1CB0C4],80067C41
00683EE6 : 57 PUSH EDI
00683EE7 : 83CDFF OR EBP,FFFFFFFF
00683EEA : EB0E JMP SHORT zplayer.00683EFA
... skipped ...
All we need now is to find Real (Original) Entry Point (REP/OEP). You might ask: how to do that? Well, it depends on a cracker, his experience and intuition. Each man will do different. As we know that the program is packed with UPX we can simply scroll down with PageDown key. Personally I pressed it 8 times until I came to the right place. There's also the most simplest method to get there. Press Ctrl+F to start find command dialog and type in it "POPAD", but drop the flag "Entire block", press "Find" button to search the instruction... We're right at the place.
... skipped ...
0068402B : 55 PUSH EBP
0068402C : FF96 7CA62800 CALL DWORD PTR DS:[ESI+28A67C]
00684032 : 09C0 OR EAX,EAX
00684034 : 74 07 JE SHORT zplayer.0068403D
00684036 : 8903 MOV DWORD PTR DS:[EBX],EAX
00684038 : 83C3 04 ADD EBX,4
0068403B : EB D8 JMP SHORT zplayer.00684015
0068403D : FF96 80A62800 CALL DWORD PTR DS:[ESI+28A680]
00684043 : 61 POPAD ; <= the place we've found
00684044 : E9 9774F4FF JMP zplayer.005CB4E0 ; jump to the REP
00684049 : 00 DB 00
0068404A : 00 DB 00
0068404B : 00 DB 00
We searched for "POPAD" (61) instruction, because the program is packed with UPX. I do the same, when unpacking ASPack, but there I need to search a few more times, but for UPX just once (usually), or more times (seldom).
Note. Of course everything could be much harder sometimes, but if it's a standard of UPX (even a little bit modified) you will always find out. Besides, you can try to find the REP (OEP) with automatic tracing, but this way sometimes (very seldom) can be wrong.
Put a breakpoint with F2 at address 00684044. Press F9 to run the proggy. Ok, we're right at the place. Now press F7 or F8 (it doesn't matter now, 'cause it's just a jump) once to get to the REP.
The sneaky arctic fox
We're at the Real Entry Point now.
005CB4E0 : 55 PUSH EBP ; <= the REP itself
005CB4E1 : 8BEC MOV EBP,ESP
005CB4E3 : B9 07000000 MOV ECX,7
005CB4E8 : 6A 00 PUSH 0
005CB4EA : 6A 00 PUSH 0
005CB4EC : 49 DEC ECX
Now, let's remember what inscriptions have we seen at the NAG. We will need only some phrase. Let it be "This is a Trial". We have to search these words in dump. To do this, do a right click at the address we're now, and then select "Follow in Dump/Selection".
Go to the dump window. Its address should be now equal to the REP (005CB4E0). We did it to set the cursor in dump window to one of memory cells of the executable we're cracking. This's very important, 'cause otherwise we won't find our string. That's because such search doesn't cover all the windows memory and you have to select the memory range in which you will search by yourself (just like in SoftIce).
Note. We have set the cursor the that address, because we're now right in the unpacked code. Thus, it has another memory address than UPX entry point. But as we're gonna search for a NAG's string, we must set the cursor to the appropriate memory range. That's because we won't find the string we wanna search in the packed part of the executable. We can only find it in the unpacked memory range.
Press Ctrl+B in dump to start binary search dialog. Type "this is a trial" in ASCII field, set the flag "Entire block" (if you won't set this flag, the search will only go down from the position your cursor has now, but with it the search will cover all the executable related memory). Press "OK" and wait a bit. No luck, mate! Don't worry, just try to search the same string as Unicode. Type the same string, but in UNICODE field. Trying... Success! Personally I found that string at address 00541790.
Note. Never set "Case sensitive" flag, when searching for a string in the memory.
In dump select only the first byte of the string we've found. We select only one byte, because it's always enough to catch a string treatment (if the last is present). Now it should be marked with grey color. Right click on this (selected) byte, then go up through the menu and choose "Breakpoint/Memory, on access".
Now just press F9 to let the program run and debugger will catch the moment, when the string (from NAG) will be read. You will land somewhere in kernel32 module.
77E7AB08 : 0FB702 MOVZX EAX,WORD PTR DS:[EDX] ; <= you're here
77E7AB0B : 8A0418 MOV AL,BYTE PTR DS:[EAX+EBX]
77E7AB0E : 42 INC EDX
... skipped ...
Remove the breakpoint we've set before. To do this, right click at the address you're now in code window (or in dump window) and select "Breakpoint/Remove memory breakpoint" from the appeared pop-up menu.
Once you've done that go to address 77E7AB26 and press F4 to trace right until the command we need. That's because we don't need to run in the cycle below like a squirrel in a wheel.
... skipped ...
77E7AB26 : C2 1800 RETN 18 ; trace out of here
77E7AB29 : 2BF7 SUB ESI,EDI
77E7AB2B : 8975FC MOV DWORD PTR SS:[EBP-4],ESI
77E7AB2E : E901FEFFFF JMP kernel32.77E7A934
After you will out of this call you have to trace with F8 for a long time until you will see the NAG. Yes, that screen we've seen before. We're doing all that stuff, because if the program treats to the trial string (before the dialog), that means the NAG will appear soon.
You maybe wondering, why to do all that tricky stuff, if we can just set a breakpoint to some API call. Well, it's partly correct. First, we don't know on function to set that breakpoint, because it looks like that the program was written in Delphi. We can conclude that because of many unnecessary calls (too deep recursion). Secondly, we just don't have the import table. You may try to Press Ctrl+N to check the names. You will see only kernel names (when you'll be in the range 77E7...) and only the UPX functions (when you'll be in the range 404E..), but there're no real (unpacked) executable names, which can observed. Of course if we were cracking with SoftIce, we would try to set some breakpoint, using our intuition. But for you to say, if this's a Delphi composed proggy then standard API functions won't work, 'cause Delphi uses its own drawing style. You may only try some non-standard functions, like DestroyWindow, CreateWindow or others. All those things you can do with SoftIce, but let's get back to Olly.
Note. I have to warn you. Don't try to make your life easier by pressing F9 to reach the dialog. That won't help, because in such case program just runs. I can give you a hint how to trace "without the brains". Just after you will be out of the call at address 77E7AB26, trace with F8 (yes, just hold it) till you see the NAG. Olly will stop the cursor at the needed address for you.
Olly stopped at address 0057ACA7. Now click "Zoom Player" process in your taskbar to see the NAG. Once you'll see it wait for 3 seconds then press appropriate button to continue tracing.
After the "accident"
Ok, after you pressed the appropriate button Olly took the control again.
0057ACA5 : 8B00 MOV EAX,DWORD PTR DS:[EAX]
0057ACA7 : 8B10 MOV EDX,DWORD PTR DS:[EAX]
0057ACA9 : FF92 EC000000 CALL DWORD PTR DS:[EDX+EC] ; the NAG itself
0057ACAF : 837D DC00 CMP DWORD PTR SS:[EBP-24],0 ; <= you should be here
0057ACB3 : 0F84FA000000 JE zplayer.0057ADB3
0057ACB9 : B201 MOV DL,1
Take a look around. If we're here (at 0057ACAF) then it must be a way to get here. Scroll a bit up to check, if is there something interesting.
0057AC19 : E8 E2460100 CALL zplayer.0058F300
0057AC1E : 803D90315D0000 CMP BYTE PTR DS:[5D3190],0 ; if (@[5D3190]==0) badboy
0057AC25 : 0F8584000000 JNZ zplayer.0057ACAF ; jumps if registered
0057AC2B : 807DC3 01 CMP BYTE PTR SS:[EBP-3D],1
0057AC2F : 7555 JNZ SHORT zplayer.0057AC86
Do you see what I see? At address 0057AC1E we see an interesting compare. It looks like after that compare the conditional jump at address 0057AC25 won't jump, because we're not registered users. Other words, that jump "should" direct the program over the NAG, right at address where we're now (0057ACAF). That also means that the memory cell with address [5D3190] contains the registration status (0 ain't registered, > 0 is registered).
All we need to know now is when the program changes the status to unregistered. Other words, when it will write zero value in that memory cell.
A simple majority
Press Ctrl+F2 to terminate the program. We need to start the program again to catch a moment, when the cell [5D3190] will be written with 0 (unregistered status). But if we had seen the NAG already we won't catch that moment. That's why we have to start it over. So, run the program again. You will break before the REP at our first breakpoint (at 00684044). Press F8 once. Now we're at the start of the real (unpacked code) again at 005CB4E0.
Now go to dump window. Press Ctrl+G, type 5D3190 and press "OK" to go for it. You will be at the right place.
Do the same as we do above (when catching a treatment to the trial string). Other words, first, right click the selected byte (at 5D3190) in dump window. After that select "Breakpoint/Memory, on write". We need only to know when the program will write zero in the memory cell. We already know, where it will read it. Besides, we don't need the last thing at all, because if we patch the memory cell at the right place once (change unregistered status to registered) we'll get registered program without caring about where it will read that memory cell next time to avoid the NAG (maybe also enable disabled/hidden features, whatever).
Let the program run with F9... Bang! Olly broke-up here.
0057A28D : C645C300 MOV BYTE PTR SS:[EBP-3D],0
0057A291 : C60590315D0000 MOV BYTE PTR DS:[5D3190],0 ; <= Olly broke-up here
0057A298 : 8D854CFDFFFF LEA EAX,DWORD PTR SS:[EBP-2B4]
0057A29E : 8B15 48275D00 MOV EDX,DWORD PTR DS:[5D2748] ; points to "zplayer.regkey"
Aha, let's remember this place as the first place we've met unregistered state saving. By the way, if you'll trace a bit down you'll see that the program tries to load file "zplayer.regkey" and then to check it for validity. But we're keep going, 'cause we don't care about that key much, our goal is to register program without finding valid serials/keys, etc., but with reversing the unregistered state to registered. That's why keep on going by pressing F9 to see next place (if the program has it).
Trying... Program ran and NAG has appeared. Ok, that means here's a classic algorithm. First, program moves to a cell unregistered status, and then it tries to load a license file/license ini-file/license key from registry (it doesn't matter, 'cause these are only the methods). Secondly, if it fails to load the license key or it's not valid then it just runs as usual, shows NAG, all pay-features are disabled, etc. If the key is valid (after checking it) the program will definitely move the registered status (in our case something > 0 [1..FF], because of that JNZ, remember) and it will be registered.
Note. Everything can be not so easy sometimes. Register state cell could be initialized with unregistered status already (yes, during the compilation). Or there could be a few such memory cells. Besides, there could be not such a simple construction like ours:
CMP BYTE PTR [RegisteredState],0
JNZ GOOD_BOY
But there could be some other compare tricks like this:
MOV EAX,[RegisteredState]
PUSH EAX
... other commands ...
XCHG DWORD PTR [EBP+A],EDX
CMP EDX,00BAD000
JZ BAD_BOY
Or something else even more perky. Everything depends on an author's fantasy. But it doesn't mean that you can't catch it. It only means that everything ain't so simple.
Now we know that we have standard (classic) scheme, which means registered state memory cell can be written only twice (next situations only as examples):
once during the compilation (unregistered/registered state) and then during the program runs (if the key is invalid/valid);
only during the program runs, first, before the key-check procedure and after that if the key is valid/invalid.
Therefore, we just need to replace one byte in command MOV BYTE PTR DS:[5D3190],0 to MOV BYTE PTR DS:[5D3190],1 at address 0057A291.
We don't know exactly what value it should be, but if there was a JNZ instruction, then we suggest that it should be ANY value bigger than zero in byte interval (01..FF).
By knowing all the above, all we've left is to try will it work or not. It's simple to test. Restart the program again. Press F9. It should break at REP once more. Press F7/F8 to get at REP. We're at address 005CB4E0 again. Press Ctrl+G, type 0057A291 (it's the address we've found earlier) in the field, press "OK". Now, press space and type "MOV BYTE PTR DS:[5D3190],1" instead of "MOV BYTE PTR DS:[5D3190],0", press "Assemble" to confirm the changes.
As you can see only one byte is changed (from 00 to 01) at address 0057A297. Remember that fact for further patching. After that press F9 for a beta-testing. Hooray!!! It runs fine without the NAG. Besides, if you will go to the options again you won't find the "Register" button in the information bar. Nevertheless, that string still remains "This copy is unregistered". I suppose that's because the program didn't find the reg-file with code and name, but it thinks that it's registered anyway.
Ok, restart the program again (with Ctrl+F2) and get ready to patch this baby.
Making an inline patch on the fly
Run the program with F9 to stop at well-known breakpoint (at 00684044). We know what to patch and where. All we need is to find a place where to make our inline patch. To do this we have to find what ImageBase has the executable. That info we need to find some free space (a sequence of zeroes), which we will use as our patch.
To find out what ImageBase has the executable open the memory map with Alt+M shortcut. Find the line "zplayer" in owner column and "PE header" in type column.
Notice the value in the address column. It equals 00400000. That's ImageBase itself. Of course you may also click that line and scroll down in the appeared window to find out the same value (it'll have the appropriate name - Image Base). Actually we need this value only to find a free space from the beginning of the executable. Exactly from "MZ" signature (first two bytes of the program). You will always find a sequence of zeroes in any executable's header. It's a reserved space, which never used. Thus, it can be used.
We've got all the info to make a patch. Close the memory map and go to the dump window. Press Ctrl+G, type 00400000 (the value we've got earlier) and press "OK". We're at the beginning of the executable (right at the header's beginning of the unpacked executable). Scroll a bit down and look for a sequence of zeroes. Scrolling... What are we're seeing now? A little space at address 00400080. Well, you can use this space, but you rather scroll a bit down more. There should be a space from 00400300. Scrolling again... Aha! Just like I told you, there's a lot of free space at the forenamed address. Most of executables have it, and exactly from that address.
Go to the code window. Your cursor should still stand at address 00684044. Press space to change the jump from "JMP 005CB4E0" to "JMP 00400300". Other words, we just changing jump from jump to REP to our patch.
Press enter at the command you just changed, to go to 00400300. You see, it's full of zeroes. Let's create our patch now. Press space and type "MOV BYTE PTR [0057A297],1". Drop the flag "Fill with NOP's" and press "Assemble" (don't close assemble window). That's our patch!
The one thing we have to do is to direct the program to the REP. Simply, type "JMP 005CB4E0", press "Assemble" again and close the assemble window.
We're changing 00 to 01 at address [0057A297], because it's the last byte of the command. Other words, it's only the changes we've made to the command. Thus, there's no need to move to the memory the same commands (bytes), but only those, which need to be changed.
We're done for now. All you have to do is to save all modifications to another or to the same file.
The spirit of the sea
I used Zoom Player for a few weeks, but when I started it one day I saw a strange message, which says something about the integrity check. I didn't even treat it seriously and pressed "OK", but the program just exited to my surprise.
After that, I started the program again and I suddenly understood that it's a hidden protection, which wasn't detected before, because it just couldn't be detected. It seems that this somekind of simple self-check of executable. Other words, it's just a simple CRC check of the module content.
All we need to do is to eliminate this unexpected message, which stops the program from running. To do this restart the program once again (if it runs). Set a breakpoint at address 00684044, and then press F9. Olly breaks at that address. Now press F8 to step to our inline patch at 00400300. Step till you reach the REP of the program (005CB4E0).
Now do the same as we do when we were searching the NAG. Other words, do a right click at the address we're now, and then select "Follow in Dump/Selection". Press Ctrl+B, and try to search the "integrity check" as a Unicode string (don't forget to set the flag "Entire block" to cover all the memory range of the executable)... Uh, no luck! Hm, strange. Try to search as an ASCII string. Yes! I found it at 0054A444. Look a bit above, you will see the beginning of the string ("Zoom Player Professional").
I think that we didn't find it as Unicode, 'cause probably the author made it to save the space. Yes, that's right. He made a simple string ("Zoom Player Professional") to concatenate it with other strings. That's it. That's why we didn't find it as Unicode string. Of course it's only my theory, so don't treat it as a dogma.
Now set a memory breakpoint on access to the first byte of the string we've found. As you know we don't need, just like with the NAG string we just need to catch a treatment to that string. Therefore, select only one byte at address 0054A444, right click on this (selected) byte, then go up through the menu and choose "Breakpoint/Memory, on access". Now press F9 and wait for approximately 10 seconds. When Olly will break at some address (my was 004D1E82) remove the breakpoint. To do it, right click in code or dump window, then select "Breakpoint/Remove memory breakpoint" from the appeared pop-up menu. Trace with F8 a little until this place:
0057DF3C : 803D8C315D0000 CMP BYTE PTR DS:[5D318C],0 ; the reason of jump
0057DF43 : 7544 JNZ SHORT zplayer.0057DF89 ; jump over the NAG
0057DF45 : 833D64245D0000 CMP DWORD PTR DS:[5D2464],0 ; the second reason
0057DF4C : 753B JNZ SHORT zplayer.0057DF89 ; another jump
0057DF4E : 6A00 PUSH 0
0057DF50 : 8D45F4 LEA EAX,DWORD PTR SS:[EBP-C]
0057DF53 : 8B0D58245D00 MOV ECX,DWORD PTR DS:[5D2458] ; @("Zoom Player Professional")
0057DF59 : 8B1554245D00 MOV EDX,DWORD PTR DS:[5D2454] ; @("has failed integrity...")
0057DF5F : E84C71E8FF CALL zplayer.004050B0 ; it concatenates two strings
0057DF64 : 8B55F4 MOV EDX,DWORD PTR SS:[EBP-C] ; <= you will be here
0057DF67 : 8D45F8 LEA EAX,DWORD PTR SS:[EBP-8]
0057DF6A : E8C177E8FF CALL zplayer.00405730 ; converts string to Unicode
0057DF6F : 8B45F8 MOV EAX,DWORD PTR SS:[EBP-8] ; EAX points to Unicode string
0057DF72 : 668B0DC8E25700 MOV CX,WORD PTR DS:[57E2C8]
0057DF79 : 33D2 XOR EDX,EDX
0057DF7B : E884E0F2FF CALL zplayer.004AC004 ; this is that crazy message
0057DF80 : 33D2 XOR EDX,EDX
As you can see, if the content of the memory cell with address [5D318C] or with address [5D2464] is not zero, then you will see no message. That's the way we need. Ok, restart the program once again, press F9. You're at address00684044 again. Trace until the REP. Right click, select "Follow in Dump/Selection". Press Ctrl+G, in the appeared dialog type "005D318C", and press "OK". All we need now is to catch a moment, when this cell will be written with some value (probably zero, because the original [not patched] exe's CRC differs from the patched exe's CRC).
Note. We need only to know, when one of the cells we be written, because both of 'em compares with zero, and after that jump (or don't) over the message. That's why we need only one of 'em. So, choose any. I chose the first, because it's a BYTE (the second is DWORD).
To know, when the cell [5D318C] will be written with a value, right click on address 005D318C, select "Breakpoint/Memory, on write". Now just press F9. Once you've done that, you'll see something like this:
00562DF2 : BA00040000 MOV EDX,400
00562DF7 : B8DB030000 MOV EAX,3DB
00562DFC : E85B500500 CALL zplayer.005B7E5C ; returns 0 (bad CRC) || > 0 (good)
00562E01 : A28C315D00 MOV BYTE PTR DS:[5D318C],AL ; <= Olly broke-up here (EAX = 0)
00562E06 : 33C0 XOR EAX,EAX
Remember this place (00562E01) and press F9 again to see, if is there any other place before that compare at address 0057DF3C... Bams! And you see the the same message now. It means that there's only one place, where our cell [5D318C] is written with zero value. All we've left to do is to write some positive & bigger than zero value (e.g. 01) into that cell.
Improving the inline patch
First of all, you have to decide how to patch it, and then to add that patch you've decided into our inline patch. There're many way of patching it. Your first thought will probably be such. Let's change "MOV BYTE PTR DS:[5D318C],AL" (A2 8C 31 5D 00 => 5 bytes) to "MOV BYTE PTR DS:[5D318C],01" (C6 05 8C 31 5D 00 01 => 7 bytes). Well, not a bad idea, but alas it will increase the command from 5 to 7 bytes. You maybe say, so what. Nothing, but there's a command "XOR EAX,EAX" (33 C0) at address 00562E06, which's exactly two bytes long. If we'll patch in such way we'll just lose the command. As we didn't write this program, then we don't know, how the program will react without that two bytes long command.
You may patch it in other different ways, but leave it. I think the most "correct" way is to patch so. Don't forget, that we're making an inline patch, so this patch differs from a usual patch (in not packed exe). With such kind of patch you can just imagine what to patch, but don't patch it directly in exe. Because you can't patch it directly in unpacked exe, you have to wait when exe is unpacked and then patch it. That's the main sense of the inline patch.
What do we need for the final patch. Not too much, just think a bit. Imagine, that we just don't have the command "MOV BYTE PTR DS:[5D318C],AL" in the unpacked exe. Just imagine it. If so, how will the program act in such case? Right, it will always bring us the message, if of couse the second memory cell ([5D2464]) will be zero too. So, if we'll just fill that 5 byte long command with NOP's, then the program will never write the value with zero. That's what we need, but it's not quit enough. The last thing to do is to write the memory cell [5D318C] with some value bigger than zero (e.g. 01).
The actual patch will be such. Restart the program in the last time, press F9 to get to the well-known breakpoint at 00684044. Press F8 once to get to our patch at address 00400300. We're gonna to add it a bit. Press F8 again (because our first patch works fine, so leave it) to set the cursor to 00400307. Press space now and type "MOV EAX,562E01", press "Assemble". It's the first byte of the address we wanna fill with NOP's. Type "MOV DWORD PTR DS:[EAX],90909090" to fill the first 4 bytes with NOP's. Then type "MOV BYTE PTR DS:[EAX+4],90" to fill the last (the 5th) byte with NOP. After that, type "MOV BYTE PTR DS:[5D318C],1" to set a value we need instead of zero. We set one (01), because so the program will always think that it's CRC is ok. And finally, type the last command of the patch "JMP 005CB4E0", which will return the control to the unpacked program after our patch.
Yes, such patch is much bigger, but it works every time. Save the changes in the last time and run the program. It works fine.
The last thing to say
It's not so hard to find a place or a way to make an inline patch, especially for UPX. I just showed you one of ways of patching. By the way, if you don't familiar too much (or at all) with UPX inline patching, then I recommend you to read Detten's tutorial about how to do it. I recommend you his tutorial, because it's short, simple and pretty clear to start with. Besides, it contains some theory, which I didn't mention in this investigation.
As the final words of this essay, I have to say, that there're as many ways as many people to do the same as we did above. You can even use different methods of cracking, nothing to say about inline patching, because the last thing might vary from cracker's experience, his programming knowledge, intuition, and of course from his logic. That's why don't worry about anything you can't do. Just keep on going forward and you will do it one day.