A chain is no stronger than its weakest link
It is known that any system reliability is determined by its weakest link. Now we take a good look at the protection from copying of one popular toy that was released a few days ago for OS X and the way of its bypass. In addition, we just look at one of the options for implementing the protection from copying. Of course, this research was conducted in the study purposes, and you still should buy the good software and games.
Let’s run the game and see the registration form or purchase. The registration is done online by entering a serial number, or manually by entering a name and the key in accordance with the displayed identifier of a specific computer. Next, we run gdb and get program exited with code 055.
IOS X has an option to disable debugging process by calling it ptrace
(PT_DENY_ATTACH, 0, 0, 0). It's not tricky, we run to disassemble our binary otool or remarkable program otx, which writes some useful information in the output file, and we get 68 megabytes of assembly code. We look for a call ptrace(), but we could not find it, that's interesting. Well, we set a breakpoint on the call of this function and see the following:
Breakpoint 1, 0x98ce6a18 in ptrace ()
#0 0x98ce6a18 in ptrace ()
#1 0x00e3867f in StartupLicensing ()
#2 0x00e380d9 in Protect ()
#3 0x00cefdcd in dyld_stub_write ()
#4 0x8fe11203 in __dyld__ZN16ImageLoaderMachO18doModInitFunctionsERKN11ImageLoader11LinkContextE ()
#5 0x8fe10d68 in __dyld__ZN16ImageLoaderMachO16doInitializationERKN11ImageLoader11LinkContextE ()
#6 0x8fe0e2c8 in __dyld__ZN11ImageLoader23recursiveInitializationERKNS_11LinkContextEjRNS_21InitializerTimingListE ()
#7 0x8fe0f268 in __dyld__ZN11ImageLoader15runInitializersERKNS_11LinkContextERNS_21InitializerTimingListE ()
#8 0x8fe03694 in __dyld__ZN4dyld24initializeMainExecutableEv ()
#9 0x000288c2 in _start ()
#10 0x00028838 in start ()
Namely, there are called the functions by a cunning way that is why this piece of code does not disassemble. In the function _start it looks like this:
+110 call 0x00c60323 ___keymgr_dwarf2_register_sections
+115 leal 0xe0(%ebp),%eax
+118 movl %eax,0x04(%esp)
+122 movl $0x004ebe64,(%esp) __dyld_make_delayed_module_initializer_calls
+129 calll __dyld_func_lookup
+134 call *0xe0(%ebp)
Somewhere must be registered an address of some initialization function, which is apparently in a different segment / section, and it is called by non-trivial way. If we just looked at the output otool-l, then we would have found this one:
Load command 6
align 2^2 (4)
and second one (at this address are function addresses that are called ImageLoaderMachO::doModInitFunctions, in our case it is Protect)
align 2^2 (4)
Now we only have to find them in a binary. There is hidden one full executable module, we disassemble it and get our Protect function.
Protect function does not do anything interesting itself, and contains calls like
StartupLicensing, NeedsLicenseAsk, ValidDemoLaunch, AskForLaunchOptions, and finally, RunRealStaticInitializers, and afterwards it terminates. Let’s try to change the return value (to be more precise its verification), for example,
ValidDemoLaunch. After this the program just ends without any messages and errors. So we will see the function
Let’s take a look at some other function names:
DigitalRiver::CheckEccSignatureFromPublic(unsigned char*, int*, char const*, char const*, int)
DigitalRiver::MakeEccPublicKeyBlock(char const*, int)
DigitalRiver::Random128::init(char const*, unsigned long)
DigitalRiver::TEA_Crypt(unsigned long*, void*, unsigned long, int)
DigitalRiver::GetKeyMD5(unsigned long*, char const*, int)
DigitalRiver::KeyAndCertificate::DecryptCertificate(void const*, unsigned long, int)
And so on, that gives the impression of a serious protection with some non-trivial cryptography. Anyway, let’s get back to our function, cryptography is scary, but the programs that use it were cracked, because that jne was replaced with je.
Here is almost a complete listing
+18 00002de8 movl 0x0c(%ebp),%eax
+21 00002deb leal 0xfffffee8(%ebp),%esi
+27 00002df1 movl $0x00000100,0x08(%esp)
+35 00002df9 movl %esi,0x04(%esp)
+39 00002dfd shrl $0x02,%eax
+42 00002e00 movl %eax,0xfffffee0(%ebp)
+48 00002e06 leal 0x00019bef(%ebx),%eax
+54 00002e0c movl %eax,(%esp)
+57 00002e0f calll _DRGetVariable
+62 00002e14 movl 0xfffffee0(%ebp),%edx
+68 00002e1a testl %edx,%edx
+70 00002e1c setne %dl
+73 00002e1f testl %eax,%eax
+75 00002e21 movl %eax,0xfffffee4(%ebp)
+81 00002e27 setne %al
+84 00002e2a testb %al,%dl
+86 00002e2c je 0x00002ebd
+92 00002e32 movl %esi,(%esp)
+95 00002e35 calll 0x00026557 _strlen
+100 00002e3a movl 0x0c(%ebp),%edx
+103 00002e3d movl %edx,(%esp)
+106 00002e40 movl %eax,%esi
+108 00002e42 calll 0x000264f3 _malloc
+113 00002e47 xorl %ecx,%ecx
+115 00002e49 movl %eax,%edi
+117 00002e4b jmp 0x00002e71
+119 00002e4d movl %ecx,%eax
+121 00002e4f xorl %edx,%edx
+123 00002e51 divl %esi
+125 00002e53 movl %edx,0xfffffed4(%ebp)
+131 00002e59 movl 0x08(%ebp),%edx
+134 00002e5c movzbl (%edx,%ecx),%eax
+138 00002e60 movl 0xfffffed4(%ebp),%edx
+144 00002e66 xorb 0xfffffee8(%ebp,%edx),%al
+151 00002e6d movb %al,(%edi,%ecx)
+154 00002e70 incl %ecx
+155 00002e71 cmpl 0x0c(%ebp),%ecx
+158 00002e74 jb 0x00002e4d
+160 00002e76 xorl %esi,%esi
+162 00002e78 jmp 0x00002e9f
+164 00002e7a movl (%edi,%esi,4),%eax
+167 00002e7d testl %eax,%eax
+169 00002e7f je 0x00002e9e
+171 00002e81 movl 0x1c(%ebp),%edx
+174 00002e84 movl %edx,0x0c(%esp)
+178 00002e88 movl 0x18(%ebp),%edx
+181 00002e8b movl %edx,0x08(%esp)
+185 00002e8f movl 0x14(%ebp),%edx
+188 00002e92 movl %edx,0x04(%esp)
+192 00002e96 movl 0x10(%ebp),%edx
+195 00002e99 movl %edx,(%esp)
+198 00002e9c call *%eax
+200 00002e9e incl %esi
+201 00002e9f cmpl 0xfffffee0(%ebp),%esi
+207 00002ea5 jb 0x00002e7a
For a start we try to get out of this function with the good return code. Then the program begins to fall mostly in its own code, there we see continuous function calls at some addresses, which are stored in the memory at other addresses. Yeah, so this thing still initializes something important. Let's take a closer look at it.
The first thing we see here is a function call
DRGetVariableat string +57 and without a valid key the variable is not found and program is terminated. We try to put any value, and we get a fall at string
+198. Now we look very closely and see the amazing things:
+57we get the value of a variable, buffer size is 255 bytes.
+95we see the value length that was obtained.
+108we separate out 336 bytes of memory (=84*4).
+119we XOR byte by byte some values from the memory with our variable and we put it in the allocated memory.
+160we consider these values as 84 addresses and call them.
We get XOR with a key length smaller than the data after all functions with clever names. If we have the addresses in our small binary then the high byte must be equal to 0, and the next one also must be equal to 0, 1 or 2. Thus, we know every fourth byte of the key, the smaller is its length, the more known bytes already we have, but we just do not know the length.
We know that we should get the addresses of the functions (rather than data or the middle functions). Let’s write a simple program to search through the data, considering the above conditions, trying to get at the beginning of some functions. It does not work, but at least we know that the key length is 170 bytes.
After a day we find by chance in our main binary a number of functions
+0 00004270 movl $0x0000ffff,%edx
+5 00004275 movl $0x00000001,%eax
+10 0000427a jmp xxxx
+15 0000427f nop
Optimism returns soon we have exactly 84 pieces. I found the real key for decryption. However, I must say, it would not help, because there are more than one possible key, by the way, the length can only be one 251 bytes, but the order does not matter, we take any key or just any sequence of decoded address, write them down then we run and enjoy it.
The conclusion is banal and written at the beginning of the article after the implementation, for example, we should find the weakest spots and protect them from the possible attacks.
I think it is simple to improve the protection. First of all, there should be replaced the direct call with ptrace(), next XOR should not be used at all or at least the length of the key should be increased and predetermination of every 4th byte should be eliminated. The most importantly it is somehow to hide 84 functions of initialization, because they make all protection pointless.
|Vote for this post
Bring it to the Main Page