Reverse engineering Fall Guys on macOS
So recently I wanted to play some more games with my friends, and what better co-op game to play than Fall Guys? Unfortunately, I have a Macbook, and Fall Guys is only available for Windows. What a shame!
PlayCover to the rescue
Naturally, my first instinct would be to run the game through CrossOver, which uses Wine to provide a translation layer for Windows games to run on Mac. However, due to the fact that Fall Guys uses kernel-level anticheat, the game won’t run in Wine. Luckily, there is an alternative: Fall Guys can also be played as an iOS app, so we can use PlayCover to run it natively! Problem solved!
Problem… not solved?
I scoured the internet looking for a decrypted .ipa file of the
latest version, and I stumbled across this
one. After importing into PlayCover, I double-clicked the icon, but
the game would immediately quit. I did manage to find a working IPA of
the previous version of the game, created by u/Gliglue, who
also helpfully posted a script to patch the game which I have reproduced
below.
#!/usr/bin/env bash
set -e
APP="$HOME/Library/Containers/io.playcover.PlayCover/Applications/uk.co.mediatonic.fallguys.app"
FW="$APP/Frameworks/MarketplaceKit.framework"
UNITY="$APP/Frameworks/UnityFramework.framework/UnityFramework"
cp "$UNITY" "$UNITY.bak" 2>/dev/null || true
# 1. Stub MarketplaceKit
mkdir -p "$FW"
cat > /tmp/marketplace_stub.swift << 'SWIFT'
@_silgen_name("$s14MarketplaceKit14AppDistributorO11marketplaceyACSScACmFWC")
public func _marketplace_witness() {}
@_silgen_name("$s14MarketplaceKit14AppDistributorO7currentACvgZTu")
public func _marketplace_current_tu() {}
public enum AppDistributor: String, RawRepresentable, Hashable, CaseIterable {
case marketplace = "marketplace"
public static var current: AppDistributor { .marketplace }
}
SWIFT
swiftc \
-module-name MarketplaceKit \
-emit-library \
-target arm64-apple-macos12.0 \
-Xlinker -install_name \
-Xlinker /System/Library/Frameworks/MarketplaceKit.framework/MarketplaceKit \
-o "$FW/MarketplaceKit" \
/tmp/marketplace_stub.swift
codesign --force --sign - "$FW/MarketplaceKit"
# 2. @rpath
install_name_tool \
-change \
/System/Library/Frameworks/MarketplaceKit.framework/MarketplaceKit \
@rpath/MarketplaceKit.framework/MarketplaceKit \
"$UNITY"
# 3. Patches binaires
python3 -c "
patches = {
0x4e5520: bytes([0x1f, 0x20, 0x03, 0xd5]), # brk -> nop
0x4e54e4: bytes([0xd6, 0x02, 0x1f, 0x2a]), # ldrb w22 -> mov w22, #0
0x4e54d4: bytes([0x1f, 0x20, 0x03, 0xd5]), # bl dispatch_semaphore_wait -> nop
}
with open('$UNITY', 'r+b') as f:
for offset, patch in patches.items():
f.seek(offset)
current = f.read(4)
if current == patch:
print(f'{hex(offset)} already patched')
else:
f.seek(offset)
f.write(patch)
print(f'patched {hex(offset)}: {current.hex()} -> {patch.hex()}')
"
codesign --force --sign - "$UNITY"
echo "Done."All that’s left for me to do is translate this patch to the current version, which is v21.3.0 at the time of writing.
Translating the patch
First, I tried to understand how the original patch worked by looking
at the dissasembly for the already-patched UnityFramework library and
seeking to the offsets using radare2. The modified lines
are prefixed with a ***.
[0x004e54c0]> pdf @ sym.func.004e53c8 ; pdf @ method.class.UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace
; CALL XREF from func.004e518c @ 0x4e51c0(x) ; sym.UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace.allocator..i.Type_...FZ_
; CALL XREF from method.class.UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace @ 0x4e5560(x)
┌ 348: sym.func.004e53c8 (int64_t arg1, int64_t arg2);
│ `- args(x0, x1) vars(10:sp[0x8..0x50])
│ 0x004e53c8 fa67bba9 stp x26, x25, [var_50h]!
│ 0x004e53cc f85f01a9 stp x24, x23, [var_10h]
│ 0x004e53d0 f65702a9 stp x22, x21, [var_20h]
│ 0x004e53d4 f44f03a9 stp x20, x19, [var_10h]
│ 0x004e53d8 fd7b04a9 stp x29, x30, [var_20h]
│ 0x004e53dc fd030191 add x29, sp, 0x40
│ 0x004e53e0 f40301aa mov x20, x1 ; arg2
│ 0x004e53e4 f60300aa mov x22, x0 ; arg1
│ 0x004e53e8 a0ce02f0 adrp x0, 0x5ebc000
│ 0x004e53ec 00003b91 add x0, x0, 0xec0 ; int64_t arg1
│ 0x004e53f0 5a020094 bl sym.func.004e5d58
│ 0x004e53f4 08805ff8 ldur x8, [x0, -8]
│ 0x004e53f8 082140f9 ldr x8, [x8, 0x40]
│ 0x004e53fc e90308aa mov x9, x8
│ 0x004e5400 508d02f0 adrp x16, reloc.Foundation._convertNSErrorToError_...So0C0CSgF_ ; 0x5690000
│ 0x004e5404 104246f9 ldr x16, [x16, 0xc80] ; [0x5690c80:8]=0x801000000000018e ; reloc.__chkstk_darwin
│ 0x004e5408 00023fd6 blr x16
│ 0x004e540c e9030091 mov x9, sp
│ 0x004e5410 083d0091 add x8, x8, 0xf
│ 0x004e5414 08ed7c92 and x8, x8, 0xfffffffffffffff0
│ 0x004e5418 350108cb sub x21, x9, x8
│ 0x004e541c bf020091 mov sp, x21
│ 0x004e5420 000080d2 mov x0, 0
│ 0x004e5424 d1311d95 bl sym.imp.dispatch_semaphore_create
│ 0x004e5428 f80300aa mov x24, x0
│ 0x004e542c a0ce02f0 adrp x0, 0x5ebc000
│ 0x004e5430 00203d91 add x0, x0, 0xf48 ; int64_t arg1
│ 0x004e5434 49020094 bl sym.func.004e5d58
│ 0x004e5438 01048052 mov w1, 0x20
│ 0x004e543c e2008052 mov w2, 7
│ 0x004e5440 51351d95 bl sym.imp.swift_allocObject
│ 0x004e5444 f30300aa mov x19, x0
│ 0x004e5448 f70300aa mov x23, x0
│ 0x004e544c ffce01b8 str wzr, [x23, 0x1c]! ; 0x5ebcf64
│ ; aav.0x05ebcf64
│ 0x004e5450 1f0800f9 str xzr, [x0, 0x10]
│ 0x004e5454 28008052 mov w8, 1
│ 0x004e5458 08600039 strb w8, [x0, 0x18]
│ 0x004e545c 000080d2 mov x0, 0
│ 0x004e5460 e72d1d95 bl fcn.04c30bfc
│ 0x004e5464 e30300aa mov x3, x0
│ 0x004e5468 08805ff8 ldur x8, [x0, -8]
│ 0x004e546c 081d40f9 ldr x8, [x8, 0x38]
│ 0x004e5470 e00315aa mov x0, x21
│ 0x004e5474 21008052 mov w1, 1
│ 0x004e5478 22008052 mov w2, 1
│ 0x004e547c 00013fd6 blr x8
│ 0x004e5480 20a702d0 adrp x0, 0x59cb000
│ 0x004e5484 00e03491 add x0, x0, 0xd38
│ 0x004e5488 01088052 mov w1, 0x40 ; '@'
│ 0x004e548c e2008052 mov w2, 7
│ 0x004e5490 3d351d95 bl sym.imp.swift_allocObject
│ 0x004e5494 f90300aa mov x25, x0
│ 0x004e5498 1f7c01a9 stp xzr, xzr, [x0, 0x10] ; 0x59cbd50
│ ; aav.0x059cbd50
│ 0x004e549c 165002a9 stp x22, x20, [x0, 0x20] ; 0x59cbd58
│ ; reloc....Bi32_WV
│ 0x004e54a0 136003a9 stp x19, x24, [x0, 0x30]
│ 0x004e54a4 e00314aa mov x0, x20
│ 0x004e54a8 61351d95 bl sym.imp.swift_retain
│ 0x004e54ac e00313aa mov x0, x19
│ 0x004e54b0 5f351d95 bl sym.imp.swift_retain
│ 0x004e54b4 bd331d95 bl sym.imp.objc_retain_x24
│ 0x004e54b8 f40300aa mov x20, x0
│ 0x004e54bc a1ce02f0 adrp x1, 0x5ebc000
│ 0x004e54c0 21603d91 add x1, x1, 0xf58 ; int64_t arg2
│ 0x004e54c4 e00315aa mov x0, x21 ; int64_t arg1
│ 0x004e54c8 e20319aa mov x2, x25 ; int64_t arg3
│ 0x004e54cc d2010094 bl sym.func.004e5c14
│ 0x004e54d0 54351d95 bl sym.imp.swift_release ; void swift_release(void *arg0)
*** 0x004e54d4 1f2003d5 nop
│ 0x004e54d8 e00317aa mov x0, x23
│ 0x004e54dc e0331d95 bl sym.imp.os_unfair_lock_lock
│ 0x004e54e0 750a40f9 ldr x21, [x19, 0x10]
*** 0x004e54e4 d6021f2a orr w22, w22, wzr
│ 0x004e54e8 e00317aa mov x0, x23
│ 0x004e54ec df331d95 bl sym.imp.os_unfair_lock_unlock
│ ┌─< 0x004e54f0 96010037 tbnz w22, 0, 0x4e5520
│ │ 0x004e54f4 68331d95 bl sym.imp.objc_release_x20
│ │ 0x004e54f8 e00313aa mov x0, x19 ; void *arg0
│ │ 0x004e54fc 49351d95 bl sym.imp.swift_release ; void swift_release(void *arg0)
│ │ 0x004e5500 e00315aa mov x0, x21
│ │ 0x004e5504 bf0301d1 sub sp, x29, 0x40
│ │ 0x004e5508 fd7b44a9 ldp x29, x30, [var_20h]
│ │ 0x004e550c f44f43a9 ldp x20, x19, [var_10h]
│ │ 0x004e5510 f65742a9 ldp x22, x21, [var_20h]
│ │ 0x004e5514 f85f41a9 ldp x24, x23, [var_10h]
│ │ 0x004e5518 fa67c5a8 ldp x26, x25, [sp], 0x50
│ │ 0x004e551c c0035fd6 ret
│ │ ; CODE XREF from sym.func.004e53c8 @ 0x4e54f0(x)
*** └─> 0x004e5520 1f2003d5 nop
;-- method.UnityFramework.iOSMarketplaceApiImpl.class.fetchMarketplace:
;-- func.004e5524:
┌ 92: method.class.UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace ();
│ afv: vars(1:sp[0x10..0x10])
│ 0x004e5524 f44fbea9 stp x20, x19, [sp, -0x20]!
│ 0x004e5528 fd7b01a9 stp x29, x30, [var_10h]
│ 0x004e552c fd430091 add x29, sp, 0x10
│ 0x004e5530 2a351d95 bl sym.imp.swift_getObjCClassMetadata
│ 0x004e5534 f30300aa mov x19, x0
│ 0x004e5538 20a702d0 adrp x0, 0x59cb000
│ 0x004e553c 00603291 add x0, x0, 0xc98
│ 0x004e5540 01038052 mov w1, 0x18
│ 0x004e5544 e2008052 mov w2, 7
│ 0x004e5548 0f351d95 bl sym.imp.swift_allocObject
│ 0x004e554c f40300aa mov x20, x0
│ 0x004e5550 130800f9 str x19, [x0, 0x10]
│ 0x004e5554 a0ce02f0 adrp x0, 0x5ebc000
│ 0x004e5558 00603c91 add x0, x0, 0xf18 ; mach0_cmd_7 ; int64_t arg1
│ 0x004e555c e10314aa mov x1, x20 ; int64_t arg2
│ 0x004e5560 9affff97 bl sym.func.004e53c8
│ 0x004e5564 f30300aa mov x19, x0
│ 0x004e5568 e00314aa mov x0, x20 ; void *arg0
│ 0x004e556c 2d351d95 bl sym.imp.swift_release ; void swift_release(void *arg0)
│ 0x004e5570 e00313aa mov x0, x19
│ 0x004e5574 fd7b41a9 ldp x29, x30, [var_10h]
│ 0x004e5578 f44fc2a8 ldp x20, x19, [sp], 0x20
└ 0x004e557c c0035fd6 ret
As you can see, the patched function is called by the
fetchMarketplace method, which presumably checks if the app was loaded
from an alternative
app marketplace, since Fall Guys on iOS is only available in the EU
on AltStore. Specifically, the code
uses a semaphore to check the value of some function in a background
thread and triggers a breakpoint if the value is not zero, so we can
just manually set the value to zero and edit out the call and breakpoint
just to be safe. Furthermore, we need to have a stub for the
MarketplaceKit library which the game is linked with, and this is done
in the first part of the script. Now, our strategy is clear: look for
the iosMarketplaceApiImpl.fetchMarketplace function and
find the new offsets! So, I loaded the latest version of the
UnityFramework library into radare2 and looked for the fetchMarketplace
symbol with is~iOSMarketplaceApiImpl:
25 0x00020f40 0x00020f40 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplC16fetchMarketplace17completionHandleryyAA01iC4TypeOc_tFZ UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace.allocator(...FZ)
26 0x00020b3c 0x00020b3c GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplC16fetchMarketplaceAA01iC4TypeOyFZ UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace.allocator..i.Type(...FZ)
27 0x000218e0 0x000218e0 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCACycfC UnityFramework.iOSMarketplaceApiImpl.allocator.(...cfC)
28 0x00021900 0x00021900 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCACycfc UnityFramework.iOSMarketplaceApiImpl.allocator.(...cfc)
29 0x00021930 0x00021930 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCMa UnityFramework.iOSMarketplaceApiImpl.allocator(...a)
30 0x0608c6f8 0x0608c6f8 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCMn UnityFramework.iOSMarketplaceApiImpl.allocator(...n)
31 0x06582a58 0x06582a58 GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCN UnityFramework.iOSMarketplaceApiImpl.allocator
32 0x0002198c 0x0002198c GLOBAL FUNC 0 _$s14UnityFramework21iOSMarketplaceApiImplCfD UnityFramework.iOSMarketplaceApiImpl.allocator
695 0x06582a58 0x06582a58 GLOBAL FUNC 0 _OBJC_CLASS_$__TtC14UnityFramework21iOSMarketplaceApiImpl
734 0x06583fd0 0x06583fd0 GLOBAL FUNC 0 _OBJC_METACLASS_$__TtC14UnityFramework21iOSMarketplaceApiImpl
Then s 0x20b3c to seek to the function that needs
patching:
; CALL XREF from func.00020b3c @ 0x20b70(x)
; CALL XREF from method.class.UnityFramework.iOSMarketplaceApiImpl.fetchMarketplace @ 0x20f20(x)
┌ 368: sym.func.00020d74 (int64_t arg1, int64_t arg2);
│ `- args(x0, x1) vars(10:sp[0x8..0x50])
│ 0x00020d74 fa67bba9 stp x26, x25, [var_50h]!
│ 0x00020d78 f85f01a9 stp x24, x23, [var_10h]
│ 0x00020d7c f65702a9 stp x22, x21, [var_20h]
│ 0x00020d80 f44f03a9 stp x20, x19, [var_30h]
│ 0x00020d84 fd7b04a9 stp x29, x30, [var_40h]
│ 0x00020d88 fd030191 add x29, sp, 0x40
│ 0x00020d8c f40301aa mov x20, x1 ; arg2
│ 0x00020d90 f60300aa mov x22, x0 ; arg1
│ 0x00020d94 002b03f0 adrp x0, aav.0x06583000 ; aav.0x06583000
│ 0x00020d98 00003f91 add x0, x0, 0xfc0 ; int64_t arg1
│ 0x00020d9c c1c402f0 adrp x1, 0x58bb000
│ 0x00020da0 21401591 add x1, x1, 0x550 ; int64_t arg2
│ 0x00020da4 9f000094 bl sym.func.00021020
│ 0x00020da8 08805ff8 ldur x8, [x0, -8]
│ 0x00020dac 092140f9 ldr x9, [x8, 0x40]
│ 0x00020db0 283d0091 add x8, x9, 0xf
│ 0x00020db4 08ed7c92 and x8, x8, 0xfffffffffffffff0
│ 0x00020db8 900c0390 adrp x16, reloc.NDR_record ; 0x61b0000
│ 0x00020dbc 104e40f9 ldr x16, [x16, 0x98] ; [0x61b0098:8]=0x8010000000000013 ; reloc.__chkstk_darwin
│ 0x00020dc0 00023fd6 blr x16
│ 0x00020dc4 e9030091 mov x9, sp
│ 0x00020dc8 350108cb sub x21, x9, x8
│ 0x00020dcc bf020091 mov sp, x21
│ 0x00020dd0 000080d2 mov x0, 0
│ 0x00020dd4 36895b95 bl sym.imp.dispatch_semaphore_create
│ 0x00020dd8 f80300aa mov x24, x0
│ 0x00020ddc 202b0390 adrp x0, 0x6584000
│ 0x00020de0 00600091 add x0, x0, 0x18 ; int64_t arg1
│ 0x00020de4 c1c402f0 adrp x1, 0x58bb000
│ 0x00020de8 21201a91 add x1, x1, 0x688 ; int64_t arg2
│ 0x00020dec 8d000094 bl sym.func.00021020
│ 0x00020df0 01048052 mov w1, 0x20
│ 0x00020df4 e2008052 mov w2, 7
│ 0x00020df8 ba8c5b95 bl sym.imp.swift_allocObject
│ 0x00020dfc f30300aa mov x19, x0
│ 0x00020e00 f70300aa mov x23, x0
│ 0x00020e04 ffce01b8 str wzr, [x23, 0x1c]! ; 0x6584034 ; aav.0x06584034
│ 0x00020e08 1f0800f9 str xzr, [x0, 0x10]
│ 0x00020e0c 28008052 mov w8, 1
│ 0x00020e10 08600039 strb w8, [x0, 0x18] ; 0x6584030 ; aav.0x06584030
│ 0x00020e14 000080d2 mov x0, 0
│ 0x00020e18 4d855b95 bl fcn.0570234c
│ 0x00020e1c e30300aa mov x3, x0
│ 0x00020e20 08805ff8 ldur x8, [x0, -8]
│ 0x00020e24 081d40f9 ldr x8, [x8, 0x38]
│ 0x00020e28 e00315aa mov x0, x21
│ 0x00020e2c 21008052 mov w1, 1
│ 0x00020e30 22008052 mov w2, 1
│ 0x00020e34 00013fd6 blr x8
│ 0x00020e38 800c03d0 adrp x0, aav.0x061b2000 ; aav.0x061b2000
│ 0x00020e3c 00001f91 add x0, x0, 0x7c0 ; 0x61b27c0 ; aav.0x061b27c0
│ 0x00020e40 01088052 mov w1, 0x40 ; '@'
│ 0x00020e44 e2008052 mov w2, 7
│ 0x00020e48 a68c5b95 bl sym.imp.swift_allocObject
│ 0x00020e4c f90300aa mov x25, x0
│ 0x00020e50 1f7c01a9 stp xzr, xzr, [x0, 0x10] ; 0x61b27d8 ; "` \x02"
│ 0x00020e54 165002a9 stp x22, x20, [x0, 0x20]
│ 0x00020e58 136003a9 stp x19, x24, [x0, 0x30]
│ 0x00020e5c e00314aa mov x0, x20
│ 0x00020e60 ca8c5b95 bl sym.imp.swift_retain
│ 0x00020e64 e00313aa mov x0, x19
│ 0x00020e68 c88c5b95 bl sym.imp.swift_retain
│ 0x00020e6c 298b5b95 bl sym.imp.objc_retain_x24
│ 0x00020e70 f40300aa mov x20, x0
│ 0x00020e74 c3c402f0 adrp x3, 0x58bb000 ; DATA XREF from sym.func.009d96e8 @ 0x9d9b04(r)
│ 0x00020e78 63601a91 add x3, x3, 0x698 ; int64_t arg4
│ 0x00020e7c 000080d2 mov x0, 0 ; int64_t arg1
│ 0x00020e80 010080d2 mov x1, 0 ; int64_t arg2
│ 0x00020e84 e20315aa mov x2, x21 ; int64_t arg3
│ 0x00020e88 e40319aa mov x4, x25 ; int64_t arg5
│ 0x00020e8c bc010094 bl sym.func.0002157c
│ 0x00020e90 bb8c5b95 bl sym.imp.swift_release ; void swift_release(void *arg0)
│ 0x00020e94 3d855b95 bl fcn.05702388
│ 0x00020e98 e00317aa mov x0, x23
│ 0x00020e9c 478b5b95 bl sym.imp.os_unfair_lock_lock
│ 0x00020ea0 750a40f9 ldr x21, [x19, 0x10]
│ 0x00020ea4 76624039 ldrb w22, [x19, 0x18] ; [0x6584030:8]=0 ; aav.0x06584030
│ 0x00020ea8 e00317aa mov x0, x23
│ 0x00020eac 468b5b95 bl sym.imp.os_unfair_lock_unlock
│ ┌─< 0x00020eb0 96010037 tbnz w22, 0, 0x20ee0
│ │ 0x00020eb4 d28a5b95 bl sym.imp.objc_release_x20
│ │ 0x00020eb8 e00313aa mov x0, x19 ; void *arg0
│ │ 0x00020ebc b08c5b95 bl sym.imp.swift_release ; void swift_release(void *arg0)
│ │ 0x00020ec0 e00315aa mov x0, x21
│ │ 0x00020ec4 bf0301d1 sub sp, x29, 0x40
│ │ 0x00020ec8 fd7b44a9 ldp x29, x30, [var_40h]
│ │ 0x00020ecc f44f43a9 ldp x20, x19, [var_30h]
│ │ 0x00020ed0 f65742a9 ldp x22, x21, [var_20h]
│ │ 0x00020ed4 f85f41a9 ldp x24, x23, [var_10h]
│ │ 0x00020ed8 fa67c5a8 ldp x26, x25, [sp], 0x50
│ │ 0x00020edc c0035fd6 ret
│ │ ; CODE XREF from sym.func.00020d74 @ 0x20eb0(x)
└ └─> 0x00020ee0 200020d4 brk 1
The structure is basically identical, with a helper function to
coordinate the thread check at 0x20e94, the loading of the
verification result to register w22 at
0x20ea4, and the breakpoint at 0x20ee0. So we
can replace those addresses in the script and run it. It works!
For those who are curious, the script is available on GitHub.