NetShark: Attack Vectors

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.

https://github.com/ValveSoftware/source-sdk-2013/blob/56accfdb9c4abd32ae1dc26b2e4cc87898cf4dc1/sp/src/mathlib/IceKey.cpp

https://github.com/ValveSoftware/source-sdk-2013/blob/56accfdb9c4abd32ae1dc26b2e4cc87898cf4dc1/sp/src/public/mathlib/IceKey.H

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

Valve uses PKI Infrastructure

I am happy to inform you that from a perspective as a game security researcher, NetShark GO has been a success.

After the recent update of CS Global Offensive Valve decided to use good crypto for their Matchmaking Servers (their own PKI infrastructure with asymmetric crypto and digitial server certificates).

Remember that free publishing hackers are not your enemy, but your friends that push developers to implement secure and solid code.
This technique (reading game traffic over the network) has been around in the dark for a very long time and game developers are often oblivious to these kind of attacks, because in their minds what you can not see, does not exist. A very naive security approach.

But keep in mind that this might not be the last word on this and research must continue. The next step should be a crypto audit of their implementation to find possible weaknesses.