… An exemplary walkthrough in 15 minutes
Introduction
Many commercial voice and messenger applications are known to have an encrypted network protocol. I will show you how to decode and analyze proprietary network protocols in just a few steps. The method i’ll show you will work for many programs. Teamspeak3 is taken exemplarily. You should be able to adopt and apply the techniques described here to whatever program you like. Note: some of the described aspects will also work on the otherwise undebuggable Skype client binary :]
Background
Why are many proprietary protocols encrypted? If I had to guess it’s because of two reasons:
- The programmers think its more secure, because security issues can’t be detected so easily.
- To reduce the number of compatible opensource software, by saying its forbidden to reverse engineer their encrypted protocols.
The first one is a common mistake. I call it security by obscurity. Encrypting an interface (the network protocol is an interface) only increases the chance that evil guys write exploits to use them and keep them for themselves. The second one is not applicable and overgrown by European law.
So why do we decode them?
- for the purpose of interoprability: opensource clients and other platforms
- point out security issues
- to protect your privacy. You can’t know what data is transferred until you checked it
- for fun and profit
What you need is:
- a recent mmBBQ version (3.0.1 or upwards)
- a text editor to add the Lua codes to your config.lua file
- the official Teamspeak3 client binary (i.e. 3.0.8.1)
STEP 1 – Reading RAW network data
For reading raw un-decrypted network data, we just hook the used windows network API: WSASendTo(…) and WSAReadFrom(…). Note other win32 apps maybe using the WSASend(…) and WSARecv(…) variants or even other methods to read data. In order to hook the network functions just need some lines of code at the bottom of your config.lua:
-- MSDN: int WSARecvFrom( __in SOCKET s, __inout LPWSABUF lpBuffers, __in DWORD dwBufferCount, __out LPDWORD lpNumberOfBytesRecvd, ... );
local function wsa_recv(context)
local buflen = context.arg32(2, "uint32_t**")[0];
local buf = context.arg32(2, "char**")[1];
local recv = context.arg32(4, "uint32_t*")[0];
if recv > 0 then
printf("[RECV] 0x%X", recv);
hexdump(buf, recv);
end
end
codecave.inject(nil, getProcAddress("ws2_32", "WSARecvFrom"), wsa_recv, codecave.INTERCEPT_RETURN)
The `codecave.inject(…)` does the actual hooking. The hook refers to the functions declared before: `wsa_send` and `wsa_recv`. The hooks just parse and dump the stack arguments from the function. The signature can be taken from the official MSDN docs. If you are known to LuaJIT fii API theres no magic in it, if not I recommend reading the LuaJIT ffi Doc and maybe our codecaving doc.
The applied hooks will result in this console output:

We can see the raw data as it passes by, just as if we are using wireshark. By just estimating the number and distribution of zero bytes 0×00 in the bigger transmissions we can assume that the data is still encrypted. The repetitive transmission seem to be the keepalive or timesync. those look unencrpyted, because we can see incrementing hex numbers and a similar data layout as prefix.
STEP 2 – Locating the decryption routines
Most applications use symmetric stream encryption (http://en.wikipedia.org/wiki/Symmetric-key_algorithm). They work either by pseudo number generators or by predefined shuffle arrays. This is no rocket science. Its well proven, less CPU intense and included in many stream encryption libraries used by those applications. In order to detect the decrpytion routines in the binary we can exploit a common nature of those algorithms: They work directly on the input data, and are executed as the first access to a received network buffer.
With that knowledge in mind, we just add a memory breakpoint to a buffer when it was received (at the end of hook function wsa_recv). When it fires we know the Instruction pointer (EIP) where the buffer is going to be modified next. Note: we check the length of received buffer, because the “small” keepalives don’t seem to be encrypted.
...
if recv > 0xF then
dbg.mbpAdd(buf + 0xF, function(bp) bp.print(); bp.del(); end, 1, dbg.MBP_WRITE);
end
This automatically installs a one-shot memory breakpoint when data is received from network. On trigger it will execute two things: `dbg.print()` and `bp.del()`. The `dbg.print()` will print out the cuurent context and disassembly when the memory breakpoint fires. The `bp.del()` will delete the memory breakpoint again, so it behaves in a one shot manner… As easy as pie, isn’t it?
Now, when connected we attach our code again and click around the channels to trigger a network read that is larger than the small keepalives. Eventually we will see the decoding function context print that we just installed automatically … :]

Yay, now we see theres a little loop that is writing to our buffer… it jumps back to 0x009037D0 on a certain loop condition. lets see the whole loop. just type dbg.asm(0x009037D0, 7) into the Lua console and we will get:
0x009037D0 8b3c02 mov edi, [edx+eax]
0x009037D3 33bc0e94000000 xor edi, [esi+ecx+0x94]
0x009037DA 83c104 add ecx, 0x4
0x009037DD 8938 mov [eax], edi
0x009037DF 83c004 add eax, 0x4
0x009037E2 3b4e04 cmp ecx, [esi+0x4]
0x009037E5 7ce9 jl 0x9037d0
The XOR operation at 0x009037D3 is also a very good hint that this is part of a decryption/encryption routine. Pretty straight forward this little snippet does the following things:
- load the next DWORD to EDI (from the buffer pointer EDX)
- decode the data using XOR and a stream source object or class (ECX+0×94)
- increment the streamsource ECX by 0×4 (sizeof(DWORD))
- write out the decrypted data to the original buffer (we catched that instruction earlier)
- also increment output pointer EAX (used in the last instruction)
- compare abort condition and repeat until finished
Sure there is more to understand about the streamsource and maybe the initial key, but for the time being that’s it. Lets just try to get the data displayed decoded as it passes through…
STEP 3 – Dumping the decoded data
When we look closer at the previous Context print (last console screenshot) we notice a Code reference in the current Stack
0x00903894 : CODE 0x00400000 ts3client_win32.exe+0x00503894(). Code references in the stack are most likely return addresses of function calls. Lets investigate that by installing a debug codecave: codecave.debug(0x00903894):

… the decrypted stuff!! The following entry in the stack should be the buffer size (0×21). When we look at the previous match, we can see that this cave is also executed for encrypting things (the yellow mark in the screenshot).
Now we can make a suitable cave that dumps us any data: post-decrypted when received and pre-encryption when send. Lets reload our console so codecaves and BPs are cleared. Maybe you also want to remove the hexdumps and auto breakpoint thing set in Step1. To find the entrypoint into the crypt function we can query the “return address – 0×5″. In x86 assembler the returns address always points to the CALL+0×5 bytes, because thats the size of the call instruction itself. So we can examine the function call to the crypt function to get the entrypoint:
dbg.asm(0x00903894 - 0x5) -> call dword 0x903640.
What we did here is to determine the first instruction of the encryption/decryption function. This can also be done with OllyDbg by just looking there. The next versions of mmBBQ will eventually include more disassembly functions.
Now lets place a debug cave at there: codecave.debug(0x903640)
In the same way we detected the return address before we now see two calls to this address. The one we already knew for decrypting 0x00903894 and the call for encrypting data 0x0090F81B.
Okay, now we just make Lua codecaves there, one that is executed on return (decryption) and one that is executed on call (encryption). We can differ them by looking at the return address. The approach is the same in the hooking of WSARevcFrom in Step1.
local function decrpyt_hook(context)
local ret = context.arg32(0, "uint32_t");
if ret == 0x00903894 then
printf("RECV_CRYPT: %s", str(context.arg32(2, "char*"), context.arg32(3, "int")));
end
end
codecave.inject(nil, 0x903640, decrpyt_hook, codecave.INTERCEPT_RETURN)
local function encrypt_hook(context)
local ret = context.arg32(0, "uint32_t");
if ret == 0x0090F81B then
printf("SEND_CRYPT: %s", str(context.arg32(2, "char*"), context.arg32(3, "int")));
end
end
codecave.inject(nil, 0x903640, encrypt_hook, codecave.INTERCEPT_PRE)
Doing so will result in the fully decrypted control port protocol. So we are finished here. Funny thing is that ts3 uses an ASCII plain text protocol which is not very common. Maybe its easier to debug. I learned to design protocols by sending codes and compressed data with little overhead.
SEND_CRYPT: clientinitiv alpha=lk34sdLIASjdka== omega=LIJd5ldk2LJDjalK1UaF411oon\/V82tqmaM0p8dr6qKmQp9RiK+sCID7
W98OrGCKD+c8erzHWyAaASsxc3l2kkl\/TGxQbV3 ip=123.123.123.123
RECV_CRYPT: initivexpand alpha=lk34sdLIASjdka== beta=wp82a901Q05O8qg== omega=LIJd5ldk2LJDjalK1UaF411oon\/Afh\/GOSJLsAwdV
e\/BUbnFL0sYc49hSfCIDB\/37L0EZb0WaI2pC4FAd5gbvzgp2ZjsSrvnOSASMav
SEND_CRYPT: clientinit client_nickname=will client_version=3.0.8.1\s[Build:\s1343657352] client_platform=Windows client_
input_hardware=1 client_output_hardware=1 client_default_channel client_default_channel_password client_server_password=
fHzk+43klKq2SnfCS1B3r+HCakI= client_key_offset=291 client_nickname_phonetic client_default_token hwid=198b8902374b0973c1
8as097aft3,01h4m5cmpw8qytc83
RECV_CRYPT: initserver virtualserver_name=FooBarServer virtualserver_welcomemessage=Welcome\sto\sTeamSpeak,\sc
heck\s[URL]www.teamspeak.com[\/URL]\sfor\slatest\sinformation virtualserver_platform=Linux virtualserver_version=3.0.5\s
[Build:\s1335655378] virtualserver_maxclients=32 virtualserver_created=1337381436 virtualserver_codec_encryption_mode=0
virtualserver_hostmessage virtualserver_hostmessage_mode=0 virtualserver_default_server_group=8 virtualserver_default_ch
annel_group=8 virtual
RECV_CRYPT: server_hostbanner_url virtualserver_hostbanner_gfx_url virtualserver_hostbanner_gfx_interval=0 virtualserver
_priority_speaker_dimm_modificator=-18.0000 virtualserver_id=1 virtualserver_hostbutton_tooltip virtualserver_hostbutton
_url virtualserver_hostbutton_gfx_url virtualserver_name_phonetic virtualserver_icon_id=0 virtualserver_ip virtualserver
_ask_for_privilegekey=0 virtualserver_hostbanner_mode=0 acn=will aclid=1 pv=6 lt=0 client_talk_power=75 client_needed_se
rverquery_view_power=
RECV_CRYPT: channellist cid=4 cpid=0 channel_name=#1337 channel_topic channel_codec=2 channel_codec_quality=7 channe
l_maxclients=-1 channel_maxfamilyclients=-1 channel_order=0 channel_flag_permanent=1 channel_flag_semi_permanent=0 chann
el_flag_default=1 channel_flag_password=0 channel_codec_latency_factor=1 channel_codec_is_unencrypted=1 channel_flag_max
clients_unlimited=1 channel_flag_maxfamilyclients_unlimited=1 channel_flag_maxfamilyclients_inherited=1 channel_needed_t
alk_power=0 channel_n
RECV_CRYPT: ame_phonetic channel_icon_id=0
...
SEND_CRYPT: channelgetdescription cid=4 return_code=1:d
RECV_CRYPT: notifychanneledited cid=4 channel_description reasonid=9
RECV_CRYPT: error id=0 msg=ok return_code=1:d
SEND_CRYPT: clientmove cid=4 cpw clid=0 return_code=1:e
RECV_CRYPT: notifyclientmoved ctid=4 reasonid=0 clid=4
RECV_CRYPT: notifyclientchannelgroupchanged invokerid=0 invokername=Server cgid=8 cid=4 clid=4 cgi=4
RECV_CRYPT: error id=0 msg=ok return_code=1:e
SEND_CRYPT: connectioninfoautoupdate connection_server2client_packetloss_speech=0.0000 connection_server2client_packetloss_keepalive=0.0000 connection_server2client_packetloss_control=0.0000 connection_server2client_packetloss_total=0.0000
SEND_CRYPT: channelgetdescription cid=22 return_code=1:f
RECV_CRYPT: notifychanneledited cid=22 channel_description reasonid=9
RECV_CRYPT: error id=0 msg=ok return_code=1:f
SEND_CRYPT: clientmove cid=22 cpw clid=0 return_code=1:g
RECV_CRYPT: notifyclientmoved ctid=22 reasonid=0 clid=4
RECV_CRYPT: error id=0 msg=ok return_code=1:g
RECV_CRYPT: notifyclientchannelgroupchanged invokerid=0 invokername=Server cgid=8 cid=22 clid=4 cgi=22
...
Now it’s up to you to build Lua code that can inject command packets to test how the server is responding.
have fun,
Michael Willigens