As already stated Valve uses Public-key-cryptography to exchange the ICE-Key to encrypt its game traffic on its own servers. The community servers use a key that just depends on the client build number.
The method used is RSAES-OAEP according to the PKCS#1 v1.5 standard. While this has some weaknesses we must remember that our attack has to be feasible within seconds or minutes during game session. It wouldn’t matter if we retrieve secrets even hours after the game took place. We need them instantaneously to gain any advantage.
NetShark was strong because it was non-intrusive and out of scope of anticheat technology. We have encountered a blow to this mechanism.
But what if we make a little compromise. What if we just quickly steal the ICE Key from memory (through any means), save it to a file, move it to the PC where NetShark runs and do the same as we did before.
It is a compromise because we could still be caught by stealing the key. But other than reading 128 Bit from memory (4 DWORDs) we keep being clean for the rest of the time.
I updated NetShark to use the IceKey from the file „icekey“ in the same folder.
How you get the IceKey is described below.
Getting the ICE-Key from Memory
Hint: All addresses are rebased.You will have to find everything yourself or calculate base differences.
The whole encryption and decryption of game traffic is done in the „engine“ module of the game. Carefully following breakpoints put on recv() we end up in a function with some string references, one of which is:
„NET_ReceiveDatagram: receiving voice from %s (%d bytes)“
This helps us finding the function more quickly the next time. Following the usage of the network buffer, we manage to find the decryption loop:
.text:1013183A xor esi, esi // initialize loop counter
.text:1013183C mov ebx, esp // temp variable
.text:1013183E cmp [edi+40h], esi // size of buffer
.text:10131841 jle short loc_10131869
.text:10131843 mov eax, [edi+18h] // pointer to buffer with encrypted data
.text:10131846 lea ecx, [ebp+var_18] // IceKey-Instance class pointer
.text:10131849 push ebx // push plaintext temp buffer
.text:1013184A add eax, esi
.text:1013184C push eax // push ciphertext buffer
.text:1013184D call sub_101C2DE0 // IceKey::decrypt (look in SDK)
.text:10131852 mov eax, [ebp+var_205C]
.text:10131858 movq xmm0, qword ptr [ebx]
.text:1013185C movq qword ptr [eax+esi], xmm0
.text:10131861 add esi, 8 // block cipher, 8 bytes for every block
.text:10131864 cmp esi, [edi+40h] // are we done yet?
.text:10131867 jl short loc_10131843 // end of loop
Those who encountered ICE before will recognize it, because it has a blocklength of 8 bytes.
Let’s look in the SDK.
Before anything can be decrypted, the IceKey class has to be initialized and the key has to be set.
Let’s look for more calls of the class instance [ebp+var_18]. Right above we’ll find:
.text:101317D9 call sub_10130260 // FindNetChannel(…,
.text:101317DE test eax, eax
.text:101317E0 jz loc_10131AD2
.text:101317E6 mov edx, [eax]
.text:101317E8 mov ecx, eax
.text:101317EA call dword ptr [edx+134h] // GetChannelEncryptionKey
.text:101317F0 mov esi, eax
.text:101317F2 test esi, esi
.text:101317F4 jz loc_101318B7
.text:101317FA push 2
.text:101317FC lea ecx, [ebp+var_18]
.text:101317FF call sub_101C2B50 // IceKey::IceKey(2)
.text:10131804 push esi // push encryption key
.text:10131805 lea ecx, [ebp+var_18]
.text:10131808 call sub_101C3080 //IceKey::set(char* key)
Now we know where to look for our key. We also know it must be 128 bit in size.
64 bit is the standard key length and the constructor is called with n=2, doubling it.
The engine has support for several logical communication channels. Usually only one channel is used, but file transfers for example are delivered over a second allocated channel object.
So apparently every channel instance stores it’s own encryption key, probably in one of its members.
This means we need to get the pointer to the main channel instance first and then access the encryption key member afterwards.
So let’s look into FindNetChannel(…) to find the main channel.
.text:101302B0 mov eax, dword_106A0818
.text:101302B5 mov ebx, [eax+esi*4]
dword_106A0818 has our channel list and the main channel is – for all cases we are aware of – the first element. This is all we need to know.
Now let’s look into GetChannelEncryptionKey(), which was at vtable+134h.
sub_10127020 proc near ; DATA XREF: .rdata:103BE594o
.text:10127020 mov eax, dword_106F5058
.text:10127025 test al, 1
.text:10127027 jnz short loc_1012703F
.text:10127029 mov edx, dword_106C2AB4
.text:1012702F or eax, 1
.text:10127032 mov dword_106F5058, eax
.text:10127037 mov dword_106F505C, edx
.text:1012703D jmp short loc_10127045
.text:1012703F loc_1012703F:
.text:1012703F mov edx, dword_106F505C
.text:10127045 loc_10127045:
.text:10127045 test al, 2
.text:10127047 jnz short loc_101270C4
.text:10127049 or eax, 2
.text:1012704C mov byte ptr word_10430FDC, dl
.text:10127052 mov dword_106F5058, eax
.text:10127057 mov eax, edx
<snip—snip–snip> // build static key and store
.text:101270C4 loc_101270C4:
.text:101270C4 cmp dword ptr [ecx+4288h], 0 // are we on a valve server?
.text:101270CB jz short loc_101270D4
.text:101270CD mov eax, [ecx+4284h] // return dynamic session key (valve servers)
.text:101270D3 retn
.text:101270D4 loc_101270D4:
.text:101270D4 mov eax, offset unk_10430FD8 // return static key (community servers)
.text:101270D9 retn
.text:101270D9 sub_10127020 endp
Upon examination we discover several things. First: What we actually get as a key, depends on dword ptr [ecx+4288h]. Testing reveals: This is NULL on community servers, and we find our key (after it was built together) at unk_10430FD8. It is the same key every time and it depends on the build number.
On Valves servers (Matchmaking, etc.) the key is retrieved from the channel class member [ecx+4284h].
So this is all we need. We retrieve the first member of the channel list. (dword_106A0818): (CNetchan**)engine.dll+0x7C0818 and from this we retrieve the key
(ICE_KEY*)(DWORD*)CNetChan+0x4284.
If no icekey file is found, NetSharkGO.exe uses a static ICE-Key for the community servers. This might break on the next update. So use this tutorial and put the new key into
an icekey file.
FindIceKey.exe might break also, because it uses static offsets.
We can now retrieve the icekey, share it with the netshark computer and cheat as we are used to.
While this is a compromise and we have to read memory, other memory-hacks need to read constantly and run cheat code. We just need to read something quickly and we can fuck off completely. If done right, this is still an advantage.
Warning: FindIceKey.exe is only a demonstration application. If Valve decides to blacklist this process, it will get you banned. Be creative when accessing and reading out memory.
Download: NetSharkGO, small resolution, FindIceKey.exe —>
http://uptobox.com/7nbouge7xco7 (update 14.01, wrong community icekey)
Update 15.01:
New Community-IceKey: http://pastebin.com/yhqCimU9