Let's assume you find all the spots by doing simple on-read grinding with an executable (get your hook spot; debug on access its first byte; see what accesses that; then take the first byte of the instruction reading your byte; find out what accesses that; and so on). If the developers update the game (either the version or just the executable alone), then all those locations will be lost. So what would you do.. grind again on the new executable? Task might be tedious given the time spent or involved procedures (aside from debugging, you might be required to hot-patch some of the scanners to continue finding out others).
So,
predprey, yes and no; the VAs are for both the scanner-instructions-to-be-checked and game-executable-code-to-be-checked. Why would they split them up?
It wouldn't make any sense to me, as a developer. If they're mixed up together, it makes the task harder for the reverser. But then again, the reverser doesn't want to split them as well; just patch the core of the detection routines. Given the same function's spliced across the executable, it's pretty tedious to find each and every scanner. Sure, you might get the hang of it so the "82" scanners-patching can be replicated easily (scripted) at some point in time. Or simply fingerprint the structure of the scanner function (the reader part) and scan whole memory for occurences. Break on all instances and see if the debugger stops and on which; and what happens when the read occurs. That's one way to tell, approximately, how many scanners are there. Wouldn't surprise me if Caliber did exactly that
Doesn't take a genius to find one scanner, Ctrl+F the bytes (give or take the spliced JMPs) and count them. There's no need to check if they break or not, the effect alone on the public is enough -> "there are 82 scanners"; people: "omfg, wow". Smug smile after that.
Let's assume this is the xor_after_read part of a scanner:
Code: Select all
32 02 - xor al,[rdx]
48 83 C2 01 - add rdx,1
FF 4D 00 - dec [rbp]
Theoretically you can scan whole protector's memory for this pattern: 32 02 48 83 C2 01 FF 4D 00. Check if found locations are indeed those 3 instructions (the bytes might be part of other instructions, thus invalid ASM) and break on all. See which are triggered and count them up. That's how you get fast that "82".
But.. you won't get all
Here's why:
Code: Select all
32 02 - xor al,[rdx]
E9 xx xx xx xx - jmp loc_1
..
loc_1:
48 83 C2 01 - add rdx,1
E9 xx xx xx xx - jmp loc_2
..
loc_2:
FF 4D 00 - dec [rbp]
That's called splicing. Can be done with JMPs along and/or
junk code. Here's another example:
Code: Select all
32 02 - xor al,[rdx]
E9 xx xx xx xx - jmp loc_1
..
loc_1:
48 83 C2 01 - add rdx,1
F9 - stc
E9 xx xx xx xx - jmp loc_2
..
loc_2:
F8 - clc
F6 C4 85 - test ah,-7B
FF 4D 00 - dec [rbp]
That's the reason I said it might be a tedious task
But hey, we're all adventurers out here
Now.. regarding the memory copy or file copy and why it wouldn't work. Let's assume we have this function:
Code: Select all
read_god_toggle:
00007FF7A50C90C0 | 8A 81 80 07 00 00 | MOV AL,BYTE PTR DS:[RCX+780] |
00007FF7A50C90C6 | C3 | RET |
This would be a core function that the engine uses to check-up on
god status. Simply put, there's a BOOL at 0x780 in the structure referred-to by RCX that, if set to 1, will enable god in-game. Let's assume there's no other way to toggle it (no other read/write functions but this one) and you don't want to use breakpoints to read RCX and manually set that 1. Let's say you wanna hook it, thus place a JMP at 7FF7A50C90C0. Once you do that, the code changes to:
Code: Select all
00007FF7A50C90C0 | E9 3B 7F FF FF | JMP 7FF7A50C1000 | <- our hook
00007FF7A50C90C5 | 90 | NOP | <- evening out bytes
00007FF7A50C90C6 | C3 | RET |
..
00007FF7A50C1000 | 8A 81 80 07 00 00 | MOV AL,BYTE PTR DS:[RCX+780] | <- original
00007FF7A50C1006 | E9 BB 80 00 00 | JMP 7FF7A50C90C6 | <- JMP back and resume flow
Once you do that, the scanner I mentioned earlier kicks in and instead of finding 8A 81 80 07 00 00 at 00007FF7A50C90C0, it finds E9 3B 7F FF FF 90. Of course the XOR result will be different, thus a computed/expected hash of the memory section will fail; crash. Let's assume now you wanna fix that. Without breakpoints and just hooks/patches, you would do the exact same hooking approach, but this time on the "xor al,[rdx]" instruction of the scanner. Its size is 2 bytes; a near-JMP needs 5. If there's JMP splices, you can re-route those and JMP back to the spliced JMP's destination
OK, you do that, you hook the scanner, you make it so you feed the original bytes to the XOR routine and everything works nice. But wait.. why is the game still crashing? Well, that's because there's another scanner checking up the scanner checking up on your game hook
Nice, eh?
So the approach above has to be replicated to all the possible scanners you find checking up on any piece of code you hook; inside the hook either feed the original bytes to the XOR loop (you'd need a list to keep track of them) or simply trace the code structure to find the loop exit and storage of the computed XOR hash. Once found, restore it to the value you know should be the correct one. Thus you can work with a table of hashes, which makes things a lot easier.
Now.. back to why copies of the memory would fail when changing the checked pointer. Quick background derived from the above method: you can make a copy of the game memory - allocate enough memory, duplicate the executable (you might need to duplicate all, not just the - usually - .text executable section), then at the XOR spot instead of letting the scanner check your RDX, you feed the calculated pointer to the original address in your copy. So:
- xor al,[rdx] reads the byte of 7FF7A50C1000 (0x8A)
- swap rdx (7FF7A50C1000) with its equivalent from your memory copy (let's say 7FFFA50C1000; at this copy location the byte is 0x8A)
- you can now hook 7FF7A50C1000 and won't crash from this alone; scanner will check 7FFFA50C1000
The approach above has to be replicated to all locations where scanners exist/are triggered/etc. Alternately, you can easily write-up a generic function that uses as parameters the offset deducted from address - module base, to which you apply the copy's module base
Thus you end-up re-routing all scanners to 1 function.
Still haven't gotten to the fail part
Bear with me.
Back to our function:
Code: Select all
read_god_toggle:
00007FF7A50C90C0 | 8A 81 80 07 00 00 | MOV AL,BYTE PTR DS:[RCX+780] |
00007FF7A50C90C6 | C3 | RET |
What I've seen protectors do nowadays is to run a RDTSC instruction or some timer to determine a "timestamp" of the process code state in memory. And when they do that, they store the result somewhat close to the function above. The computed hash will then take into account this "timestamp". Much like the Enigma machine. Encrypt one layer, add something extra, encrypt the next layer, and so on. Done in waves. Here's a demo:
Code: Select all
read_god_toggle:
00007FF7A50C90C0 | 8A 81 80 07 00 00 | MOV AL,BYTE PTR DS:[RCX+780] |
00007FF7A50C90C6 | C3 | RET |
00007FF7A50C90C7 | 44 32 2C 38 | XOR R13B,BYTE PTR DS:[RAX+RDI] |
The result of the RDTSC operation is a DWORD in RAX. This is stored at 7FF7A50C90C7. When the compute is done, code between 7FF7A50C90C0 and 7FF7A50C90C7+4 is hashed. You get a DWORD as result (e.g.: 78923C4A). Now.. if you create that memory copy when 7FF7A50C90C7 is 0x382C3244, chances are another encryption cycle occurs, at which point a different RDTSC result is stored at 7FF7A50C90C7 (e.g.: 45 38 29 3A). Since you copied the memory with the old RDTSC value, well.. guess what happens with all the bypassing logic described above
Yup, your hash goes to shit.
The reason StealthEdit is, in my opinion, the best choice here is you don't have to deal with re-routing code with so much hassle. Like you put it, predprey, read/write operations are redirected to the copy, execute operations are done on the original
And not in the DBVM fashion.
Fun times.