Hey folks, today I write about a (probably unknown) TeamSpeak 2 session hijacking vulnerability. TeamSpeak 2 is a voice-over-Internet Protocol (VoIP) software that was written in Delphi. Although there is already a newer version (TS 3) one can still find more than 1000 online TS 2 servers some of which are still used for voice communication and in addition we can always learn something even from software that is not “mainstream” anymore. Before we start let’s have an overview about what is possible by exploiting this vulnerability.
Contents
Known security issues
Over the time the following issues have been found:
- 2010: Remote code execution (found by nSense):
The vulnerability is caused due to an error when processing certain packets and can be exploited to corrupt memory by e.g. sending a specially crafted TS2T_VOICE_DATA_CELP_WINDOWS_5_2 packet. The vulnerability is confirmed in version 2.0.32.60 for Windows.
- See CVE Details for about 4 further relatively boring issues.
Security concept
User groups
There are 6 possible user groups: Server Admin (SA), Channel Admin (CA), Operator (O), Voiced (V), Registered (R) and Anonymous. A user can be assigned to more than one group e.g. (R SA CA) (c.f. with screenshot above) and all groups have their own customizable permissions (e.g. SA rights normally invoke the right to kick a player from the server).
Binary protection
There are no security measures against reverse engineering except a UPX compression. The command ‘upx -d’ allows to decompress the binary in an instant. For this article we will refer to the upx-decompressed 2.0.23.19 server binary.
TeamSpeak 2 Protocol
Like many other VoIP softwares TS also uses a UDP based protocol. Since we focus on how to hijack a foreign session we need to filter out all relevant parts first. Once a client has connected most packets will contain the following header:
Class | Function | Session key | Client id | Sequence number | Resend counter | Fragment number | CRC32 |
---|---|---|---|---|---|---|---|
2 bytes | 2 bytes | 4 bytes | 4 bytes | 4 bytes | 2 bytes | 2 bytes | 4 bytes |
not relevant | e.g. 0xAE01 for text messages | authenticates a client | number that gets incremented after each packet | only relevant for big packets | checksum |
At this point it becomes obvious that in order to impersonate someone we need the correct session key. In addition there is an annoying sequence number that also has to be correct (this is not obvious and makes brute-forcing the session key infeasible).
Session hijacking
Our first goal is to get the correct session key. A client gets a new session key after each new connection establishment. Another observation we make is that the session key is always only 3 bytes long. Let’s have a closer look into where this session key comes from.
Basic binary analysis
Some debugging with IDA and OllyDbg reveals the following function (found @004036BC).
1 2 3 4 5 6 7 8 9 | push ebx xor ebx, ebx imul edx, ds:random_number[ebx], 8088405h inc edx mov ds:random_number[ebx], edx mul edx mov eax, edx pop ebx retn |
1 2 3 4 5 | int __fastcall generate_new_session_key(unsigned int mul_param) { random_number = (134775813 * random_number) + 1; return ((unsigned int)random_number * (unsigned __int64)mul_param) >> 32; } |
To generate a new session key this function does two things:
- Update the number random_number.
- Use the parameter mul_param in combination with this random number for session_key generation.
Some debugging reveals that mul_param is always 0x00FFFFFF. If random_number is 0xAABBCCDD the session_key will thus become something like 0x00AABBCD (random_number is shifted by one byte and the multiplication leads to some changes in the last bits). This explains our observation that although there are 4 bytes reserved for the session_key it will always only occupy 3 bytes. To complete our overview the next thing we need to find out is how random_number is initialized. A scan for all referenced usages reveals the following function (found @004030D0).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | add esp, 0FFFFFFF8h push esp call QueryPerformanceCounter_0 test eax, eax jz short loc_4030E8 mov eax, dword ptr [esp+8+var_8] mov ds:random_number, eax pop ecx pop edx retn call GetTickCount_0 mov ds:random_number, eax pop ecx pop edx retn |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | DWORD __cdecl System::Randomize() { DWORD result; // eax@2 __int64 v1; // [sp+0h] [bp-8h]@1 if ( QueryPerformanceCounter_0((LARGE_INTEGER *)&v1) ) { result = v1; random_number = v1; } else { result = GetTickCount_0(); random_number = result; } return result; } |
This seems to be an internal Delphi function. Let’s look into its documentation.
The Randomize procedure is used in conjunction with the Random function. It repositions the random number generator in its sequence of 232 pseudo random numbers. Randomize uses the time of day as the seed for this repositioning, so should provide a reliable method of creating an unpredictable sequence of numbers, even if they are a part of a predetermined sequence.
This function is only called once the server is started in order to initialize random_number. Both functions have to do with the Delphi internal pseudorandom number generation that is a Linear congruential generator (LCG) and are used in a commonly known way to generate pseudo random numbers: 1) Setting a seed 2) Calling the random function. However it is known that using a Linear congruential generator is not secure for cryptographic usage since about 1987 [ref]. By the time I analyzed this issue I didn’t know these things and found out how to break this pseudorandom number generator (PRNG) on my own.
Breaking an LCG based PRNG
LCG setup
Let’s have a look at the mathematical definition of an LCG. An LCG is defined by the following recurrence relation:
sequence of pseudorandom numbers | |
n-th random_number ( is called the seed) | |
multiplier (in our case 134.775.813) | |
increment (in our case 1) | |
modulus (in our case we have GF(232) so it is 4.294.967.296) |
Altogether Delphi and TS 2 use the following LCG:
Remember that our goal is to be able to make predictions about all generated session keys. This goal can be reached by finding which allows us to compute the complete sequence. At this point we need to focus on what we are given. In addition to our own session_key we also know n (the number of already generated random numbers) because it corresponds to our client_id. The n-th session_key is a transformed version of :
(c.f. generate_new_session_key source code).
Since this transformation mostly is a one-way function we loose information here. If we are given the n-th session_key it is thus impossible to retrieve its correct . Luckily by connecting a second time and retrieving the (n+1)-th session key we can determine exactly (having two consecutive session keys compensates this information loss). By retrieving we can already predict all future session keys. However since we also want to get correct session keys for already connected users we still have to aim for .
Seed computation
Surely one approach is to brute-force all 232 possible and to each time check if the n-th generated number matches our . Let’s not hesitate but think about a more elegant solution first. With some reflection we can reformulate our given problem to:
e_multiplier | exponentiated multiplier ( where a is 134.775.813) |
remaining_polynom |
By solving the equation for we finally get:
We have to be careful with inverse numbers since we are in a GF (c.f. Modular multiplicative inverse). only exists, because the multiplier used by Delphi has some specific features. At this point I would like to thank my colleague Dario Weißer for his kind and insightful help regarding these cryptography issues. The following pseudocode can be used to deterministically retrieve the correct seed given only the sequence position n and its random number .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function get_lcg_seed(int n, int nth_random_number) { // All calculations are meant to happen in GF(2^32)! int e_multiplier = pow(134775813, n); int lcg_seed, remaining_polynom = 1, sub_multiplier = 1; for(i=1; i < n; i++) { sub_multiplier *= 134775813; remaining_polynom += sub_multiplier; } // mul_inverse is a function that calculates // the multiplicative inverse of an element in GF(2^32) lcg_seed = (nth_random_number - remaining_polynom) * mul_inverse(e_multiplier); return lcg_seed; } |
By using an algorithm like this we can immediately get and compute the complete LCG sequence.
Final steps for hijacking
Consequently the things we need to do in order to hijack a specific session is:
- Connect to retrieve a session_key and a client_id n. Disconnect afterwards
- Connect again to retrieve another session_key.
- Use both keys to get the correct n-th random_number.
- Use the target’s client_id, n and the n-th random number to calculate the target’s session_key (using get_lcg_seed).
- Brute-force the correct sequence number by sending text messages as the “target” to our client. Once we receive a text message we know that we have found the correct session key and sequence number.
The puppet master
There are some interesting things we can do after having successfully hijacked a session. Two examples for such are:
- Privilege escalation by granting server admin rights to one’s client
Only possible if a server admin is allowed to give server admin privileges to other users (this is the case by default). - Voice hijacking
Faking the voice’s origin becomes feasible, too. The best part here is that the target is not able to notice that it has been hijacked (there is no point for the server to send voice packets back to their origin).
Limitations
One major limitation is that we can’t receive packets that are directed to the hijacked target. Thus we can’t intercept text messages or voice packets that have been sent privately/only to this target.
At this point I suggest that you have a closer look at the video in the beginning again, most things should make sense now.
Some remarks
My favorite sentence of the Delphi documentation is “should provide a reliable method of creating an unpredictable sequence of numbers” (c.f. “randomize” documentation). Claiming to generate unpredictable numbers is NEVER a good idea… Keeping the same (vulnerable) PRNG algorithm for such a long time isn’t a good idea either.
Conclusion
In conclusion we can learn the following things:
- Previous to generating any numbers please inform yourself about what PRNG algorithm you are really using and about how it has to be properly used.
- Please stop using TeamSpeak 2 (why are there still so many servers up and running?) and move on to TeamSpeak 3 already…
Add Comment