Contents

A failed Attempt to crack Thunder for Mac

The new film Ready Player One made my heart itch, and I want to go to the Pirate Bay to enjoy the next wave of gun version. Seriously downloaded uTorrent for a while… This shit is too slow to bear. Thunder - start! Download, open…huh? Member trial? try out! Wow. Cool! ——The membership trial is over! Ugh, my god it’s slower than uT. I just feel itchy and want to practice how to write a hook, so come on.

bypass check

Throwing IDA and Hopper are all the most basic. I have to say that since the leak of IDA 7.0, not only the Mac version no longer needs a virtual machine, but also IDA’s method recognition for ObjC is more accurate. It is very, very convenient, and can be directly in the F5 Check out the call of the ObjC method! Great!

I checked some information first, and found an analysis of a relatively new version 1 about half a year ago, which introduced some methods for Xunlei to detect the old version of hooks and bypass the integrity check. It uses a tool, which is Logos2 in theos toolbox. Its usefulness is as a preprocessor of the code, which makes it extremely convenient for us to write hooks~ this tool is the core of this article. I am going to use it first, and then analyze how it is implemented in the next article.

Here I made a little improvement on the hook that bypasses the integrity check, because during the debugging process, I found that besides the directory that Xunlei checks its own MD5 name, there are other plug-ins whose directory length is also 32.

默认	10:35:13.556340 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/d50c5550bf387fc861039286aac68014
默认	10:35:14.196828 +0800	Thunder	XPC: DownloadServer launched: version = 4.0316.460.2 - 4.0316.2r.
默认	10:35:14.699477 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/subtitle.xlplugin/Contents/MacOS
默认	10:35:14.704012 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/660ba3a53307f21b0660d784ccc3b05c
默认	10:35:14.717713 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/viprenew.xlplugin/Contents/MacOS
默认	10:35:14.722513 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/feedback.xlplugin/Contents/MacOS
默认	10:35:14.723403 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/afa7edc8c8b23d21a3b33a0be5858b43
默认	10:35:14.726564 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/settings.xlplugin/Contents/MacOS
默认	10:35:14.730920 +0800	Thunder	===== fileExistsAtPath: /Applications/Thunder.app/Contents/PlugIns/xlplayer.xlplugin/Contents/MacOS

In order not to destroy the function, some improvements have been made to this hook, and the codes are piled at the bottom. Use the unsign I mentioned in the previous article to kill the signature and delete all _CodeSignature directory, we can inject the dylib we just wrote.

DLL injection

There are two methods here, the first one is what I am using now. Since we’ve changed the binary by getting rid of app signing, I don’t mind continuing to change it.

# 此处你已经将 theos clone 下来了。
THEOS=/your/path/to/theos/repo
HOOKFILE=/save/source/file/below
# 生成 dylib
$THEOS/bin/logos.pl $HOOKFILE > hook.m && clang -shared -undefined dynamic_lookup -o /Applications/Thunder.app/Contents/MacOS/libThunder.dylib hook.m
# 向 Thunder 插入它
optool install -c load -p @executable_path/libThunder.dylib -t /Applications/Thunder.app/Contents/MacOS/Thunder

Another way is to utilize the environment variable DYLD_INSERT_LIBRARIES, that is, the /Content/MacOS The binary with the same name inside is replaced with a shell script, which inserts new environment variables and starts the program, which is more elegant. Here is the code of Hopper cracked by ChinaPYG (because I am using it hahaha). It is appended to the original program name with a _.

#!/bin/bash
Hopper_PATH="`dirname "${0}"`"
Hopper_BIN=$Hopper_PATH/"`basename "${0}"`"_

export DYLD_INSERT_LIBRARIES="${Hopper_PATH}/libChinaPYG.dylib"
"$Hopper_BIN"

So far we have successfully bypassed the integrity verification of Thunder and macOS and loaded the first hook library (Sahua), and then we will add functions to it. After each modification, directly run the above command containing logos.pl and clang to deploy the hook we just wrote with one click.

positioning function

Of course, I threw the main program into it at the first time, but later found that some methods with the name Vip in it had nowhere to be referenced, let alone in the applicationWillFinishLaunching It also detects that if isVip is 1 (the old version hook), the program will exit immediately. Absolutely.

Then I found in the log that if I click on the trial, there will be this message.

默认	09:48:58.184348 +0800	Thunder	try tip window init
默认	09:48:58.229485 +0800	Thunder	try tip window deallocated
默认	09:48:59.263859 +0800	Thunder	setUserInfoToPlayer viptaskstate = 3
默认	09:49:00.128474 +0800	Thunder	setUserInfoToPlayer viptaskstate = 3

find hidden files

I use it in the Xunlei directory grep -R position try tip, found it in ./PlugIns/viptask.xlplugin/Contents/MacOS/viptask in. However, I opened the PlugIns directory, but I couldn’t find this plug-in! I ls It’s there again, okay, I’ve got a routine again.

/2018/crack-thunder-for-mac/00.png

I think this thing should be related to the metadata of macOS files. .DS_Store, it is inferred that it should be stored in the file system like the quarantine state. Enumerate.

$ xattr -l /Applications/Thunder.app/Contents/PlugIns/* | grep -v com.apple.quarantine
/Applications/Thunder.app/Contents/PlugIns/660ba3a53307f21b0660d784ccc3b05c: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/afa7edc8c8b23d21a3b33a0be5858b43: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/liveupdate.xlplugin: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/lixianspace.xlplugin: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/settings.xlplugin: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/userlogin.xlplugin: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020
/Applications/Thunder.app/Contents/PlugIns/viptask.xlplugin: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  |........@.......|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020

this com.apple.FinderInfo It’s very interesting. I checked and this is sys/stat.h A hidden flag 3 [squint] is defined in it, and 4 is also given to remove the flag, but the method he introduced is not very useful. I still use the method of removing the quarantine attribute.

# 不是很好用,留作存档
chflags nohidden viptask.xlplugin
# 下面这个可以
xattr -dr com.apple.FinderInfo viptask.xlplugin

This little trick can be written down and used later to fool people. Maybe the CTF will come out soon?

analyzeviptask

In viptask, we can also VipServiceHelper found in isVip method. even this isLogin The method is hooked together first, because it exists in self, which means that the data in the plug-in is copied from the main program.

char __cdecl -[VipServiceHelper isVip](VipServiceHelper *self, SEL a2)
{
  return self->_isVip;
}

But the membership level seems to be different here, a HostController is called here.

unsigned __int8 __cdecl -[VipServiceHelper vipRank](VipServiceHelper *self, SEL a2)
{
  __int64 v2; // rax
  VipServiceHelper *v3; // rbx
  XLHostVipTaskProtocol *v4; // rdi
  unsigned __int8 result; // al

  v3 = self;
  v4 = self->_hostController;
  if ( v4 && (unsigned __int8)objc_msgSend((void *)v4, "respondsToSelector:", "vipRank", v2) )
    result = (unsigned __int64)objc_msgSend((void *)v3->_hostController, "vipRank");
  else
    result = 0;
  return result;
}

In this case, it is better to find its source if we directly hook it. We search in the main program vipRank Find this Controller class is XLHostBaseController. But it calls again with the same method UserController, (that is, the class of the old version hook) and this thing continues to call self->_loginManager->loginSession->userInformation->vipLevel. We followed the map to find XLUserInformation middle vipLevel. Also in this class vasType, isVip, isYear Wait for a bunch of functions, these are the sources that all components read after logging in. We can kill it here, or we can drill down a bit further to where the getter data came from.

back to main program

During login, -[XLLoginSession updateSession:] Called after successful login -[XLUserInformation initWithUserInfo:] Initialize an information class and fill in the information. The key parts of this class are as follows.

// ...
	// v30 = v3.get("vipList")
    v30 = _objc_msgSend(v3, "objectForKey:", CFSTR("vipList"));
    v31 = (void *)objc_retainAutoreleasedReturnValue(v30);
    v32 = _objc_msgSend(&OBJC_CLASS___NSArray, "class");
	// isinstance(v30, NSArray)
    if ( (unsigned __int8)_objc_msgSend(v31, "isKindOfClass:", v32) )
    {
      v34 = _NSConcreteStackBlock;
      v35 = -1040187392;
      v36 = 0;
      // v37: 对 NSArray 中每个元素调用的 block
      v37 = __38__XLUserInformation_initWithUserInfo___block_invoke;
      v38 = &__block_descriptor_tmp_37;
      v39 = _objc_retain(v4, "isKindOfClass:");
      _objc_msgSend(v31, "enumerateObjectsUsingBlock:", &v34);
      _objc_release(v39);
    }
    _objc_release(v31);
// ...

Among them, v37 is a method called on each element of the array, which is equivalent to making a map.

__int64 __fastcall __38__XLUserInformation_initWithUserInfo___block_invoke(__int64 a1, __int64 a2, __int64 a3, _BYTE *a4)
{
  _BYTE *v4; // r15
  v4 = a4;
  v5 = a1;
  v6 = (void *)_objc_retain(a2, a2);
  v7 = _objc_msgSend(&OBJC_CLASS___NSDictionary, "class");
  if ( (unsigned __int8)_objc_msgSend(v6, "isKindOfClass:", v7) )
  {
    v8 = _objc_msgSend(v6, "objectForKey:", CFSTR("vasid"));
    v9 = (void *)objc_retainAutoreleasedReturnValue(v8);
    v63 = v4;
    v10 = v9;
    v11 = _objc_msgSend(v9, "copy");
    v12 = *(_QWORD *)(a1 + 32);
    v13 = *(_QWORD *)(v12 + 200);
    *(_QWORD *)(v12 + 200) = v11;
    _objc_release(v13);
    _objc_release(v10);
    if ( (unsigned __int8)_objc_msgSend(*(void **)(*(_QWORD *)(v5 + 32) + 200LL), "isEqualToString:", CFSTR("2")) )
    {
      *v63 = 1;
      v14 = _objc_msgSend(v6, "objectForKey:", CFSTR("payId"));
      v15 = (void *)objc_retainAutoreleasedReturnValue(v14);
      v16 = v15;
      v17 = _objc_msgSend(v15, "copy");
      v18 = *(_QWORD *)(v5 + 32);
      v19 = *(_QWORD *)(v18 + 176);
      *(_QWORD *)(v18 + 176) = v17;
      _objc_release(v19);
      _objc_release(v16);
       v38 = _objc_msgSend(v6, "objectForKey:", CFSTR("vipLevel"));
      // ...
      v50 = _objc_msgSend(v6, "objectForKey:", CFSTR("isVip"));
      // ...
    }
    // ...
  }
  // ...
}

It can be seen that each element of NSArray passed into this method is an NSDictionary. Every element inside will be copied to *(a1 + 32) A block of space within this object. And there is also a premise that vasid Must be 2. so -[XLUserInformation vasId] We also have to hook into that.

Expressway

So far, we have been able to successfully log in and be displayed as a high-end and high-end SVIP6 super member with an annual fee. However, choosing to accelerate at this time will still fail to accelerate.

Some of the download task management, including the high-speed channel, is completed by the aforementioned viptask plugin, which can be seen from the string list of IDA. I found that IDA doesn’t automatically recognize some unquoted UTF-16LE strings. Maybe in the future I will make a plugin to automate it.

~~ Let’s add a link to the dynamic library for the plug-in according to the gourd painting. ~~ After trying for a day, we found that we can’t let the plugin also dynamically link a certain library when loading. Because when Xunlei is dealing with the plug-in mechanism, it implements a XLBundleManager. And this loader is really too complicated… And I have never learned Objective-C, I can’t even understand F5… I can’t do it. According to my understanding, this Bundle is like a dynamically loaded code block, which is closer to DEX loading in Android than DLL, because the result of loading is not a group of functions that are globally available in the GOT table, but It is the Class object that needs to be obtained with a special getter.

The next best thing is that since all Objective-C methods are dynamic and are searched by selector (similar to the mangled name of C++ Class) at runtime, all methods must be stored in a certain object in Obj-C runtime. place. Can we inject it at a certain point after the plugin is loaded and before it runs? I selected this in the getter method of vipTaskPlugin -[XLHostTaskController vipTaskPluginObject] superior.

id __cdecl -[XLHostTaskController vipTaskPluginObject](XLHostTaskController *self, SEL a2)
{
  XLHostTaskController *v2; // rbx
  XLPluginVipTaskProtocol *v3; // rdi
  ...
  v2 = self;
  v3 = self->_vipTaskPluginObject;
  if ( !v3 )
  {
    v14 = 0LL;
    v15 = &v14;
    v16 = 0x32000000;
   	...
    v4 = _objc_msgSend(v2, "plugins");
    v5 = (void *)objc_retainAutoreleasedReturnValue(v4);
    v8 = _NSConcreteStackBlock;
    v9 = -1040187392;
    v10 = 0;
    v11 = __43__XLHostTaskController_vipTaskPluginObject__block_invoke;
    v12 = &__block_descriptor_tmp_42;
    v13 = &v14;
    // 使用 block invoke 枚举、查找 ID 为 com.xunlei.plugin.task.viptask 的插件并加载
    _objc_msgSend(v5, "enumerateObjectsUsingBlock:");
    v6 = &v2->_vipTaskPluginObject;
    _objc_release(v5);
    // ...
  }
  // ...
}

So we hook this method to judge that before it is called self->_vipTaskPluginObject Whether it has been loaded, the hook to the plugin can be applied after the plugin is successfully loaded.

But after another operation, it was found that this plug-in, like other plug-ins, is only responsible for updating the interface status.

// A lot of things are omitted here, the XPC service part is quite complicated, C++/Obj-C mixed, very annoying.

In that XPC service, Xunlei sent requests to the same API at different endpoints for official/trial VIP members when starting the acceleration. MITMProxy captured the packet and found that it was a binary stream, which was guessed to be encrypted. Use the domain name to locate a call to the AES tool class xl_aes_decrypt method, the debugger breaks down and parses to get the following return value. is a UTF8 string.

(lldb) print (char*)0x00007FFE7EE38200
(lldb) print (char*)0x00007FFE7EE38200
(char *) $1 = 0x00007ffe7ee38200 "{"flux_need":0,"flux_remain":0,"result":104,"message":"[08104] \xffffffe6\xffffffb5\xffffff81\xffffffe9\xffffff87\xffffff8f\xffffffe4\xffffffb8\xffffff8d\xffffffe8\xffffffb6\xffffffb3","client_sequence":0,"speed_duration":1800,"task_infos":[{"not_sec_requery_interval":900,"requery_averagetime":30,"requery_threshold":800,"message":"[08104] \xffffffe6\xffffffb5\xffffff81\xffffffe9\xffffff87\xffffff8f\xffffffe4\xffffffb8\xffffff8d\xffffffe8\xffffffb6\xffffffb3","result":104,"simple_msg":"\xffffffe6\xffffffb5\xffffff81\xffffffe9\xffffff87\xffffff8f\xffffffe4\xffffffb8\xffffff8d\xffffffe8\xffffffb6\xffffffb3","gcid":"FC610043C589F374429402CC9741051943961FEB"}],"is_super_privilege":0,"simple_msg":"\xffffffe6\xffffffb5\xffffff81\xffffffe9\xffffff87\xffffff8f\xffffffe4\xffffffb8\xffffff8d\xffffffe8\xffffffb6\xffffffb3","flux_capacity":0}\a\a\a\a\a\a\a"

>>> bs.decode()
'{"flux_need":0,"flux_remain":0,"result":104,"message":"[08104] 流量不足","client_sequence":0,"speed_duration":1800,"task_infos":[{"not_sec_requery_interval":900,"requery_averagetime":30,"requery_threshold":800,"message":"[08104] 流量不足","result":104,"simple_msg":"流量不足","gcid":"FC610043C589F374429402CC9741051943961FEB"}],"is_super_privilege":0,"simple_msg":"流量不足","flux_capacity":0}\x07\x07\x07\x07\x07\x07\x07'

Then I just gave up. This JSON will be converted into a structure inside the service, and the data type offset is unknown, so it is very troublesome to find the reference. I’ll just use the old version.

References