├── fairfree.py
├── LICENSE
├── README.md
└── fairfree.js
/fairfree.py:
--------------------------------------------------------------------------------
1 | import frida
2 | import sys
3 |
4 | jscode = """
5 |
6 | """
7 |
8 | with open('fairfree.js') as f:
9 | jscode = f.read()
10 |
11 | def printMessage(message,data):
12 | if message['type'] == 'send':
13 | print(' {0}'.format(message['payload']))
14 | else:
15 | print(message)
16 |
17 | process = frida.get_usb_device().attach('fairplayd.H2')
18 | script = process.create_script(jscode)
19 | script.on('message',printMessage)
20 | script.load()
21 | sys.stdin.read()
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 RootHide
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FairFree
2 |
3 | jailbreak any ipa and run on apple silicon M1/M2 macOS without decrypted.
4 |
5 |
6 | ## Initial thought
7 |
8 | when Apple released an ARM-based Mac and announced that it could allow iOS apps, everyone was excited. but soon after, apple prevented users from installing IPA at will through macos updates. and only a small number of iOS apps are available on macos appstore.
9 |
10 | so everyone began to look for other methods, such as decrypting all mach-o in the ipa and repackaging it in jailbroken iOS. however, this method has many limitations, such as some apps prevent you from decrypting, and some apps will detect whether they are decrypted when running, even checks whether its own signature is the stock version.
11 |
12 | and running decrypted ipa in macos usually requires disabling SIP and re-signing the ipa with a valid certificate, which undoubtedly adds a lot of trouble. many people also use PlayCover to run decrypted apps without re-signing the ipa, but it is convert ios app to catalysts app, some apps may not run properly.
13 |
14 |
15 |
16 | ## Investigate
17 |
18 | we started thinking about the way apple implements this restriction. is it in the code of the macOS system itself or in the ipa package? after some investigation we came to the following facts:
19 |
20 | 1: the originally released macos11.0 system can still install any ipa without restrictions.
21 |
22 | 2: some ipa downloaded in macos 11.0 can still be installed in the new macos, even if the latest version of ipa is restricted.
23 |
24 | so the conclusion is that apple implemented the flags and restrictions in both the new version of macos and the new version of ipa.
25 |
26 | ## Start journey
27 |
28 | since we didn't have a device on hand that could run macos11.0, we first started analyzing the restrictions and flags in the ipa. after some testing and analysis, we quickly found some useful clues, the installation log showed that the restriction flags in the ipa seemed From the SINF file:
29 | ```
30 | 19:25:24.665931+0800 appinstalld 0x16d5c3000 -[MIExecutableBundle validateSinfWithError:]: 1487:
31 | The SINF provided for the bundle at /private/var/containers/Temp/Bundle/Application/8EC2AA9C-9688-49E0-B9B8-C4E4FE3DD987/maps.app is not valid on this platform.
32 | ```
33 |
34 | it is easy to see some interesting things when looking at the SC_Info/*.sinf file: "mode". we compared the two ipas and found that their modes are indeed different.
35 |
36 |
37 |
38 |
39 | we tried to modify the mode value, but when installing, it prompted that the ipa was damaged. then we replaced the sinf file of one ipa with another ipa and found that the restricted ipa could indeed be installed, but it could not be run, we guessed that the sinf file contained some key information of mach-o in for the runtime decryption.
40 | ```
41 |
19:35:14.863265+0800 kernel proc 53276: set_code_unprotect() error 7 for file "maps"
42 | ```
43 |
44 | the only feasible way is to find the correct way to modify the mode value. after some testing and analysis, we found that the second half of the sinf file seems to contain a piece of encrypted data called priv.
45 |
46 |
47 |
48 |
49 | ## Into the hell
50 |
51 | after further analysis and testing, we finally found the protagonist: FairPlay
52 |
53 | there is very little information about FairPlay on the Internet. we can only know that it is a system used by apple to protect ipa from unauthorized installation and use. it includes the user-mode fairplayd daemon and the kernel iokit extension.
54 |
55 | when we started analyzing the fairplayd binary, we realized that it was an ollvm hell, all functions and instructions highly obfuscated, no any symbolic information, no logs, even no any useful strings. fortunately we were donated an advanced tool to analyze obfuscated code.
56 |
57 | 
58 |
59 | 
60 |
61 |
62 | ## Death and reborn
63 |
64 | after a whole month of analysis and debugging, we finally successfully decrypted the priv field and could freely modify the sinf file.
65 |
66 |
67 |
68 | the priv field is almost the encrypted backup of the first half of sinf. so we only have to modify the plaintext mode and the encrypted mode at the same time to install and run any iOS app on macos without restrictions.
69 |
70 | the decryption function seemed to be a variant of AES128, with all cryptographic loops fully expanded, and had almost 8000 local variables after obfuscated. so we wrote a script to hack it on specific device and ios version.
71 |
72 | ## Enjoy it
73 |
74 | how to use:
75 |
76 | 1. device iphone X & ios13.5.1
77 | 2. jailbreak & install frida-server
78 | 3. login apple id in appstore & install the app
79 | 4. ```chmod -R 777 /path/to/*.app/SC_Info/``` on ios
80 | 5. run ```python3 fairfree.py``` on computer
81 | 6. launch the app in home screen
82 | 7. check the "mode" in SC_Info/*.sinf
83 | 8. zip the *.app to a ipa file
84 | 9. copy ipa file to macos and install
85 | 10. login the apple id in macos appstore
86 | 11. launch the app in macos
87 |
88 | ## Does it safe?
89 |
90 | Yes, we only modify sinf to re-authorize the ipa, and the sandbox will prevent the app from accessing the sinf file (which contains the private information of the appleid), so the app cannot find out that it has been modified.
91 |
92 |
--------------------------------------------------------------------------------
/fairfree.js:
--------------------------------------------------------------------------------
1 |
2 | const cm = new CModule(`
3 | #include
4 |
5 | unsigned int calcDecryptDataLen(unsigned long X0)
6 | {
7 | unsigned int v0 = (X0 ^ 0x2CFCF86E);
8 | unsigned int v1 = 1514590667 * v0;
9 |
10 | unsigned int v12 = *(unsigned int *)(X0+0x28) + v1;
11 |
12 | unsigned int v770 = v12 - 0x44A75F25;
13 |
14 | unsigned int v24 = (v770 - ((2 * v770 + 274998170) & 0x66F8C006) - 0x44518E30) ^ 0xB37C6003;
15 |
16 | return v24;
17 | }
18 |
19 |
20 | unsigned int calcEncryptDataLen(unsigned long a1)
21 | {
22 | unsigned int v2 = 2101767179 * (a1 ^ 0x4A16AFDC);
23 |
24 | unsigned long v5 = (unsigned int)(*(unsigned int *)(a1 + 0x28) - v2);
25 | unsigned long v7 = v5 - 1275503213 - ((2 * (v5 - 1275503213) - 441333462) & 0xB165B874) + 1267450063;
26 | unsigned int v13 = (v7 ^ 0xE74FB145) + (2 * v7 & 0x7FFADAFE ^ 0x31609874) + 2065611771;
27 | unsigned int size = v13 - 0x3B1C297A;
28 | return size;
29 | }
30 |
31 |
32 | typedef unsigned long uint64_t;
33 | typedef unsigned int uint32_t;
34 | typedef unsigned int bool;
35 |
36 | void signEncryptArg(uint64_t pArg, int datalen)
37 | {
38 | // int datalen = 0xD0;
39 | // uint64_t pArg = 0x16FA31798;
40 |
41 | uint64_t X13 = pArg;
42 | uint32_t W23 = 0x6B9A323B + datalen;
43 |
44 | uint32_t W13 = (X13 ^ 0x4A16AFDC) * 0x7D466C0B;
45 | uint32_t W11 = W13 + W23 + 0xED938B9D;
46 |
47 | printf("\n%p %X %X\n", X13, W13, W11); //DF239AEE 38515994
48 |
49 | *(uint32_t*)(pArg + 0x28) = W11;
50 |
51 | {
52 | bool flag=1; //10025DF3C CMP W8, W9
53 | uint32_t __W13 = 5; //10025DF40 LDR W13, [SP,#0x137C]
54 |
55 | uint32_t W9 = !flag;
56 | uint32_t W10 = flag;
57 |
58 | //uint32_t
59 | W9 = W9*0x6195 + __W13;
60 |
61 | uint32_t W22 = W10*0x6198 + W9;
62 |
63 | uint32_t W12 = W22 + 0xFFFF9E65;
64 |
65 | //uint32_t
66 | W9 = W12 ^ W13;
67 |
68 | printf("\n\n%X %X %X\n\n", W22, W12, W9);
69 |
70 | *(uint32_t*)(pArg + 0x2C) = W9;
71 | }
72 | }
73 |
74 | `);
75 |
76 | // console.log(JSON.stringify(cm));
77 |
78 | const calcDecryptDataLen = new NativeFunction(cm.calcDecryptDataLen, 'int', ["pointer"]);
79 | const calcEncryptDataLen = new NativeFunction(cm.calcEncryptDataLen, 'int', ["pointer"]);
80 | const signEncryptArg = new NativeFunction(cm.signEncryptArg, 'void', ["pointer","int"]);
81 |
82 |
83 | var base_addr = Module.findBaseAddress("fairplayd.H2");
84 | send("base_addr addr:" + base_addr);
85 |
86 | global.lastsinf = "";
87 |
88 | Interceptor.attach(base_addr.add(0x021F3E4), {
89 | onEnter: function(args) {
90 |
91 | let key = args[0].add(0x10).readPointer();
92 | let iv = args[0].add(0x30).readPointer();
93 | let priv = args[0].add(0x20).readPointer();
94 | let outbuf = args[0].add(0x8).readPointer();
95 |
96 | let len_enc = args[0].add(0x28).readUInt();
97 |
98 | let datalen = calcDecryptDataLen(args[0]);
99 | console.log("\ndecode***********", args[0], "len="+datalen.toString(16), "in="+priv, "out="+outbuf);
100 |
101 | send("\ncallstack:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
102 | send("return address: " + this.returnAddress.sub(base_addr));
103 |
104 | console.log("\n----key:\n", hexdump(key,{offset: 0,length: 16,header:false,ansi:false}),
105 | "\n----iv:\n", hexdump(iv,{offset: 0,length: 16,header:false,ansi:false}),
106 | "\n----data:\n", hexdump(priv,{offset: 0,length: datalen})
107 | );
108 |
109 | this.datalen = datalen;
110 | this.outbuf = outbuf;
111 | this.key = key;
112 | this.iv = iv;
113 |
114 | //if(datalen==0x1B0) {
115 | // for(let i=0; i<16; i++) key.add(i).writeU8(0);
116 | // for(let i=0; i<16; i++) iv.add(i).writeU8(0);
117 | // for(let i=0; i<0x1B0; i++) priv.add(i).writeU8(0);
118 | //}
119 |
120 | },
121 | onLeave: function(retval){
122 | //retval.replace(0);
123 |
124 | for(let i=0; i