浅谈 iOS Device ID 的修改

Dark side of the Force

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


FBI WARNING

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

为何要修改 iOS Device ID ?

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

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

现状

在开始讲如何作以前,笔者决定先简单介绍一下业界如今已经能作什么: 安全

一款常见的改机软件

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

如何破解一款程序?

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

/* 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 ,另外一个则须要与操做系统斗智斗勇罢了。框架

思路

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

思路

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

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

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

相关文章
相关标签/搜索