<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet type='text/xsl' href='/build/web.xsl'?>

<rsml version="1.0" language="en"
      xmlns="https://kekkan.org/RsML">
  <meta>
    <title>Bypass Apple<apostrophe/>s FairPlay DRM for Penetration
    Tests</title>
    <subtitle>Decrypting Mach-O Bundled in iOS Application
    Archive</subtitle>
    <author>Jing Huang</author>
    <date>
      <year>2025</year>
      <month>--11</month>
      <day>---03</day>
    </date>
    <abstract>This article presents a technical analysis of runtime
    Mach-O decryption techniques for iOS application security
    research, based on a binary dumper implementation in C originally
    developed by Stefan Esser in 2011 and later enhanced by Conrad
    Kramer. This allows us to bypass FairPlay DRM protection during
    penetration tests through runtime memory access when encrypted
    binaries exist in their decrypted state.</abstract>
  </meta>

  <unit role="chapter">
    <heading>Introduction</heading>
    <paragraph>Apple<apostrophe/>s FairPlay DRM system encrypts
    specific parts of the <verbatim>__TEXT</verbatim>
    segment<footnote>Containing <verbatim>__text</verbatim> (the
    compiled machine code), <verbatim>__const</verbatim> (general
    constant data), <verbatim>__cstring</verbatim> (literal string
    constants) and <verbatim>__picsymbol_stub</verbatim>
    (position-independent code stub routines used by
    <verbatim>dyld</verbatim>).</footnote> within Mach-O executable
    files of iOS applications distributed through the App Store. This
    encryption is designed to protect the intellectual property of
    developers and prevent unauthorized modification or distribution
    of applications.</paragraph>
    <paragraph>When an encrypted application launches, iOS
    automatically decrypts the <verbatim>__TEXT</verbatim> segment
    into memory for execution. This means the decrypted segment is
    accessible through runtime introspection.</paragraph>
    <paragraph>There are various ways to inject a dynamic library into
    a running process and thus access its memory, name a
    few:</paragraph>
    <list type="itemize">
      <item><verbatim>DYLD_INSERT_LIBRARIES</verbatim>, instructing
      the dynamic loader to load libraries into a process at launch
      time.</item>
      <item>Task port injection, allocating memory in a target process
      and create a remote thread loading libraries.</item>
      <item>ROP/JOP chain injection, constructing a sequence of
      existing code gadgets.</item>
      <item>Substrate/Substitute hooking, based on runtime hooking
      frameworks to modify function implementations.</item>
      <item>Debugger injection, attaching <verbatim>lldb</verbatim> to
      the target process and execute
      <verbatim>dlopen()</verbatim>.</item>
    </list>
    <paragraph>In this article, we will only cover the
    <verbatim>DYLD_INSERT_LIBRARIES</verbatim> approach, which is the
    simplest and most robust at cost of flexibility. Encrypted
    segments are located using
    <verbatim>LC_ENCRYPTION_INFO</verbatim>.</paragraph>
    <message type="note">Recent iOS hardening prevents non-system
    processes from spawning containerized apps, while entitlement
    workarounds that try to fake a platform binary will break
    framework loading<footnote>Non-platform frameworks are rejected by
    <verbatim>dyld</verbatim> with <quote>mapping process is a
    platform binary, but mapped file is not.</quote></footnote>. To
    inject dynamic libraries into running process, we can use
    MobileSubstrate instead; however the idea is essentially the
    same.</message>
  </unit>

  <unit role="chapter">
    <heading>Implementation</heading>
    <paragraph>So here is the implementation in C, for decrypting
    protected images, with explanations. The full implementation is
    also available on <link
    literal="GitHub">https://github.com/RadioNoiseE/fairplay</link>. Explanation
    will be divided into sections, and only basic knowledge on the C
    programming language is assumed.</paragraph>

    <unit role="section">
      <heading>Header Files</heading>
      <verbatim lang="c" line="11"><![CDATA[
#include <dlfcn.h>
#include <fcntl.h>
#include <mach-o/dyld.h>
#include <mach-o/fat.h>
#include <mach-o/loader.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
      ]]></verbatim>
      <paragraph>Structure containing information of dynamic linking
      is provided by <verbatim>dlfcn.h</verbatim>, and low-level file
      input and output is provided by
      <verbatim>fcntl.h</verbatim>. The three headers under
      <verbatim>mach-o</verbatim> folder provides dynamic linker
      interface, fat (universal) binary related information, and
      Mach-O binary format structures respectively.</paragraph>
    </unit>

    <unit role="section">
      <heading>Helper Function</heading>
      <verbatim lang="c" line="21"><![CDATA[
static inline uint32_t bswap32 (uint32_t value) {
  return ((value & 0xFF000000) >> 24) | ((value & 0x00FF0000) >> 8) |
         ((value & 0x0000FF00) << 8) | ((value & 0x000000FF) << 24);
}
      ]]></verbatim>
      <paragraph>This function performs a byte swap, reversing the
      byte order of a 32-bit integer, which is used for conversion
      between big-endian and little-endian representations. This is
      used in handling fat (universal) binaries when the magic
      constant has value
      <verbatim>FAT_CIGAM</verbatim><footnote><verbatim>CIGAM</verbatim>
      is the reverse of <verbatim>MAGIC</verbatim>, itself indicating
      big-endian representation.</footnote>, that all multi-byte
      values require swapping on little-endian devices.
      </paragraph>
    </unit>

    <unit role="section">
      <heading>Constructor</heading>
      <verbatim lang="c" line="3"><![CDATA[
__attribute__ ((constructor)) static void dump () {
  _dyld_register_func_for_add_image (&queue);
}
      ]]></verbatim>
      <paragraph>The <verbatim>constructor</verbatim> attribute tells
      the compiler that this function will get called right after the
      shared library get loaded, adding a hook to execute the callback
      function <verbatim>queue</verbatim> every time a new Mach-O
      image gets loaded into the memory.</paragraph>
      <paragraph>This will ensure that every encrypted image including
      frameworks that is been used in the target application is being
      processed by function <verbatim>queue</verbatim>, which acts
      exactly like a queue.</paragraph>
    </unit>

    <unit role="section">
      <heading>Queue Function</heading>
      <verbatim lang="c" line="5"><![CDATA[
static void queue (const struct mach_header *mh, intptr_t slide) {
  Dl_info ii;
  dladdr (mh, &ii);
  decrypt (ii.dli_fname, mh);
}
      ]]></verbatim>
      <paragraph>What this function does is using the Mach-O header
      being passed in to get the actual path to that image using
      <verbatim>dlfcn.h</verbatim> provided
      <verbatim>dladdr</verbatim>, then hand it together with the
      header, to the function <verbatim>decrypt</verbatim> which does
      all the heavy job.</paragraph>
    </unit>

    <unit role="section">
      <heading>Decrypter</heading>
      <verbatim lang="c" line="10"><![CDATA[
int      in_fd, out_fd;
long     pos_tmp;
char     buffer[4096], in_path[4096], out_path[4096], *str_tmp;
off_t    off_cid, off_rest, off_read;
uint32_t off_tmp = 0, int_tmp = 0, zero = 0;

struct fat_arch                *fa;
struct fat_header              *fh;
struct load_command            *lc;
struct encryption_info_command *eic;
      ]]></verbatim>
      <paragraph>Receiving the path to the image, and the header, this
      is where the actual decrypting is done. But first we declare the
      variables that we will be using. This function itself receives
      <verbatim>*path</verbatim> and <verbatim>*mh</verbatim>, from
      the queue.</paragraph>
      <verbatim lang="c" line="12"><![CDATA[
switch (mh->magic) {
case MH_MAGIC:
  lc = (struct load_command *) ((unsigned char *) mh +
                                sizeof (struct mach_header));
  break;
case MH_MAGIC_64:
  lc = (struct load_command *) ((unsigned char *) mh +
                                sizeof (struct mach_header_64));
  break;
default:
  _exit (1);
}
      ]]></verbatim>
      <paragraph>Here we find the start of the load command area using
      offset of the memory pointer to header and its size, since load
      command is located right after the header. When the magic number
      in header is unknown, we escape.</paragraph>
      <verbatim lang="c" line="3"><![CDATA[
if (realpath (path, in_path) == NULL)
  strlcpy (in_path, path, sizeof (in_path));
str_tmp = strrchr (in_path, '/');
      ]]></verbatim>
      <paragraph>Stores the base name of the target by chopping
      everything before the last path-separator <verbatim>/</verbatim>
      in <verbatim>str_cmp</verbatim>.</paragraph>
      <verbatim lang="c" line="9"><![CDATA[
for (int i = 0; i < mh->ncmds; i++) {
  if (lc->cmd == LC_ENCRYPTION_INFO || lc->cmd == LC_ENCRYPTION_INFO_64) {
    eic = (struct encryption_info_command *) lc;
    if (eic->cryptid == 0)
      break;
    // to be continued
  }
  lc = (struct load_command *) ((unsigned char *) lc + lc->cmdsize);
}
      ]]></verbatim>
      <paragraph>Starting at the beginning of the load commands, we
      iterate through each of the <verbatim>ncms</verbatim> commands,
      until we reach the encryption info command, then cast it to
      <verbatim>encryption_info_command</verbatim> for further
      processing. It stops early when there is nothing to
      decrypt.</paragraph>
      <verbatim lang="c" line="4"><![CDATA[
off_cid = ((unsigned char *) &eic->cryptid - (unsigned char *) mh);
in_fd   = open (in_path, O_RDONLY);
int_tmp = read (in_fd, (void *) buffer, sizeof (buffer));
fh      = (struct fat_header *) buffer;
      ]]></verbatim>
      <paragraph>We first store the offset of the
      <verbatim>cryptid</verbatim> relative to the image in the memory
      so later we can overwrite it with the value zero, standing for
      not encrypted image. Then we open the target image, and read in
      4kb data (which is much larger than any possible fat header) to
      <quote>reinterpret cast</quote> it to
      <verbatim>fat_header</verbatim>.</paragraph>
      <verbatim lang="c" line="17"><![CDATA[
switch (fh->magic) {
case FAT_CIGAM:
  fa = (struct fat_arch *) (fh + 1);
  for (int i = 0; i < bswap32 (fh->nfat_arch); i++, fa++) {
    if (mh->cputype == bswap32 (fa->cputype) &&
        mh->cpusubtype == bswap32 (fa->cpusubtype)) {
      off_tmp = bswap32 (fa->offset);
      break;
    }
  }
  break;
case MH_MAGIC:
case MH_MAGIC_64:
  break;
default:
  _exit (1);
}
      ]]></verbatim>
      <paragraph>Here we store in <verbatim>off_tmp</verbatim> the
      offset to correct slice when we have a fat image that contains
      multiple slices of images. Note that all values are stored in
      bit-endian and require swapping.</paragraph>
      <verbatim lang="c" line="5"><![CDATA[
strlcpy (out_path, getenv ("HOME"), sizeof (out_path));
strlcat (out_path, "/tmp/", sizeof (out_path));
strlcat (out_path, str_tmp + 1, sizeof (out_path));
strlcat (out_path, ".d", sizeof (out_path));
out_fd = open (out_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
      ]]></verbatim>
      <paragraph>Create the output file in the application container
      root, which makes sandbox happy.</paragraph>
      <verbatim lang="c" line="3"><![CDATA[
int_tmp  = off_tmp + eic->cryptoff;
off_rest = lseek (in_fd, 0, SEEK_END) - int_tmp - eic->cryptsize;
lseek (in_fd, 0, SEEK_SET);
      ]]></verbatim>
      <paragraph>Put the absolute file offset to the start of the
      encrypted region into <verbatim>int_tmp</verbatim>, and the
      number of bytes remained after the encrypted region into
      <verbatim>off_rest</verbatim>. Then we reset the read position
      back to the start of file.</paragraph>
      <verbatim lang="c" line="6"><![CDATA[
while (int_tmp > 0) {
  off_read = int_tmp > sizeof (buffer) ? sizeof (buffer) : int_tmp;
  pos_tmp  = read (in_fd, buffer, off_read);
  pos_tmp  = write (out_fd, buffer, off_read);
  int_tmp -= off_read;
}
      ]]></verbatim>
      <paragraph>Copy the first not encrypted part, that is,
      everything before the encrypted region which offset is
      known.</paragraph>
      <verbatim lang="c" line="2"><![CDATA[
pos_tmp =
    write (out_fd, (unsigned char *) mh + eic->cryptoff, eic->cryptsize);
      ]]></verbatim>
      <paragraph>Then we write the decrypted in-memory segment after
      the header.</paragraph>
      <verbatim lang="c" line="2"><![CDATA[
int_tmp = off_rest;
lseek (in_fd, eic->cryptsize, SEEK_CUR);
      ]]></verbatim>
      <paragraph>Get the iterator variable ready, and set the file
      position pointer to the start of the final not encrypted
      region.</paragraph>
      <verbatim lang="c" line="6"><![CDATA[
while (int_tmp > 0) {
  off_read = int_tmp > sizeof (buffer) ? sizeof (buffer) : int_tmp;
  pos_tmp  = read (in_fd, buffer, off_read);
  pos_tmp  = write (out_fd, buffer, off_read);
  int_tmp -= off_read;
}
      ]]></verbatim>
      <paragraph>Finally, copy the not encrypted remainder to the
      decrypted image.</paragraph>
      <verbatim lang="c" line="6"><![CDATA[
if (off_cid) {
  off_cid += off_tmp;
  if (lseek (out_fd, off_cid, SEEK_SET) != off_cid ||
      write (out_fd, &zero, 4) != 4)
    _exit (1);
}
      ]]></verbatim>
      <paragraph>To make this an unencrypted image, the last step is
      to overwrite the <verbatim>cryptid</verbatim>, indicating that
      this image is no longer encrypted. Afterward, the file
      descriptors should be closed, though modern operating system
      typically handle this.</paragraph>
    </unit>
  </unit>

  <unit role="chapter">
    <heading>Instructions</heading>
    <paragraph>The following command compiles it into a arm64 iOS
    dynamic library, with Xcode installed.</paragraph>
    <verbatim lang="sh" line="4"><![CDATA[
xcrun --sdk iphoneos clang -arch arm64 -dynamiclib -Os               \
  -isysroot `xcrun --sdk iphoneos --show-sdk-path`                   \
  -F`xcrun --sdk iphoneos --show-sdk-path`/System/Library/Frameworks \
  fairplay.c -o fairplay.dylib
    ]]></verbatim>
    <paragraph>This dynamic library should be at least ad-hoc signed,
    using either <verbatim>codesign -fs -</verbatim> or <verbatim>ldid
    -S</verbatim>, then placed in
    <verbatim>/Library/MobileSubstrate/DynamicLibraries/</verbatim>,
    with the following filter<footnote>Filters are implemented as
    <verbatim>.plist</verbatim> that lives beside the
    <verbatim>.dylib</verbatim>, with identical base
    name.</footnote>.</paragraph>
    <verbatim lang="xml" line="10"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">
  <dict>
    <key>Filter</key>
    <dict>
      <key>Bundles</key>
      <array>
        <string>com.YoStarJP.Arknights</string>
      </array>
    </dict>
  </dict>
</plist>
    ]]></verbatim>
    <paragraph>After launching
    <verbatim>com.YoStarJP.Arknights</verbatim>, this library will be
    injected, dumping the decrypted image.</paragraph>
  </unit>
</rsml>
