PDA

View Full Version : A different approach V2



maggotboy
11-17-2002, 10:39 AM
I've got another idea :) It occurred to me while shaving this morning and mulling over the idea of further stealthing this key sniffer. Without question, the key goal is to run in the address space of the game so it can operate with impunity. V1 accomplished this goal, but it leaves a few faint signatures which leave it open (however slightly) to detection. I don't want to go into the details of the detection process for fear of Verant implementing them before I'm ready ...

Onto V2 ...

The idea I've come up with is much MUCH sneakier. RUNDLL32 won't be in the process list, and even if they enumerate the DLL's running in their address space, it won't appear there either. As a matter of fact, it'd be running in their address space, but they'd have no idea where to look for it or how to detect its presence! It won't hijack any DLL entry points, block any API calls or interfere in any way with the game -- won't change anything in the running game binary or any DLL's it uses either.

I'm not even sure it'll work (although the code to do it is not complex and I may even have a work-in-progress later today), and it'll require a few advanced techniques, but it'll be all straight C/C++ code, no assembly, and be initially injected much like the previous code I wrote. However, once injected, it dumps its payload and exits -- the DLL will be unloaded, but the payload will remain, and RUNDLL32 will exist only long enough to deliver the package.

I need to do a couple tests to see if it works before I go explaining the details ... if it does, Verant will be extremely hard-pressed (as if they weren't already) to detect this method.

Maggotboy

Outlaw111
11-17-2002, 11:21 AM
For now, I have done something like this ;)

Lets say my Antivirusshield scanns the EQdir all 10-30 min and saves some data to a viruslogfile :rolleyes:

The transfer is done with a simple patchfile. :D
########################

In the moment i dry to code a windowssystemservicehook that can handle the problem. (There is a service who scans the HDD all day long for indexing)

But I think the only "endsolution" is something like yours.

LordCrush
11-17-2002, 11:38 AM
You Guys rock !!!

Thank you :D !!!

maggotboy
11-17-2002, 07:36 PM
So far, I've had some success and some failure in implementing my idea.

For one thing, its much more complicated than I originally thought, and it has required 2 lines of inline assembly, which I was hoping to avoid.

If I can get some of the unresolved issues taken care of, I may post it prematurely in hopes of getting a collaborative effort going.

Maggotboy

Ratt
11-17-2002, 11:41 PM
I'm really impressed with the level of effort towards this ... keep up the killer work :)

maggotboy
11-18-2002, 12:25 AM
I'm almost read to post this ... needs some more tweaking and testing ...

Essentially it works like this ...

1. Set a global hook into Windows which auto-injects the DLL into all processes. The DLL is dormant until a process matching the description (in our case "eqgame.exe") attaches.

2. When the matching process attaches, we begin Phase 2, which is:

3. Obtain a pointer to the first and last functions in our DLL that pertain to the actual payload. Subtract one from the other to get the length of the code.
4. Call VirtualAlloc() an allocate some committed virtual memory with the PAGE_EXECUTE_READWRITE flag set, large enough to contain the code from step 3 plus some extra room for data.
5. Copy the code from our DLL into the new allocated memory, along with a struct containing useful information and API call pointers (these API calls must be explicitly de-referenced because the DLL holding the lookup tables will disappear in 2 steps)
6. Create a timer which calls into the new relocated code segment.
7. Detach the DLL and remove the global hook. All instances of the DLL in all processes will be dropped, and RUNDLL32 which was in standby awaiting payload delivery, will end.

What is all that mess in layman's terms?

What I've done is taken a block of code and a bit of data with it, and created a NEW code-only memory segment inside the address space of the game. I've copied the bit of code from the DLL into the newly-created memory space and have told Windows to execute a Timer callback procedure inside this new code block. Once the new code block is in place, the DLL no longer needs to remain, so it drops out. As a matter of fact, the system won't even realize the DLL was there in the first place.

Once the payload is delivered, the global hook is released, thereby causing RUNDLL32.EXE to drop out of the process list.

The end result ... I've allocated some memory inside the game's address space, injected some code, and set Windows to call on it every half second.

My tests in XP have it working, sortof. I need to make sure its stable before I post it.

Some caveats ... I don't know if the darn thing will work with another compiler. I use an inline assembly function:

{
LPVOID pvmem;

__asm mov pvmem, esi

}

which at least in my tests, has received the memory block start address and is mandatory in order for the code to get its bearings.

Since the code is executing in memory other than where it was originally found, its impossible for the code to know where it is unless I use some assembly. This fact alone may deter most people from using the method. However, it is IMO the *most* stealth method I've seen that is in a state to be implemented.

In order for VI to detect this injected code, they'd need to first figure out where it was allocated. Since the memory isn't associated with any DLL, doesn't have a lock count or a HANDLE, can't be HeapWalk()'ed, and is of arbitrary size, location and offset, they'll have their hands full.

Maggotboy

baelang
11-18-2002, 01:01 AM
Am i correct in assuming that this bit of code goes away once eqgame exits and the game's memory space is cleaned up?

maggotboy
11-18-2002, 01:16 AM
Of course. Some extra RAM is allocated on the running process only, and the code injected into it. Once the program exits, all that memory is freed automatically. The actual game isn't modified in any way, either in its running form or otherwise.

Maggotboy

zarzax
11-18-2002, 08:27 AM
CreateRemoteThread, baby. :)

maggotboy
11-18-2002, 11:34 AM
Absolutely not!

When you create a new thread in a process, EVERY DLL IN THAT PROCESS GETS NOTIFIED!

While its true you can call DisableThreadLibraryCalls(), Verant can easily test to see if the thread library calls are disabled and shut the game down if it detects tampering.

Maggotboy

throx
11-18-2002, 11:40 AM
Devil's advocate here, not suggesting Sony is even looking for sniffers at the moment.

What do you use for the timer? CreateTimer() runs through the message loop and can be interecepted remember.

Interesting plan though. As always, the best defense is using VirtualProtect() and kernel mode hooking to determine what's trying to read your protected memory. To be relatively sure of not having your calls intercepted in user land, you should only link against ntdll and not kernel32, user32 or advapi32.

If I was to implement sniffing countermeasures I'd do something like the following (all kernel mode stuff):

- Hook OpenProcess and monitor it for eqgame.exe's pid.
- Protect the key either with GUARD or just no access.
- Hook VirtualProtectEx and monitor it for eqgame.exe's pid. If the pid matches then look at the user mode call stack to figure if the call matches a known location that eqgame.exe is *supposed* to call it from.
- Move the key around randomly in memory (which is going to happen anyway if VirtualAlloc is used).

Alternately just move then entire decryption logic (key exchange and all) into a driver. Ship EQ2 with a hardware dongle that actually does the decrypt (not just supplies a key).

maggotboy
11-18-2002, 11:55 AM
The SetTimer() function does generate messages that get processed in the message queue, however ...

Since I've made it using a callback notification, the timer's callback is executed automatically by the system.

After doing some research on the queue, I've decided that it probably isn't quite as bullet-proof as it needs to be, so I'll be coming up with an alternative in the very near future. I was under the original impression that WM_TIMER messages were considered non-queued messages -- ones that could not be seen by a PeekMessage call. It appears however, that its not the case, and it *may* be possible to detect those messages on the queue if you time it right.

As for a kernel-mode driver ...

API-hooking is a bad idea. It causes undue system overhead and causes each instance of the kernel32.dll to be copied to each process rather than having them all share the same instance. The memory of DLL's is marked with PAGE_EXECUTE_WRITECOPY protection.

A safer and less intrusive approach would be to write a winsock layered service provider to obtain legal and designed-for access to the socket layer...once inside, check the PID to find out if its EQ, and then sink your teeth into it from there.

Maggotboy

The Mad Poet
11-18-2002, 12:03 PM
As this is living in the process space couldn't EQ just take a look at the memory footprint and do some checking to see if there is extra code in there?

That seems to me how virus detection is done....

maggotboy
11-18-2002, 12:04 PM
... should have read more closely ... you were discussing countermeasures, not sniffing measures :)

There's several problems with the countermeasures ...

1. VirtualAlloc is an expensive call and should not be done in tight loops.
2. If they move the key to random locations, it is still necessary to keep a pointer to those locations. If you create a GlobalAlloc() with GMEM_MOVEABLE and constantly lock and unlock the page to get the key ... Windows may or may not move it, no guarantee. Even if it was moved after EVERY unlock, you still have to have a constant pointer to the memory's HANDLE ... sniff out the handle, and you have unfettered access to the key.
3. VirtualProtect() does nothing if you KNOW where the location is. You merely un-protect and un-guard the memory, then read it, then replace the original protection flags.
4. An injected DLL has the bonus ability of being able to keep track of all threads that get created courtesy DllMain. If you're worried about being monitored, your function can SuspendThread on every thread in the process (except the current one), read the memory, then ResumeThread. The operation would be quick enough that none of the threads would really be aware they were momentarily shorted.

Maggotboy

maggotboy
11-18-2002, 12:23 PM
Not really, Poet.

In order to make the distinction between memory allocated for code, and memory allocated for use by the application, you'd need to do three things ...

1. Figure out the difference between memory YOU allocated, and memory allocated by something ELSE. Even if you *can* make this distinction, you CANNOT determine whether the memory allocated by something else is legitimate or not -- and you cannot determine exactly WHO allocated the memory. For example ... when you initialize winsock, it allocates thread-local storage and memory so it can do name lookups, etc. You didn't allocate the memory, you've no idea where it was allocated, when, how much and by who.

2. If you *could* figure out what other memory was allocated, you'd then need to scan it by calling VirtualQuery() on tiny chunks of it at a time to determine which chunks were marked PAGE_EXECUTE to determine the code. One could walk all the blocks of memory a dozen bytes at a time, but VirtualAlloc() allocates memory in blocks around the locations of loaded DLL's, so you'd have to navigate in and around the loaded modules -- otherwise you could mistake a legitimate DLL for a sniffer. In order to navigate around the DLL's, you'd have to know where they all are in memory, and know how large they are.

3. While navigating the memory, you'd have to be wary, because not all of it could be committed at the time. You'd have to constantly check for invalid pages, of which you have no idea how large/small they are.

How is this more difficult than checking for Viruses? I'm glad you asked ...

The sniffers are 10x more difficult to find than checking for a virus, because they are 10x more polymorphic. Each compiler compiles differently, and depending on the compiler settings, each compiler can compile the code 100 different ways. Add to that the ability to rearrange the code, add unused bits of code, fiddle with the order of stack variables, etc. Everyone who runs a sniffer will be running a DIFFERENT sniffer with a different signature!

Viruses are compiled once, and are only polymorphic up to a point. They can't 100% reinvent themselves on every infection, and this is why virus scanners can detect them.

If Verant succeeds in making a program that can detect every variation of code on a sniffer, then they may just have to launch a new Virus Scanning division and kill off McAfee and Norton :)

Maggotboy

MisterSpock
11-18-2002, 03:25 PM
Maggotboy -- Thank you! I couldn't agree more with your last post. To find the myriad of sniffers out there is a monumental task, at best.

Access to "high entropy" areas (i.e. exchanged session keys) is always a topic of consideration when attempting to implement secure protocols of any sort. Many experts on the subject are becoming more concerned about the security of the data at the endpoint (user machine) than they are with on-the-wire safety. In fact, several papers have been published on the exact situation we are conversing about. The general conclusion is that general memory protection (access, alteration, intrusion detection) is all but non-existent on consumer operating systems.

Beyond putting McAfee and Norton out of business, they could also turn their attention to creating a new SSL, etc.

RavenCT
11-18-2002, 08:28 PM
Thinking about this, don't be suprised if you see Microsoft putting something either in .NET or whatever is going to come after it to solve this issue... I'd imagine that there not happy about people being able to work around memory detection (sniffing) issues...

Ratt
11-18-2002, 09:28 PM
Memory protection would have to be implimented at the hardware level, like big iron machines do now. I can't see a software solution presenting itself and being viable/secure.

MisterSpock
11-18-2002, 10:47 PM
That's very true, Ratt.

Also, as long as OS authors want to maintain some degree of backward compatibility, I doubt any serious advances will be made that render useless lots of existing software.

Suffice it to say, I'm not going to worry about it in the context of SEQ, Keysniffers, etc. By the time secure hardware solutions proliferate the home market, I'm sure EQ will be a distant memory.

RavenCT
11-19-2002, 07:33 AM
D'OH!

Color me wrong Ratt! Like I said, I'm a network gimp :)

Thanks for the info.... Actually, I'm almost (not qute, but almost) glad that this happened... it's forcing me to learn a lot more about developing apps, how memory works, etc... (Not to mention the two for dummies books I'm plowing through).

maggotboy
11-19-2002, 01:10 PM
I got this working today!

It was a derivative of the 1.1 codebase, so I need to clean it up, document it, update it to 1.3, and then I'll post it.

This is the codeblock that is going to screw everyone's compiler (if you're not using a MS compiler) :

// Gets the EIP register, essentially.
// We need to know where in memory we are, in order to find
// out where our INJECTSTRUCT is.

LPVOID pvmem;
__asm
{
call $ + 5
pop pvmem
add pvmem, 7
}

3 lines of inline assembly, which is mandatory in order for the injected code to figure out where the hell it's executing so it can determine where its datastructure is.

There's no way around the assembly .. I've tried it six ways to sunday, and there's just no getting around it.

Maggotboy

wiz60
11-19-2002, 02:32 PM
I am surprised that the binary code at the lowest level is relocatable without some major modifications.

The linking process binds the code to run at a certain spot in a virtual address space. Memory management satisfies the demands associated with the address space by creating a "virtual" space.

I am anxious to see how you solve this issue - you have basically created code which is ambivalent to its memory space - ie: all relative addressing - is this a trick? - or a standard feature associated with MS-VC.

The side effect is that the solution may well be highly compiler specific.

Fantastic info BTW - thanks for the effort.

maggotboy
11-19-2002, 02:45 PM
Aha! You are both right and wrong, wiz60 ...

The very nature of a DLL is that it must be relocatable from its base address. All calls are relative unless explicitly stated otherwise. Windows must be able to load a DLL into address space other than where it was intended -- otherwise you'd have conflicts if 2 DLL's were written to run in a specific memory location.

So ... what I'm doing is merely an extra relocation into newly-allocated space, and leaving that relocated code orphaned when the DLL drops out of existance. As long as the relocated code makes no calls OUTSIDE the relocated area except what is explicitly allowed, we remain perfectly self-contained and portable to any memory address.

Maggotboy

Alwayslost
11-19-2002, 03:01 PM
Kinky... -Hedley Lamarr

:D

wiz60
11-19-2002, 06:35 PM
So you use pragma to create a known starting base address (that was there anyway as I recall in V1.2) - then relocate code from that start - thru the end of the executing region.

I still think other compilers will have an issue - the need to create strictly relative code had got to be a code generation time option. Perhaps if they can make DLLs they will be fine.

Nice solution!!

throx
11-20-2002, 05:41 PM
Originally posted by maggotboy
There's several problems with the countermeasures ...

1. VirtualAlloc is an expensive call and should not be done in tight loops.
2. If they move the key to random locations, it is still necessary to keep a pointer to those locations. If you create a GlobalAlloc() with GMEM_MOVEABLE and constantly lock and unlock the page to get the key ... Windows may or may not move it, no guarantee. Even if it was moved after EVERY unlock, you still have to have a constant pointer to the memory's HANDLE ... sniff out the handle, and you have unfettered access to the key.
3. VirtualProtect() does nothing if you KNOW where the location is. You merely un-protect and un-guard the memory, then read it, then replace the original protection flags.
4. An injected DLL has the bonus ability of being able to keep track of all threads that get created courtesy DllMain. If you're worried about being monitored, your function can SuspendThread on every thread in the process (except the current one), read the memory, then ResumeThread. The operation would be quick enough that none of the threads would really be aware they were momentarily shorted.

Maggotboy

Ok, dealing point by point (yes, I did think through most of these):

1. VirtualAlloc wouldn't have to be called in a tight loop. Basically it's only called on zoning to get a new memory block to store the new key in. All this is really is just some petty obfuscation to hide the real protections. I used VirtualAlloc() so I could them call VirtualProtect() on the memory block.
2. Yes, you have to keep a pointer somewhere. Can't get around that. Can do all sorts of hellish things to make it very, very hard for someone reading the disassembly to understand though. That's probably where I'd start messing around first - play with key storage and monitor the threads over here to see what you guys come up with. Moving the key is essentially unnecessary for detecting the sniffer though - just gives a greater delay time for the sniffers and increases the frustration of the sniffer writers.
3. Note I said to hook VirtualProtectEx() inside the kernel (I don't mean kernel32.dll, I mean using a device driver). Any call your injection code makes to unprotect the memory will flag as an "illegal" access immediately.
4. I have no idea why you are talking about other threads. I'm talking about hooking the APIs your sniffing code *must* call to unprotect the memory in question. In other words, it doesn't matter what cunning tricks you use to hide your code, to get at the data you have to go through Sony's validation code which is loaded into the kernel.

If you really want to get serious then you implement a file system driver which does the funky stuff. Unless someone here has a *LOT* of experience in kernel mode programming and is willing to pony up the $1000 for the SDK from Microsoft then it's the end of the road.

At the moment you are thinking on the level of Sony detecting the sniffer by looking for the sniffer itself, hence your use of hooks, funky xors and removing the attaching DLL. What I'm looking at is the general sniffer behavior instead of the code itself. Fundamentally the sniffer must get to the key and by protecting the key and flagging any unauthorized reads from that memory location then you can detect sniffers.

Of course, you can just toss the decryption code into a kernel mode driver and force people to start learning whole new areas of programming just to sniff the EQ session key. That will defeat any user mode sniffer that may come up but by forcing sniffers to become kernel mode it inherently makes them far, far harder to detect.

Basically, the process I outlined would detect your current sniffer code instantly no matter what compiler it was running under.

maggotboy
11-20-2002, 07:39 PM
throx -

I've hooked API calls from USER mode, but not from KERNEL mode. I'm not sure of the details involved -- have to research it before discussing it further.

One thing to note however, is that protecting memory with VirtualProtect works only on an entire page. If a range of memory overlaps a page (even by a single byte) the entire region is is given the protection attributes. They'd have to block out a 4k section of code for the key to sit in, which at least makes it a big target.

Maggotboy