首先需要感谢的是这个软件并没有做太多的邪恶校验(当然这是因为App Store不允许混淆加壳吧hhhhh)

打开是卡在激活界面的,因为点击会跳转到itunes到app store,所以可以在这里找字符串(itunes)从而定位到-[NOMORawStoreView jumpAction],然后往上跳转到-[NOMORawStoreView nomoItemClick]

Objective-c
void __cdecl -[NOMORawStoreView nomoItemClick](NOMORawStoreView *self, SEL a2){  void *v3; // x20  void *v4; // x21  void *v5; // x22  void *v6; // x23  void *v7; // x23  unsigned __int8 v8; // w24  if ( -[NOMORawStoreView checkNetworkAccessIsOK](self, "checkNetworkAccessIsOK") )  {    self->userHasClickedBtn = 1;    v3 = objc_msgSend(           objc_alloc((Class)&OBJC_CLASS___NSUserDefaults),           "initWithSuiteName:",           CFSTR("group.com.blink.academy.nomo"));    v4 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "objectForKey:", CFSTR("NOMO-Receipt")));    v5 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "objectForKey:", CFSTR("NOMO-Token")));    v6 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "objectForKey:", CFSTR("NOMO-VIP")));    objc_release(v6);    if ( v6 )    {      v7 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "objectForKey:", CFSTR("NOMO-VIP")));      v8 = (unsigned __int8)objc_msgSend(v7, "boolValue");      objc_release(v7);      if ( (v8 & 1) == 0 )        goto LABEL_10;    }    else if ( !objc_msgSend(v4, "length") && !objc_msgSend(v5, "length") )    {      goto LABEL_10;    }    if ( self->userHasClickedBtn )    {      -[NOMORawStoreView autoCheckNOMOStatus](self, "autoCheckNOMOStatus");LABEL_11:      objc_release(v5);      objc_release(v4);      objc_release(v3);      return;    }LABEL_10:    -[NOMORawStoreView jumpAction](self, "jumpAction");    goto LABEL_11;  }}

简单阅读可以发现主要的判断逻辑是对xxxx-VIP这个key的,所以考虑直接把objectForKey都监听一下看看是哪个地方引用了。
frida-trace -U -N "com.blink.academy.nomoraw" -m "-[* objectForKey:]",然后改下代码

Javascript
defineHandler({  onEnter(log, args, state) {    let key = new ObjC.Object(args[2]).toString();    log(`-[xxxClass objectForKey:${key}]`);  },  onLeave(log, retval, state) {      log(`-[xxxClass objectForKey:${this.key}] -> ${typeof retval} ${new ObjC.Object(retval)}`);  }});

可以看到只有NSUserDefaults被用了,所以直接hook这个就好了。

Javascript
    const objectForKey = NSUserDefaults['- objectForKey:'];    const objectForKey_old = objectForKey.implementation;    objectForKey.implementation = ObjC.implement(objectForKey, (self,sel,any) => {        const key = new ObjC.Object(any).toString();        if(key === "NOMO-VIP") {            console.warn(`----------\\nO: -[NSUserDefaults objectForKey:${key}] -> 1\\n----------`);            return 1        }        return objectForKey_old(self,sel,any);    })

嗯这样发现跑不了?继续看ida可以看到还有个地方检测了subscription_is_validsubscription_expire_at,这两个一个是bool一个是int,那就得把这几个全都hook一遍了

于是,最终代码:

Javascript
if(ObjC.available) {    console.log("ObjC.available: true");    const fakePrefs = {        "xxxx-VIP": true,        "subscription_is_valid": true,        "subscription_expire_at": "7983066142000",    };    const NSUserDefaults = ObjC.classes.NSUserDefaults;    const boolForKey = NSUserDefaults['- boolForKey:'];    const boolForKey_old = boolForKey.implementation;    boolForKey.implementation = ObjC.implement(boolForKey, (self,sel,any) => {        const key = new ObjC.Object(any).toString();        if(key in fakePrefs) {            console.warn(`----------\\nO: -[NSUserDefaults boolForKey:${key}] -> ${fakePrefs[key] ? "true" : "false"}\\n----------`);            return fakePrefs[key] ? 1 : 0;        }        let retval = boolForKey_old(self,sel,any);        return retval;    })    const stringForKey = NSUserDefaults['- stringForKey:'];    const stringForKey_old = stringForKey.implementation;    stringForKey.implementation = ObjC.implement(stringForKey, (self,sel,any) => {        const key = new ObjC.Object(any).toString();        if(key in fakePrefs) {            console.warn(`----------\\nO: -[NSUserDefaults stringForKey:${key}] -> ${fakePrefs[key]}\\n----------`);            return ObjC.classes.NSString.stringWithString_(fakePrefs[key]);        }        let retval = stringForKey_old(self,sel,any);        return retval;    })    const integerForKey = NSUserDefaults['- integerForKey:'];    const integerForKey_old = integerForKey.implementation;    integerForKey.implementation = ObjC.implement(integerForKey, (self,sel,any) => {        const key = new ObjC.Object(any).toString();        if(key in fakePrefs) {            console.warn(`----------\\nO: -[NSUserDefaults integerForKey:${key}] -> ${fakePrefs[key]}\\n----------`);            let value = parseInt(fakePrefs[key]);            return value        }        let retval = integerForKey_old(self,sel,any);        return retval;    })    const objectForKey = NSUserDefaults['- objectForKey:'];    const objectForKey_old = objectForKey.implementation;    objectForKey.implementation = ObjC.implement(objectForKey, (self,sel,any) => {        const key = new ObjC.Object(any).toString();        if(key === "NOMO-VIP") {            console.warn(`----------\\nO: -[NSUserDefaults objectForKey:${key}] -> 1\\n----------`);            return 1        }        if(key in fakePrefs) {            console.warn(`----------\\nO: -[NSUserDefaults objectForKey:${key}] -> ${fakePrefs[key]}\\n----------`);            return fakePrefs[key];        }        return objectForKey_old(self,sel,any);    })    console.log("Hooked");}

PS: 现在启动的时候必须要进入界面然后点击激活才能用。同时在激活后还会弹窗,所以这个方式显然是不对的。

如果是走dylib注入就没那么多事了,在程序启动时直接校验正确了就不会有后面什么事了(笑

Update 8.5 网上找了下还真有,但是我不贴link, 想要自己去找吧(