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.