Last Updated: 05-Mar-2006

Unleashing 10-User Conferencing in Skype 2.0 for Windows

Recently, Skype and Intel have announced a deal that would limit Skype's functionality on all but specific Intel processors. Currently, Skype 2.0 offers 10-way conference calls only on Intel's latest dual-core CPUs, while other chips, including all AMD chips, will only allow for 5-way conference calls. It is argued that only those Intel dual-core CPUs meet the requirements - which would imply that no AMD CPU is fast enough.

Now, what are these requirements? Is there some kind of micro-benchmark built into Skype which measures the processing speed? Or does Skype look for a specific hidden CPU feature? As the details on the patch reveal, the code logic behind the limitation is quite simple:

If it's a CPU with "GenuineIntel" branding and has at least two cores, then allow 10 users; else limit to 5 users.

Download

I've applied the patch, which is discussed below, to Skype version 2.0.0.90 (March, 1st). There is no virus or backdoor added!

You can download the original setup and the patch archive from this location:

Use the SkypeSetup.exe to install or update Skype. Then rename the patched Skype executable to "Skype.exe" and use it instead of the original.

Note: I have updated the patch on Mar, 5. The original (previous) one was not working properly (e.g. Skype would probably close when receiving a call).

Details on the Patch

The patch is the result of two stages: code analysis and design of the patch. The code analysis, or reverse engineering, reveals the relevant code block, which overrides Skype's limitation for Intel's dual-core CPUs. The patch design isolates the minimal set of instructions that need to be modified to cancel this limitation.

Code Analysis

An initial analysis of the executable revealed that the code references the string "You can now add up to 9 people to this call - <a href="skype:?go#intel-10way">learn more</a>"". The relevant code block is

00672680    mov eax, dword ptr [00B8E6DC]         <----- (*)
00672685    cmp dword ptr [eax], 00000004
00672688    jne 006726AE                          # if **(00B8E6DC) != 4 then skip
0067268A    mov eax, dword ptr [ebp-04]
0067268D    mov byte ptr [eax+0000015C], 01
00672694    push 006727BC                         <-- The String
00672699    xor ecx, ecx
0067269B    mov edx, 00672880
006726A0    mov eax, dword ptr [ebp-04]
006726A3    mov eax, dword ptr [eax+00000138]
006726A9    call 00689858

Of interest here is also the code marked with (*). It reveals that the string is somehow used if a certain memory location has the value 4. Theory is, this 4 means "4 additional conference members"; and if you're not running a dual-core Intel CPU, the string is shown and motivates you to "upgrade" to such a CPU. OK, now we have something to work with.

Next step is to find out, where this memory location is modified (it's initial value in the executable is 4, so it has to be changed in the code.) That's easy, only one location serves as a candidate:

006253D1    call 0071D474                         <----- (**)
006253D6    sub eax, 00000001
006253D9    jno 006253E0
006253DB    call 0040406C
006253E0    test eax, eax
006253E2    jns 006253E9
006253E4    call 00404064                        # return value in eax
006253E9    mov edx, dword ptr [00B8E6DC]
006253EF    mov dword ptr [edx], eax             # **(00B8E6DC) := eax

Instead of analysing the call to 0x404064, we focus on the call to 0x71d474, marked above with (**):

0071D474    push ebp
0071D475    mov ebp, esp
0071D477    add esp, FFFFFFF8
0071D47A    mov dword ptr [ebp-04], eax
0071D47D    mov eax, dword ptr [ebp-04]
0071D480    mov eax, dword ptr [eax+04]
0071D483    push eax
0071D484    call 00724D54                        <----- (***)
0071D489    pop ecx
0071D48A    mov dword ptr [ebp-08], eax
0071D48D    mov eax, dword ptr [ebp-08]
0071D490    pop ecx
0071D491    pop ecx
0071D492    pop ebp
0071D493    ret

# (***)
00724D54    jmp 0072A030                        <----- (****)

# (****)
0072A02E    in ax, dx
0072A02F    loopnz 0072A019
0072A031    inc eax
0072A032    pop ecx

The call reaches the "routine" marked with (****). However, it's not regular code, it's garbarge. Or encrypted. The easiest way to find out is to use a debugger. Setting the breakpoint to the code marked with (**), 0x6253D1, and inspecting the memory reveals the true code. This code itself is not shown here, since it's not too interesting and it's not worthwile to drill down. Instead, by having access to decrypted code, we may reveal new code of interest. What would that be? An initial guess is usage of the "cpuid" instruction, which allows to query a lot of information about the CPU. There are some usages in the decrypted code, e.g. for querying MMX/SSE capabilities.

And there is this code, which is actually the core logic we're looking for:

007FF780    push ebp
007FF781    mov ebp, esp
007FF783    sub esp, 00000014
007FF786    push ebx
007FF787    push esi
007FF788    mov esi, 00000003
007FF78D    mov [ebp-04], 00000001
007FF794    xor eax, eax
007FF796    cpuid                         # returns Vendor string in ebx/ecx/edx
007FF798    mov dword ptr [ebp-14], ebx
007FF79B    mov dword ptr [ebp-10], edx
007FF79E    mov dword ptr [ebp-0C], ecx
007FF7A1    mov [ebp-08], 00
007FF7A5    cmp eax, 00000004
007FF7A8    jl 007FF7BD
007FF7AA    mov eax, 00000004
007FF7AF    xor ecx, ecx
007FF7B1    cpuid                        # returns Deterministic Cache Parameters
007FF7B3    shr eax, 1A                  # Max. number of processor cores -1
007FF7B6    and eax, 0000003F
007FF7B9    inc eax                      # Now, eax = Max. number of processor cores
007FF7BA    mov dword ptr [ebp-04], eax
007FF7BD    lea eax, dword ptr [ebp-14]
007FF7C0    push 00AEAEB0                # Pointer to string "GenuineIntel"
007FF7C5    push eax
007FF7C6    call 009DCDE0                # compare vendor string with "GenuineIntel"
007FF7CB    add esp, 00000008
007FF7CE    test eax, eax                # is it "GenuineIntel"?
007FF7D0    jne 007FF7E5                 # No? then test for AMD...

            # It's an Intel CPU
007FF7D2    mov edx, dword ptr [ebp-04]  # number of CPU cores
007FF7D5    mov ecx, 00000001
007FF7DA    cmp ecx, edx                 # cores > 1 ?
007FF7DC    pop esi
007FF7DD    sbb eax, eax                 # eax := (cores > 1) ? -1 : 0
007FF7DF    pop ebx
007FF7E0    inc eax                      # eax := (cores > 1) ? 0 : 1
007FF7E1    mov esp, ebp
007FF7E3    pop ebp
007FF7E4    ret                          # return eax as the result.

            # It's not an Intel CPU
007FF7E5    lea edx, dword ptr [ebp-14]
007FF7E8    push 00AEAEA0                # Pointer to string "AuthenticAMD"
007FF7ED    push edx
007FF7EE    call 009DCDE0
007FF7F3    add esp, 00000008
007FF7F6    test eax, eax                # is it "AuthenticAMD"?
007FF7F8    mov eax, 00000002            # eax := 2
007FF7FD    je 007FF801
007FF7FF    mov eax, esi                 # eax := 3 (if not "AuthenticAMD")
007FF801    pop esi
007FF802    pop ebx
007FF803    mov esp, ebp
007FF805    pop ebp
007FF806    ret                          # return eax as the result.

I've highlighted the most relevant instructions. The routine returns in register "eax" the value

It will be interesting to see how this result is used. Actually, there is only one caller of the routine. Here's the essence of the logic:

007CCCE0    call 007FF780                # check CPU vendor/cores (see above) -> eax
007CCCE5    mov esi, eax                 # copy result from eax to register esi
007CCCE7    lea ecx, dword ptr [ebx+16]
007CCCEA    neg esi                      # CF set to 0 if the source is 0; else 1.
007CCCEC    sbb esi, esi                 # esi := Intel/Dual-Core ? 0 : -1
007CCCEE    and esi, FFFFFFFB            # esi := Intel/Dual-Core ? 0 : -5
007CCCF1 P  add esi, 0000000A            # esi := Intel/Dual-Core ? 10 : 5
007CCCF4    call 008A6260
007CCCF9    cmp esi, eax
007CCCFB    jbe 007CCCFF
007CCCFD    mov esi, eax
007CCCFF    mov edx, dword ptr [ebx+0E]
007CCD02    push 00000000
007CCD04    push esi                     # either 10 or 5
007CCD05    push 0000007D
007CCD07    mov eax, dword ptr [edx+000006C1]
007CCD0D    lea ecx, dword ptr [eax+12]
007CCD10    mov eax, dword ptr [eax+12]
007CCD13    call dword ptr [eax]         # calls code at 7ecc60
007CCD15    pop edi
007CCD16    pop esi
007CCD17    mov al, 01
007CCD19    pop ebx
007CCD1A    mov esp, ebp
007CCD1C    pop ebp
007CCD1D    ret

Again, the relevant code is highlighted. The location 0x7CCCF1 is marked with a "P", since we will place the patch here.

Patch Design

By looking at location 0x7CCCF1 ("P"), it's easy to see, how to build an appropriate patch. We simply have to make sure that the register ESI has the value 0, no matter what CPU we're running on. The "add" instruction at 0x7CCCF1 ("P") will then result in 10, which is the maximal allowed number of persons in a conference.

However, there are two obstacles:

We have two options here. One is to prepare an already decrypted binary, and also find and circumvent the code checking/hashing. This seems to be a lot of work. The second option is using code whch modifies the bytes during runtime and cleans itself up afterwards. This seems simple enough.

Let's see what we need:

  1. Unused space in the executable for the patching code.
  2. A call to the patching code. This call instruction has to be a) placed somewhere in the non-encrypted code, b) executed after the decryption has been done, and c) executed before the CPU/dual-core check is called.

Regarding point 1, remember, Skype uses a checksum/hash to check for modified code. Thus, we have to be careful. But why place the patching code in the code section, anyway? The header section of the binary has plenty of space (0x88 bytes actually) and it is not checked by Skype. The header resides at 0x400000 in memory and we have the range 0x400078 to 0x400100 for our use.

Regarding point 2, by using a debugger and the proven trial-and-error method, an instruction for calling the patching code, which fulfills requirements a) to c), is found at location 0xB7CC40:

00B7CC40    mov edi, 00B91680

Now that we have all that's required, let's design the patch logic.

First, we have to replace the instruction at 0xB7CC40 to call our routine with the actual patching code, which is located in the executable's header at 0x400078:

00B7CC40    call 00400078

The patching code works in two phases:

  1. When it is called from 0xB7CC40, it has to modify the CPUID check. Thus, it replaces the code at location 0x7CCCF1 ("P") with a call to 0x400078 (i.e. to itself) and cleans up by restoring the original five bytes at 0xB7CC40. Finally, it sets the return address on the stack to 0xB7CC40, so that the original code is executed.
  2. At some time, the CPUID check logic is called next by Skype and the call at location 0x7CCCF1 ("P") to 0x400078 is executed. The patching code detects this second phase by looking at the value in 0x7CCCF1. If it's already modified (by phase one), we restore the original five bytes at 0x7CCCF1 as a clean-up measure and take action to allow for 10 conference users. This is done by setting the ESI register to 0. Finally, it sets the return address on the stack to 0x7CCCF1, so that the original code is executed.

After the second phase, Skype's state is modified to allow for 10 users. And the code segment is restored to its original form, so Skype won't detect what has been done.

One final problem needs to be solved: We cannot simply write into the code segment - this would result in a memory protection exception, since Windows and Skype itself set the access rights to "executable and readable" (i.e. not writable). However, temporarily circumventing the memory protection is easy. We just use Windows' VirtualProtect kernel function.

Patch Code

In summary, the patch code performs these steps:

  1. Make the code at 0x7CCCF0 - 0x7CCD00 writeable and save original access rights,
  2. Detect the current phase, by looking at the value in 0x7CCCF1:
    1. First Phase - Patch the bytes at 0x7CCCF1 to make a call the patching code (second phase).
    2. Second Phase - Restore original contents of 0x7CCCF1 and modify the ESI register. Since the registers have been saved on the stack, it modifies the ESI value at stack offset 4.
  3. Restore original access rights to memory 0x7CCCF0 - 0x7CCD00.
  4. Restore original contents of 0xB7CC40.

Here's the complete assembler code:

00400078    push ebp
00400079    mov ebp, esp
0040007B    sub esp, 00000008              # Make room for two 32-bit local vars
0040007E    pushad

            # 1. Make CPUID-Check Code at 007CCCF1 writeable in memory.
0040007F    lea eax, dword ptr [ebp-04]
00400082    push eax
00400083    push 00000040                  # PAGE_EXECUTE_READWRITE
00400085    push 00000010                  # length
00400087    push 007CCCF0                  # location (007CCCF0 - 007CCD00)
0040008C    mov esi, 00408258
00400091    call esi                       # kernel32.VirtualProtect
00400093    mov eax, 007CCCF1

            # 2. Has CPUID-Check at 0x007CCCF1 been restored to its original bytes?
00400098    cmp byte ptr [eax], E8
0040009B    je 004000A9                    # No, then goto 004000A9

            # Case 2a: Modify 0x007CCCF1 to call patching code
0040009D    mov dword ptr [eax], C33382E8
004000A3    mov [eax+04], FF
004000A7    jmp 004000B8

            # Case 2b: Restore original bytes at 0x007CCCF1 and clear ESI register
004000A9    mov dword ptr [eax], E80AC683      # Restore original 5 bytes
004000AF    mov [eax+04], 67                   # - " -
004000B3    and dword ptr [esp+04], 00000000   # Set esi register on stack to 0.

            # 3. Restore access rights of CPUID-Check Code at 007CCCF1 in memory
004000B8    lea eax, dword ptr [ebp-08]
004000BB    push eax
004000BC    push [ebp-04]                  # restore original access rights
004000BF    push 00000010
004000C1    push 007CCCF0
004000C6    call esi                       # kernel32.VirtualProtect

            # 4. Restore original bytes at 0x00B7CC40
004000C8    lea eax, dword ptr [ebp-04]
004000CB    push eax
004000CC    push 00000040                  # PAGE_EXECUTE_READWRITE
004000CE    push 00000010
004000D0    push 00B7CC40                  # location (00B7CC40 - 00B7CC50)
004000D5    call esi                       # kernel32.VirtualProtect

004000D7    mov eax, 00B7CC40
004000DC    mov dword ptr [eax], B91680BF  # Restore original 5 bytes
004000E2    mov [eax+04], 00               # - " -

004000E6    lea eax, dword ptr [ebp-08]
004000E9    push eax
004000EA    push [ebp-04]                  # restore original access rights
004000ED    push 00000010
004000EF    push 00B7CC40
004000F4    call esi                       # kernel32.VirtualProtect

            # Clean up and return to restored instruction
004000F6    popad
004000F7    mov esp, ebp
004000F9    pop ebp
004000FA    sub dword ptr [esp], 00000005  # change EIP on the stack
004000FE    ret

The Result

The screenshot below is from a system with an AMD X2 CPU. Please don't get confused; this is Windows - just a bit visually enhanced by the wonderful FlyakiteOSX transformation pack.

Illustration of the Skype Hack