浅谈 iOS Device ID 的修改

4,095 阅读8分钟

Dark side of the Force

最近有一篇 文章 介绍了如何实现 AppStore App 自动下载,笔者看后收获良多。不过文中只介绍了如何去模拟用户的操作来完成下载,并没有涉及抹机、IP 更换等内容。所以笔者打算在此分享一下自己对这些方面的经验。


FBI WARNING

  1. 以下内容可能会引起很多人不适,请读者自酌。
  2. 18 岁以下请在家长陪同下观看!
  3. 部分内容可能违反你所在地相关法律,请谨慎模仿

为什么要修改 iOS Device ID ?

修改设备唯一可识别标识可以做很多事前,比如防止根据 UUID 的追踪,避免大数据「杀熟」等。但是在 iOS 设备上目前想做到修改的前提是越狱,所以为了多领几个美团红包而选择承担越狱的风险,是否值得还是要考虑清楚的。 不过在业界有大量应用这种技术的产业,比如积分墙、ASO 刷榜…… 不过这些产业就属于「灰黑产」了,涉及到了原力的黑暗面,所以笔者不建议涉世不深的读者继续阅读下去。

当你凝视深渊,深渊也在凝视着你。

现状

在开始讲如何做之前,笔者决定先简单介绍一下业界现在已经能做什么:

一款常见的改机软件

如图所示,这是一款在业内非常常见的改机软件。由于作者不可考(不过理应如此,毕竟为了自己的人生安全)、源码遗失、以及 iOS 版本的多次更新,现在已经不值钱了。但是麻雀虽小五脏俱全,它能够修改设备的五码、机型、配置 Apple ID 和一键越狱等。 前人的成功告诉了我们这是可行的,剩下的只是模仿,因此笔者深入逆向并研究了这款软件,在当我看到了一大堆用汇编写的混淆之后…… 放弃了。 所以下面的内容都是笔者编的,大家有兴趣看个开心就好,基本上可以点关闭按钮了 (●°u°●)​ 」

如何破解一款程序?

笔者依稀记得 狗神 在他那本著名的 小黄书 中提到,逆向一款软件最重要的不是最终成品的代码,而是过程的分析与思路。所以经常可以看到一款软件的破解代码重要的也许只有两三行,但是过程有多艰辛也许只有破解者才知道。例如破解 Mac 版 QQ 音乐下载需要 VIP 权限的限制的代码也许加上注释也不到一百行:

/* How to Hook with Logos
Hooks are written with syntax similar to that of an Objective-C @implementation.
You don't need to #include <substrate.h>, it will be done automatically, as will
the generation of a class list and an automatic constructor.

%hook ClassName

// Hooking a class method
+ (id)sharedInstance {
	return %orig;
}

// Hooking an instance method with an argument.
- (void)messageName:(int)argument {
	%log; // Write a message about this call, including its class, name and arguments, to the system log.

	%orig; // Call through to the original function with its original arguments.
	%orig(nil); // Call through to the original function with a custom argument.

	// If you use %orig(), you MUST supply all arguments (except for self and _cmd, the automatically generated ones.)
}

// Hooking an instance method with no arguments.
- (id)noArguments {
	%log;
	id awesome = %orig;
	[awesome doSomethingElse];

	return awesome;
}

// Always make sure you clean up after yourself; Not doing so could have grave consequences!
%end
*/


%config(generator = internal)

#import <Foundation/Foundation.h>
#include <substrate.h>

%hook DownLoadTask

- (BOOL)checkHaveRightToDownload:(int)argument {
	return YES;
}

%end

unsigned int (*old_GetFlexBOOL)(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8);
unsigned int  new_GetFlexBOOL(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  return 1;
}

%ctor {
    NSLog(@"!!!!!!inject success!!!!!!!");

    void * Symbol = MSFindSymbol(MSGetImageByName("/Applications/QQMusic.app/Contents/MacOS/QQMusic"), "_GetFlexBOOL");
    MSHookFunction(Symbol, &new_GetFlexBOOL, (void *)&old_GetFlexBOOL);
}

而真正重要的是找出思路和逆向分析的过程,操作系统本质上也是一个软件,修改 Device ID 其实和破解一款音乐 VIP 限制本质上是一样的,只是一个只需要把 checkHaveRightToDownload 的返回值改成 YES ,另一个则需要与操作系统斗智斗勇罢了。

思路

综上所述,在我们对操作系统下黑手之前应该先理清思路。顺便再说一次以下内容皆是我瞎编的,如有雷同实属巧合:

思路

如图所示,显而易见,如果只是简简单单的修改某个 App 中用到的 Device ID,极大几率只需要勾住「再封装的私有 API」就行了。

而在众多私有 API 中,最著名的当然是大名鼎鼎的 MGCopyAnswer

MGCopyAnswer

// Common form: MGCopyAnswer(CFStringRef string);
CFStringRef value = MGCopyAnswer(kMGDeviceColor);
NSLog(@"Value: %@", value);
CFRelease(value);

基本上平时从 UIDevice 还是其他大部分途径获取 Device ID,皆是通过调用 libMobileGestalt 中的 MGCopyAnswer 函数来获取的。所以只需要勾住 MGCopyAnswer,使其返回的 Device ID 为我们所要的值即可,非常简单明了。

不过虽说思路很简单,但是一个萌新想要勾 MGCopyAnswer 还是会绕很多弯路的,比如最常见的就是「挂短钩」。

挂短钩

在 ARM64 架构下,直接对 MGCopyAnswer 挂钩的话会立即使进程崩溃 invalid instruction。如果通过反汇编手段分析 libMobileGestalt 库:

01 00 80 d2        movz x1, #0
01 00 00 14        b    MGCopyAnswer_internal

易知 MGCopyAnswer 实际上在内部调用了另一个私有无符号的函数 MGCopyAnswer_internal 来实现其功能。因此 MGCopyAnswer 这个函数实际上非常短,只有 8 个字节,而我们使用 Cydia Substrate 对一个 C 函数挂钩的话,它要求被勾函数至少有 16 个字节。因此直接勾住 MGCopyAnswer 时,MGCopyAnswer 函数地址开始的 16 个字节都会被改为 goto,从而破坏了相邻函数的前 8 个字节,使进程崩溃。 因此,当我们吭哧吭哧读完汇编之后,首先想到的方法自然是去勾这个被调用的子函数 MGCopyAnswer_internal,虽说该函数并没有符号,但是在我们吭哧吭哧读了汇编之后,发现其函数地址与 MGCopyAnswer 相差 8 字节。故可以很简单粗暴的写出如下代码:

static CFPropertyListRef (*orig_MGCopyAnswer_internal)(CFStringRef prop, uint32_t* outTypeCode);
CFPropertyListRef new_MGCopyAnswer_internal(CFStringRef prop, uint32_t* outTypeCode) {
    return orig_MGCopyAnswer_internal(prop, outTypeCode);
}

extern "C" MGCopyAnswer(CFStringRef prop);

static CFPropertyListRef (*orig_MGCopyAnswer)(CFStringRef prop);
CFPropertyListRef new_MGCopyAnswer(CFStringRef prop) {
    return orig_MGCopyAnswer(prop);
}

%ctor {
    uint8_t MGCopyAnswer_arm64_impl[8] = {0x01, 0x00, 0x80, 0xd2, 0x01, 0x00, 0x00, 0x14};
    const uint8_t* MGCopyAnswer_ptr = (const uint8_t*) MGCopyAnswer;
    if (memcmp(MGCopyAnswer_ptr, MGCopyAnswer_arm64_impl, 8) == 0) {
        MSHookFunction(MGCopyAnswer_ptr + 8, (void*)new_MGCopyAnswer_internal, (void**)&orig_MGCopyAnswer_internal);
    } else {
        MSHookFunction(MGCopyAnswer_ptr, (void*)new_MGCopyAnswer, (void**)&orig_MGCopyAnswer);
    }
}

显然这段代码除了简单粗暴、没有任何框架检测与异常处理之外完美实现了挂钩任务,但是基于相对偏移量来获取函数地址也并不是很稳。

好在张总在他的一篇博文中提到可以使用 Capstone Engine,一款基于 LLVM MC 的多平台多架构支持的反汇编框架来帮助我们找到 MGCopyAnswer_internal 的「符号」。

static CFStringRef (*old_MGCA)(CFStringRef Key);
CFStringRef new_MGCA(CFStringRef Key) {
    CFStringRef Ret = old_MGCA(Key);
    NSLog(@"MGHooker:%@\nReturn Value:%@", Key, Ret);
    return Ret;
}

%ctor {
    void *Symbol = MSFindSymbol(MSGetImageByName("/usr/lib/libMobileGestalt.dylib"), "_MGCopyAnswer");
    NSLog(@"MG: %p", Symbol);
    csh           handle;
    cs_insn *     insn;
    cs_insn       BLInstruction;
    size_t        count;
    unsigned long realMGAddress = 0;
    // MSHookFunction(Symbol,(void*)new_MGCA, (void**)&old_MGCA);
    if (cs_open(CS_ARCH_ARM64, CS_MODE_ARM, &handle) == CS_ERR_OK) {
        /*cs_disasm(csh handle,
              const uint8_t *code, size_t code_size,
              uint64_t address,
              size_t count,
              cs_insn **insn);*/
        count = cs_disasm(handle, (const uint8_t *)Symbol, 0x1000, (uint64_t)Symbol, 0, &insn);
        if (count > 0) {
            NSLog(@"Found %lu instructions", count);
            for (size_t j = 0; j < count; j++) {
                NSLog(@"0x%" PRIx64 ":\t%s\t\t%s\n", insn[j].address, insn[j].mnemonic, insn[j].op_str);
                if (insn[j].id == ARM64_INS_B) {
                    BLInstruction = insn[j];
                    sscanf(BLInstruction.op_str, "#%lx", &realMGAddress);
                    break;
                }
            }

            cs_free(insn, count);
        }
        else {
            NSLog(@"ERROR: Failed to disassemble given code!%i \n", cs_errno(handle));
        }

        cs_close(&handle);

        // Now perform actual hook
        MSHookFunction((void *)realMGAddress, (void *)new_MGCA, (void **)&old_MGCA);
    }
    else {
        NSLog(@"MGHooker: CSE Failed");
    }
}

废话不多说了,我们的正题并不在这里。

如何修改 iOS Device ID

接下来的东西我是真的就不会了,但是为了不太斧头蛇尾,我就再瞎掰一段吧。 谈到修改的话,我们首先要弄清楚的一点是我们打算要从哪一层修改?比如 ECID,众所周知它是烧在芯片上的。讲道理的话要修改 ECID 是要对硬件动手的,但是我们一般不需要做的这么彻底,而是结合具体需求具体分析。例如一个普通、简单的积分墙,我们只需要对积分墙调用的 MGCopyAnswer 挂钩,就可以愉快的玩耍了。但是如果想对 AppStore 或者 iTunes 下手呢?自然仅仅勾个 MGCopyAnswer 是不行的。 例如我们想让手机连接 iTunes 时,iTunes 获取的 Device ID 是伪造的,那么就需要勾住处理手机与电脑间 USB 通信的守护进程——比如说 lockdownd。因为 iTunes 并不会直接读取手机的设备信息,而是从手机上运行的守护进程中请求数据。那么我们是不是只需要在这个守护进程安装一个钩子即可?

typedef void *LockdownConnectionRef;
typedef int   kern_return_t;

typedef unsigned int              __darwin_natural_t;
typedef __darwin_natural_t        __darwin_mach_port_name_t;
typedef __darwin_mach_port_name_t __darwin_mach_port_t;
typedef __darwin_mach_port_t      mach_port_t;
typedef mach_port_t               io_object_t;
typedef io_object_t               io_registry_entry_t;

typedef char         io_name_t[128];
typedef unsigned int IOOptionBits;

static kern_return_t (*oldIORegistryEntryGetName)(io_registry_entry_t entry, io_name_t name);
kern_return_t newIORegistryEntryGetName(io_registry_entry_t entry, io_name_t name) {
    int ret = oldIORegistryEntryGetName(entry, name);
    NSLog(@"\n\nGetName:\n\tentry:%zd\n\tio_name_t%s\n\tret:%d", entry, name, ret);
    return ret;
}

static CFTypeRef (*oldIORegistryEntrySearchCFProperty)(
    io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFTypeRef allocator, IOOptionBits options);
CFTypeRef newIORegistryEntrySearchCFProperty(
    io_registry_entry_t entry, const io_name_t plane, CFStringRef key, CFTypeRef allocator, IOOptionBits options) {
    CFTypeRef ret = oldIORegistryEntrySearchCFProperty(entry, plane, key, allocator, options);
    NSLog(@"\n\nSearchCFProperty:\n\tkey:%@\n\tret:%@\n\t%lu", key, ret, CFGetTypeID(ret));
    return ret;
}

static CFPropertyListRef (*old_lockdown_copy_value)(LockdownConnectionRef connection,
                                                    CFStringRef           domain,
                                                    CFStringRef           key);
CFPropertyListRef new_lockdown_copy_value(LockdownConnectionRef connection, CFStringRef domain, CFStringRef Key) {
    CFPropertyListRef Ret = old_lockdown_copy_value(connection, domain, Key);
    NSLog(@"LDHooker:%@\nReturn Value:%@", Key, Ret);
    return old_lockdown_copy_value(connection, domain, Key);
}

% ctor {
    void *SymbolGN =
        MSFindSymbol(MSGetImageByName("/System/Library/Frameworks/IOKit.framework/IOKit"), "_IORegistryEntryGetName");
    NSLog(@"GName: %p", SymbolGN);
    MSHookFunction((void *)SymbolGN, (void *)newIORegistryEntryGetName, (void **)&oldIORegistryEntryGetName);

    void *SymbolSC = MSFindSymbol(MSGetImageByName("/System/Library/Frameworks/IOKit.framework/IOKit"),
                                  "_IORegistryEntrySearchCFProperty");
    NSLog(@"SPropertey: %p", SymbolSC);
    MSHookFunction(
        (void *)SymbolSC, (void *)newIORegistryEntrySearchCFProperty, (void **)&oldIORegistryEntrySearchCFProperty);
    }
    else {
        NSLog(@"MGHooker: CSE Failed");
    }
}

其实我想大家应该猜到我下面想做什么了,既然都已经对守护进程下手了,要不干脆我们自己也开一个守护进程的了,加个 root 权限,对所有其他进程安装钩子,如果调用了 Device ID 相关的 API,把返回值魔改掉,岂不美滋滋!代码如下:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 红红火火恍恍惚惚
        NSLog(@"想不到吧,这次我真的编不出来了😂");
    }
    return 0;
}

那么今天的代码就写到这里了,下台鞠躬!


注:以上所有代码全是瞎掰,如能运行,纯属巧合。

参考资料

如何实现 AppStore App 的自动下载

Hooking MGCopyAnswer Like A Boss