淺談 iOS Device ID 的修改

0x11901發表於2019-03-03
淺談 iOS Device ID 的修改
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:%@
Return 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 ":	%s		%s
", 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 
", 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(@"

GetName:
	entry:%zd
	io_name_t%s
	ret:%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(@"

SearchCFProperty:
	key:%@
	ret:%@
	%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:%@
Return 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

相關文章