Skip to content

Instantly share code, notes, and snippets.

@programming086
Last active July 26, 2024 12:15
Show Gist options
  • Save programming086/0b53cde2686bca7767332ed25b4a26ec to your computer and use it in GitHub Desktop.
Save programming086/0b53cde2686bca7767332ed25b4a26ec to your computer and use it in GitHub Desktop.
Reverse engineering macOS/iOS/iPadOS

Reverse Engineering on iOS/iPadOS

Some notes, tools, and techniques for reverse engineering iOS/iPadOS binaries.

Static/Dynamic iOS/iPadOS app inspection

Jailbreak in 2023

CheckRa1n was enough for my devices on iOS 14.x.

Older, stable Jailbreaks like Electra still work. Things to remember:

  • You do NOT require Cydia Impactor to install a fresh copy of the Electra app.
  • The Electra app needs to signed by a code signing key before it works.
  • Create a Code Signing Key for free with XCode.
  • Re-sign the ELectra app with:
# find your "Apple Development" ID
security find-identity -v -p codesigning

# sign Electra app with a free Developer Account
applesign -7 -i ${CODESIGNID} -m embedded.mobileprovision Electra1141-2.0.ipa -o ready.ipa --clone-entitlements

# Deploy it to the device with a different bundle ID
ios-deploy --bundle_id='com.bar.baz.foo' -b ready.ipa

Get decrypted IPA off a Jailbroken device

Decrypting the app binary is essential if you want to find good strings, debug the app or repackage the iPA.

# Get script to decrypt iPA
https://github.com/AloneMonkey/frida-ios-dump

# Attach a jailbroken iPhone and create tunnel over USB
iproxy 2222 22 &

# Ensure Frida is running on iOS device. Then run frida-ios-dump
./dump.py foo.bar.bundleid

# Check AppStore binary is now decrypted ( cryptid 0 decrypted vs cryptid 1 encrypted )  
otool -l Payload/foo.app/foo | grep -i LC_ENCRYPTION -B1 -A4
Load command 12
          cmd LC_ENCRYPTION_INFO_64
      cryptid 0
--

Get App Store iPAs

  • Install Apple's utility Apple Configurator 2 from macOS store
  • Install the target iOS app on the target device
  • Then open Apple Configurator 2 and "sign in" with the same Apple account used on the target device
  • Sign-out and sign-in to refresh the known app list
  • Right click on device and select Add/Apps
  • Select the app you want to copy

At this point Apple Configurator 2 will download a copy of the app to:


~/Library/Group Containers/K36BKF7T3D.group.com.apple.configurator/Caches/Assets/TemporaryItems/MobileApps/

When you hit the "Skip App / Replace / Stop" modal, select nothing. Go to Finder and grab the IPA.

Full instructions.

 Dump KeyChain

Works on a clean device or Jailbroken device:

# Install Objection
pip3 install objection

# repackage app with Frida Gadget
objection --gadget "com.apple.AppStore" explore

# KeyChain dump
ios keychain dump --json output.json

Inspect files inside ipa

# Unzip the IPA file to reveal the Payload folder
unzip myApp.ipa

# big files inside ipa file
find Payload -size +2M

# Files that were mistakingly shipped inside of App Bundle
find . -name '*.json' -or -name '*.txt'

# Check for ReactNative
find . -name main.jsbundle

# Check for Certificates
find . -name '*.crt' -or -name '*.cer' -or -name '*.der'

# Property lists inside Payload folder. Recursive search.
find Payload/ -name '*.plist'

# Provisioning Profiles
find . -name '*.mobileprovision'

# Dynamically linked frameworks
find . -name '*.framework'

# Locally linked javascript
find Payload -name '*.js'

# Search all plist files for a value
find . -name '*.plist' | xargs grep "LSApplicationQueriesSchemes"

# Search all plist files for Device Permissions or App Transport Security
find . -name '*.plist' | xargs grep "NS"

# Search all files using only grep
grep "LSApplicationQueriesSchemes" . -R

# Recursive search all files using grep inside Payload folder
grep "Requires" Payload -R
# foobar.app/Info.plist:    <key>UIRequiresFullScreen</key>
# foobar.app/Info.plist:    <key>LSRequiresIPhoneOS</key>

Inspect sandbox data on Jailbroken device

iOS file structure

# Sandbox. Look here for Cookies, json files, etc
/var/mobile/Containers/Data/Application/[GUID given at install time]/

# Folder of App Bundle that was installed. Executables, frameworks, fonts, CSS, html. NIB files.
/private/var/containers/Bundle/Application/[GUID given at app install]/foo.app

# App executable
/private/var/containers/Bundle/Application/[GUID given at app install]/foo.app/foo

# freshly installed IPA is at the bottom of list
cd /private/var/mobile/Containers/Data/Application/ && ls -lrt  
cd [app guid]/Documents/
cd [app guid]/Library/

# Databases to pull off a device
/private/var/Keychains
TrustStore.sqlite3
keychain-2.db
pinningrules.sqlite3

File sharing

# Extract IPA (whether App Store encrypted or not)
scp -r -P 2222 root@localhost:/var/containers/Bundle/Application/<app GUID>/hitme.app ~/hitme.app

# Different to SSH, the uppercase P for Port with SCP. Order important.
scp -P 2222 root@localhost:/var/root/overflow.c localfilename.c

# from Jailbroken device to local machine
# Caution:no space after the root@localhost: Otherwise you copy the entire filesystem!
scp -P 2222 root@localhost:/private/var/mobile/Containers/Data/Application/<App GUID>/Library/Caches/Snapshots/com.my.app

# from local machine to remote Jailbroken device
scp -P 2222 hello.txt root@localhost:/var/root/

Logs

# physical device
idevicesyslog -u <DeviceID> | myPipedProgram

# Get logs from iOS Simulator 
xcrun simctl spawn booted log stream --level=debug

# Get logs from iOS Simulator by App Name
xcrun simctl spawn booted log stream --predicate 'processImagePath endswith "MyAppName"'

Build information

Check platform

lipo -info libprogressbar.a

Check for build errors

jtool -arch arm64 -L <binary inside app bundle>

Check minimum iOS version & restrict linker flag

jtool -arch arm64 -l <binary inside app bundle>

Check Load Commands

rabin2 -H playground

Sections of the Binary

objdump -macho -section-headers Payload/myApp.app/myApp

iOS app entitlements

codesign -d --entitlements :- Payload/MyApp.app
jtool -arch arm64 --ent <binary inside app bundle>

Simple Permissions check

cat Payload/*/Info.plist | grep -i NS

Device Support

https://gist.github.com/adamawolf/3048717

Check binary was stripped

rabin2 -I -a arm_64 <binary inside app bundle> | grep -E 'stripped|canary'

Check Position Independent Code set

rabin2 -I -a arm_64 <binary inside app bundle> | grep -E 'pic|bits

Check for Bitcode enabled

otool -l libprogressbar.a | grep __LLVM
otool -arch arm64 -l tinyDynamicFramework | grep __LLVM
// Remember this command won't work on a locally built Simulator / iPhone app. Bitcode happens after setting `Archive`

Symbols

nm

nm libprogressbar.a | less

rabin2

rabin2 -s file

radare2

is~FUNC

Strings

Check URLs

strings <binary inside app bundle>  | grep -E 'session|https'
strings <binary inside app bundle>  | grep -E 'pinning'
rabin2 -qz <binary inside app bundle>                                   // in Data Section
rabin2 -qzz <binary inside app bundle>                                  // ALL strings in binary
​
jtool -dA __TEXT.__cstring c_playground
Dumping C-Strings from address 0x100000f7c (Segment: __TEXT.__cstring)..
Address : 0x100000f7c = Offset 0xf7c
0x100000f7c: and we have a winner @ %ld\r
0x100000f98: and that's a wrap folks!\r

Applesign

Applesign is a wrapper around Codesigning tools from Apple.

npm install -g applesign

#### Create provisioning file

First, you want to get hold of an `embedded.mobileprovision` file.  Fear not, this step is simple.

Open `Xcode` and select `File/New/Project/Swift` and call it `foobar`.  Select `build` for Generic (ARM) Device.  Do not select a simulator. This is normally enough.  

You don’t need to `run` the app unless want to automagically add your device’s UUID to the Provisioning Profile.  

Now right click on the `/Product/foobar.app` - in the left hand view pane - and select "show in finder".  If you look inside the folder ( remember `foobar.app` is a folder ) you will find a fresh `embedded.mobileprovision`.  This contains the uniques IDs and an expiry date for the developer profile associated to the app.

#### Read the Provisioning Profile

Ensure your device ID is in the profile and the profile is fresh.

`security cms -D -i embedded.mobileprovision`

#### List all of your Code signing identities

```bash
security find-identity -v -p codesigning
export CODESIGNID=<GUID>

Resign iPA: change bundle ID

applesign -7 -i ${CODESIGNID} --bundleid funky-chicken.resigned

Resign iPA: set app to debuggable with custom provisioning file ( default )

applesign -7 -i ${CODESIGNID} -m embedded.mobileprovision unsigned.ipa -o ready.ipa

Resign the iPA: set output IPA name. Won't be debuggable, if it is a App Store app

applesign -7 -i ${CODESIGNID} myapp.ipa -o resigned.ipa

Speed up repackaging

rm -v unsigned.ipa | rm -v ready.ipa | 7z a unsigned.ipa Payload

// Keep original Bundle ID
applesign -7 -i ${CODESIGNID} -m embedded.mobileprovision unsigned.ipa -o ready.ipa

// Set Bundle ID
// applesign -7 -i ${CODESIGNID} -b yd.com.rusty.repackaged -m embedded.mobileprovision unsigned.ipa -o ready.ipa

ios-deploy -b ready.ipa

Sideload iOS app

ios-deploy -b myapp-resigned.ipa        // defaults to send over wifi
ios-deploy -b -W myapp-resigned.ipa     // uses USB
ios-deploy -B | grep -i funky           // list Bundle IDs

Troubleshoot codesign / iOS Deploy

Title Detail
Missing Device ID Check Provisioning Profile (embedded.mobileprovision) included device's UUID
Check code sign key has not expired Code Signing keys expire. The timeframe for the paid iOS Developer license is one-year. For the free developer signing key, it is much shorter.
Wrong Code-Signing Key check the Code Signing Key was NOT an iPhone Distribution key
identity is no longer valid Error 0xe8008018: The identity used to sign the executable is no longer valid. Make sure that the Apple Development key was selected when running security find-identity -v -p codesigning, I hit this error when I selected a Developer ID Application. I should have selected the ID associated to Apple Development credential.
Code Signing Keys Match check the Code Signing Key used when creating the Provisioning Profile matched the Code Signing Key selected when repackaging and code signing.
XCode check When generating an app - to get hold of embedded.mobileprovision file - remember the Code signing options are different for each Project Target and ProjectTests.
Delete Old Apps check no old app is installed on the phone [ that was signed with a different key ] but has the same Bundle ID.
Entitlements overload You can have a Provisioning Profile (embedded.mobileprovision) that contained more Capabilities than the app you are re-signing.
Clone Entitlements When the app is complicated, with many entitlements, sometimes it is easier just to --clone-entitlements with Applesign.
Wrong Bundle ID When you add specific Entitlments you need a unique Bundle ID. Check whether you need to change Bundle ID when re-signing.
Network settings Settings\General\Profiles and Device Management to trust the Developer Profile and App. This won't happen if you are manually proxying or setting a local DNS server., when installing with iOS-deploy.

If none of the above work open Console.app on macOS. Select your device and set process:mobile_installation_proxy in the Search Bar. This will give details behind the sideloaded IPA error message.

Frida-Server

#### update host machine
pip3 install --upgrade frida

# list available devices
frida-ls-devices

# list processes and bundle ID from USB connected device
frida-ps -Uai

# Force open Calender on USB attached device
frida -U -f com.apple.mobilecal

# open foobar over usb and force start. starts app running
frida -U -f com.apple.mobilecal --no-pause

# get the target app's process ID from USB connected device
frida-ps -U | grep -i myapp

# Run script and quit Frida
frida -U -f foobar --no-pause -q --eval 'console.log("Hi Frida");'

Frida-Gadget

Since Frida version ~12.7, it was quick and simple to Frida on a Jailed device:

# Get Frida-Gadget
<https://github.com/frida/frida/releases>

# Unzip
gunzip frida-gadget-12.xx.xx-ios-universal.dylib.gz

# Create directory for Frida-Gadget
mkdir -p ~/.cache/frida

# Move Frida-Gadget
cp frida-gadget-12.xx.xx-ios-universal.dylib ~/.cache/frida/gadget-ios.dylib

# Invoke Frida-Gadget on Clean device
frida -U -f funky-chicken.debugger-challenge

Frida basics

frida -U "My App"               // Attach Frida to app over USB

Process.id
419

Process.getCurrentThreadId()
3843

var b = "hello frida"

console.log(b)
"hello frida"

c = Memory.allocUtf8String(b)
"0x1067ec510"

Memory.readUtf8String(c)
"hello frida"

console.log(c)
0x1067ec510

console.log(c.readUtf8String(5))
hello

console.log(c.readUtf8String(11))
hello frida

ptrToC = new NativePointer(c);
"0x1067ec510"

console.log(ptrToC)
0x1067ec510

console.log(ptrToC.readCString(8))
hello fr

Memory.readUtf8String(ptrToC)
"hello frida"

Frida - Objective-C

Objective-C's syntax includes the : and @ characters. These characters were not used in the Frida Javascript API.

// Attach to playground process ID
frida -p $(ps -ax | grep -i -m1 playground |awk '{print $1}')

ObjC.available
true

ObjC.classes.UIDevice.currentDevice().systemVersion().toString()
"11.1"

ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()

ObjC.classes.UIWindow.keyWindow().toString()
RET: <WKNavigation: 0x106e165c0>

// shows Static Methods and Instance Methods
ObjC.classes.NSString.$ownMethods

ObjC.classes.NSString.$ivars

var myDate = ObjC.classes.NSDate.alloc().init()

console.log(myDate)
2019-04-19 19:03:46 +0000

myDate.timeIntervalSince1970()
1555700626.021566

myDate.description().toString()
"2019-04-19 19:03:46 +0000"

var a = ObjC.classes.NSUUID.alloc().init()

console.log(a)
4645BFD2-94EE-413D-9CE5-8982D41ED6AE

a.UUIDString()
{
    "handle": "0x7ff3b2403b20"
}
a.UUIDString().toString()
"4645BFD2-94EE-413D-9CE5-8982D41ED6AE"

NSString

var b = ObjC.classes.NSString.stringWithString_("foo");

b.isKindOfClass_(ObjC.classes.NSString)
true

b.isKindOfClass_(ObjC.classes.NSUUID)
false

b.isEqualToString_("foo")
true

b.description().toString()
"foo"

var c = ObjC.classes.NSString.stringWithFormat_('foo ' + 'bar ' + 'lives');

console.log(c)
foo bar lives

NSURL

var url = ObjC.classes.NSURL.URLWithString_('www.foobar.com')

console.log(url)
www.foobar.com

url.isKindOfClass_(ObjC.classes.NSURL)
true

console.log(url.$class)
NSURL

Frida from NSString to NSData back to Hex String

var b = ObjC.classes.NSString.stringWithString_("foo");

var d = ObjC.classes.NSData
d = b.dataUsingEncoding_(1)			//	NSASCIIStringEncoding = 1, NSUTF8StringEncoding = 4,

console.log(d)
<666f6f>					//	This prints the Hex value "666f6f = foo"

d.$className
"NSConcreteMutableData"

var x = d.CKHexString()				//	get you the Byte array as a Hex string

console.log(x)
666f6f

x.$className
"NSTaggedPointerString"

var newStr = ObjC.classes.NSString.stringWithUTF8String_[d.bytes]

Frida with xCode Simulator

// demoapp is the iOS app name
myapp=$(ps x | grep -i -m1 demoapp | awk '{print $1}')
frida-trace -i "getfsent*" -p $myapp

// Connect to process with Frida script
frida --codeshare mrmacete/objc-method-observer -p 85974

Frida find Modules

Process.enumerateModules()      
// this will print all loaded Modules

Process.findModuleByName("libboringssl.dylib")
{
    "base": "0x1861e2000",
    "name": "libboringssl.dylib",
    "path": "/usr/lib/libboringssl.dylib",
    "size": 712704
}

Process.findModuleByAddress("0x1c1c4645c")
{
    "base": "0x1c1c2a000",
    "name": "libsystem_kernel.dylib",
    "path": "/usr/lib/system/libsystem_kernel.dylib",
    "size": 200704
}

Find Address and Module of function name ( Export )

DebugSymbol.fromAddress(Module.findExportByName(null, 'strstr'))
{
    "address": "0x183cb81e8",
    "fileName": "",
    "lineNumber": 0,
    "moduleName": "libsystem_c.dylib",
    "name": "strstr"
}

Find Address of Export and use Address to find Module

Module.findExportByName(null, 'strstr')
"0x183cb81e8"

Module.getExportByName(null,'strstr')
"0x183cb81e8"

Process.findModuleByAddress("0x183cb81e8")
{
    "base": "0x183cb6000",
    "name": "libsystem_c.dylib",
    "path": "/usr/lib/system/libsystem_c.dylib",
    "size": 516096
}

Exports inside a Module

a = Process.findModuleByName("Reachability")
a.enumerateExports()
....
{
    "address": "0x102fab020",
    "name": "ReachabilityVersionString",
    "type": "variable"
},
{
    "address": "0x102fab058",
    "name": "ReachabilityVersionNumber",
    "type": "variable"
}
....
...
..

Frida's --eval flag

Enumerate all Exports, grepping for one function, and quit

frida -U -f funky-chicken.debugger-challenge --no-pause -q --eval 'var x={};Process.enumerateModulesSync().forEach(function(m){x[m.name] = Module.enumerateExportsSync(m.name)});' | grep -B 1 -A 1 task_threads

            "address": "0x1c1c4645c",
            "name": "task_threads",
            "type": "function"

Search for Module, with the Exports' Address

frida -U -f funky-chicken.debugger-challenge --no-pause -q --eval 'var x={};Process.findModuleByAddress("0x1c1c4645c");'

{
    "base": "0x1c1c2a000",
    "name": "libsystem_kernel.dylib",
    "path": "/usr/lib/system/libsystem_kernel.dylib",
    "size": 200704
}

Frida Intercepter

[objc_playground]-> var a = ObjC.classes.NSString.stringWithString_("foo");

[objc_playground]-> a.superclass().toString()
"NSString"

[objc_playground]-> a.class().toString()
"NSTaggedPointerString"

// PASTE THIS CODE INTO THE FRIDA INTERFACE...
Interceptor.attach(ObjC.classes.NSTaggedPointerString['- isEqualToString:'].implementation, {
    onEnter: function (args) {
      var str = new ObjC.Object(ptr(args[2])).toString()
      console.log('[+] Hooked NSTaggedPointerString[- isEqualToString:] ->' , str);
    }
});

// TRIGGER YOUR INTERCEPTOR
[objc_playground_2]-> a.isEqualToString_("foo")
[+] Hooked NSTaggedPointerString[- isEqualToString:] -> foo
1   // TRUE
[objc_playground_2]-> a.isEqualToString_("bar")
[+] Hooked NSTaggedPointerString[- isEqualToString:] -> bar
0   // FALSE

Frida Intercepter - monitor file open

// frida -U -l open.js --no-pause -f com.yd.demoapp

// the below javascript code is the contents of open.js

var targetFunction = Module.findExportByName("libsystem_kernel.dylib", "open");

Interceptor.attach(targetFunction, {
    onEnter: function (args) {
        const path = Memory.readUtf8String(this.context.x0);
        console.log("[+] " + path)
    }
});

Frida Intercepter - monitor Swift Mangled function

try {

    var targetFunctPtr = Module.findExportByName("YDAppModule", "$s9YDAppModule17ConfigC33publicKeyVerifyCertsSayypGvpfi");
    if (targetFunctPtr == null) {
        throw "[*] Target function not found";
    }
    Interceptor.attach(targetFunctPtr, {
        onLeave: function(retval) {
            var array = new ObjC.Object(retval);
            console.log('[*]ObjC Class Type:\t' + array.$className);
            return retval;
        }
    });
    console.log("[*] publicKeyVerifyCertificates called ");
}
catch(err){
    console.log("[!] Exception: " + err.message);
}

Frida-Trace

frida-trace --v                                                                   // check it works
frida-trace --help                                                                // excellent place to read about Flags
frida-trace -f objc_playground                                                    // spawn and NO trace
frida-trace -m "+[NSUUID UUID]" -U "Debug CrackMe"                                // trace ObjC UUID static Class Method
frida-trace -m "*[ComVendorDebugger* *]" -U -f com.robot.demo.app                 // ObjC wildcard trace on Classes
frida-trace -m "*[YDDummyApp.UserProfileMngr *]" -U -f com.robot.demo.app         //  Trace mangled Swift functions
Instrumenting functions...                                                                
           /* TID 0x403 */
  1128 ms  -[YDDummyApp.UserProfileMngr init]
  1130 ms  -[YDDummyApp.UserProfileMngr .cxx_destruct]


frida-trace -i "getaddrinfo" -i "SSLSetSessionOption" -U -f com.robot.demo        // trace C function on iOS
frida-trace -m "*[*URLProtection* *]" -U -f com.robot.demo                        // for https challenge information
frida-trace -m "*[NSURLSession* *didReceiveChallenge*]" -U -f com.robot.demo      // check whether https check delegate used
frida-trace -U -f com.robot.demo.app -I libsystem_c.dylib                         // Trace entire Module.  Bad idea!
frida-trace -p $myapp -I UIKit                                                    // Trace UIKit Module.  Bad idea.
frida-trace -f objc_playground -I CoreFoundation                                  // Trace CoreFoundation Module.  Terrible idea.
frida-trace -I YDRustyKit -U -f com.yd.mobile                                     // Trace my own module.
frida-trace -m "-[NSURLRequest initWithURL:]" -U -f com.robot.demo                // Get app files and APIs
frida-trace -m "-[NSURL initWithString:]" -U -f com.robot.demo                    // find the API endpoints
frida-trace -m "*[NSURL absoluteString]" -U -f com.robot.demo                     // my favorite of these

Edit the Frida-Trace auto-generated, template file.

onEnter: function (log, args, state) {
  log("-[NSURLRequest initWithURL:" + args[2] + "]");
  var str = new ObjC.Object(ptr(args[2])).toString()
  console.log('[*] ' , str);
},

// results
[*] https://secretserver.nl/SignIn

Frida-Trace strcpy()

frida-trace -i "*strcpy" -f hitme aaaa bbbb
Instrumenting functions...                                              
_platform_strcpy: Loaded handler at "/.../__handlers__/libSystem.B.dylib/_platform_strcpy.js"
Started tracing 1 function. Press Ctrl+C to stop.                       

Edit the auto-generated, template Javascript file.

-----------
onEnter: function (log, args, state) {
  // strcpy()  arg1 is the Source. arg0 is the Destination.
  console.log('\n[+] _platform_strcpy()');
  var src_ptr  = args[1].toString()
  var src_string = Memory.readCString(args[1]);
  var src_byte_array = Memory.readByteArray(args[1],4);
  var textDecoder = new TextDecoder("utf-8");
  var decoded = textDecoder.decode(src_byte_array);
  console.log('[+] src_ptr\t-> ' , src_ptr);
  console.log('[+] src_string\t-> ' + src_string);
  console.log('[+] src_byte_array\t-> ' + src_byte_array);
  console.log('[+] src_byte_array size\t-> ' + src_byte_array.byteLength);
  console.log('[+] src_byte_array decoded\t-> ' + decoded);
},

The results:

[+] _platform_strcpy()
[+] src_ptr	->  0x7ffeefbffaa6
[+] src_string	-> aaaa
[+] src_byte_array	-> [object ArrayBuffer]
[+] src_byte_array size	-> 4
[+] decoded	-> aaaa

[+] _platform_strcpy()
[+] src_ptr	->  0x7ffeefbffaab
[+] src_string	-> bbbb
[+] src_byte_array	-> [object ArrayBuffer]
[+] src_byte_array size	-> 4
[+] decoded	-> bbbb

Frida Objective-C Observer

frida-ps -Uai  // get your bundle ID

frida --codeshare mrmacete/objc-method-observer -U -f $BUNDLE_ID

[+] At the Frida prompt...
// Method isJailbroken
observeSomething('*[* isJail*]')

// Observe String compares
observeSomething('*[* isEqualToString*]');    

// A Class ( ObjC ) or Module (Symbol ). The first asterix indicates it can be eith Instance or Class method
observeSomething('*[ABC* *]');                                

// Watch Cookies
observeSomething('-[WKWebsiteDataStore httpCookieStore]');
observeSomething('-[WKWebAllowDenyPolicyListener *]');

// dump the URL to hit
observeSomething('-[WKWebView loadRequest:]');                

// you get all HTML, js, css, etc
observeSomething('-[WKWebView load*]');        

// Read the entire request
observeSomething('-[WKWebView loadHTMLString:baseURL:]') 

// Check for a custom UserAgent
observeSomething('-[WKWebView *Agent]');     
               

Bypass anti-Frida checks

# Rename Frida process
bash -c "exec -a YDFooBar ./frida-server &"

# Set Frida-Server on host to a specific interface and port
frida-server -l 0.0.0.0:19999 &

# Call Frida-server from Host
frida-ps -ai -H 192.168.0.38:19999

# Trace on custom port
frida-trace -m "*[NSURLSession* *didReceiveChallenge*]" -H 192.168.0.38:19999  -f $BUNDLE_ID

Cookies

Find Persisted Cookies

/private/var/mobile/Containers/Data/Application/<app guid, given at install time>/Library/Cookies/Cookies.binarycookies

Extract

scp -P 2222 root@localhost:/private/var/mobile/Containers/Data/Application/<App GUID>/Library/Cookies/Cookies.binarycookies cookies.bin

BinaryCookieReader: Written By Satishb3 (http://www.securitylearn.net
python BinaryCookieReader.py Cookie.Binarycookies-FilePath

Cookie : s_fid=0BBD745EA9BCF67F-366EC6EDEFA2A0E6; domain=.apple.com; path=/; expires=Thu, 14 Dec 2023;
Cookie : s_pathLength=homepage%3D2%2C; domain=.apple.com; path=/; expires=Fri, 14 Dec 2018;
Cookie : s_vi=[CS]v1|2E09D702852E4ACE-60002D37A0008393[CE]; domain=.apple.com; path=/; expires=Sun, 13 Dec 2020;
............
............

Find Cookies in Memory with Frida ( on real device & iOS Simulator )

$) ps -ax | grep -i WebKit.Networking
29163 ??         <longPath>/.../com.apple.WebKit.Networking

$) frida --codeshare mrmacete/objc-method-observer -p 29163

[PID::29163]-> %resume                           
[PID::29163]-> observeSomething('*[* cookiesWithResponseHeaderFields:forURL:]');

Results:

+[NSHTTPCookie cookiesWithResponseHeaderFields:forURL:]
 cookiesWithResponseHeaderFields: {
     "Set-Cookie" = "EuConsent=<removed for brevity>; path=/; expires=Sat, 16 Nov 2019 14:51:01 GMT;";
 } (__NSSingleEntryDictionaryI)
 forURL: https://uk.yahoo.com/?p=us&guccounter=1 (NSURL)

 RET: (
     "<NSHTTPCookie
 	version:0
 	name:EuConsent
 	value:<removed for brevity>
 	expiresDate:'2019-11-16 14:51:01 +0000'
 	created:'2019-11-15 14:51:01 +0000'
 	sessionOnly:FALSE
 	domain:yahoo.com
 	partition:none
 	sameSite:none
 	path:/
 	isSecure:FALSE
  path:"/" isSecure:FALSE>"
 )

Change iOS Version

WARNING: only change the minimum iOS version of a specific app's plist and not for the entire device. Things start to break - like calls into C libraries - when you change the device's read-only iOS version.

ssh onto device
root# cd /System/Library/CoreServices/
root# cat SystemVersion.plist
root# nano SystemVersion.plist
EDIT THE VALUE.  KEEP THE OLD VALUE!

LLVM Instrumentation

https://developer.apple.com/library/archive/qa/qa1964/_index.html
otool -l -arch all my_framework | grep __llvm_prf  
nm -m -arch all my_app | grep gcov

Reverse Engineering on macOS

Some notes, tools, and techniques for reverse engineering macOS binaries.

Table of Contents

Reverse Engineering Tools

Binary Ninja

Ghidra

  • https://ghidra-sre.org/
    • A software reverse engineering (SRE) suite of tools developed by NSA's Research Directorate in support of the Cybersecurity mission

Hex-Rays IDA

  • https://hex-rays.com/
    • https://hex-rays.com/ida-free/
      • This (completely!) free version of IDA offers a privilege opportunity to see IDA in action. This light but powerful tool can quickly analyze the binary code samples and users can save and look closer at the analysis results.

    • https://hex-rays.com/ida-home/
      • IDA Home was introduced thanks to the experience Hex-Rays has been gaining throughout the years to propose hobbyists a solution that combines rapidity, reliability with the levels of quality and responsiveness of support that any professional reverse engineers should expect.

    • https://hex-rays.com/ida-pro/
      • IDA Pro as a disassembler is capable of creating maps of their execution to show the binary instructions that are actually executed by the processor in a symbolic representation (assembly language). Advanced techniques have been implemented into IDA Pro so that it can generate assembly language source code from machine-executable code and make this complex code more human-readable.

        The debugging feature augmented IDA with the dynamic analysis. It supports multiple debugging targets and can handle remote applications. Its cross-platform debugging capability enables instant debugging, easy connection to both local and remote processes and support for 64-bit systems and new connection possibilities.

    • https://www.hex-rays.com/products/ida/debugger/mac/
    • https://hex-rays.com/products/ida/news/8_3/

radare2

Frida / etc

  • https://frida.re/
    • Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers.

    • Scriptable Inject your own scripts into black box processes. Hook any function, spy on crypto APIs or trace private application code, no source code needed. Edit, hit save, and instantly see the results. All without compilation steps or program restarts.

      Portable Works on Windows, macOS, GNU/Linux, iOS, watchOS, tvOS, Android, FreeBSD, and QNX. Install the Node.js bindings from npm, grab a Python package from PyPI, or use Frida through its Swift bindings, .NET bindings, Qt/Qml bindings, Go bindings, or C API. We also have a scalable footprint.

      Free Frida is and will always be free software (free as in freedom). We want to empower the next generation of developer tools, and help other free software developers achieve interoperability through reverse engineering.

      Battle-tested We are proud that NowSecure is using Frida to do fast, deep analysis of mobile apps at scale. Frida has a comprehensive test-suite and has gone through years of rigorous testing across a broad range of use-cases.

  • https://github.com/frida
  • https://github.com/Ch0pin/medusa
    • medusa Binary instrumentation framework based on FRIDA

    • MEDUSA is an extensible and modularized framework that automates processes and techniques practiced during the dynamic analysis of Android and iOS Applications.

  • https://github.com/rsenet/FriList
    • Collection of useful FRIDA Mobile Scripts

    • Observer Security Bypass Static Analysis Specific Software Other

Reversing C++ Binaries

Unsorted

C++ vtables

std::string

  • https://shaharmike.com/cpp/std-string/
    • Exploring std::string

    • Every C++ developer knows that std::string represents a sequence of characters in memory. It manages its own memory, and is very intuitive to use. Today we’ll explore std::string as defined by the C++ Standard, and also by looking at 4 major implementations.

    • One particular optimization found its way to pretty much all implementations: small objects optimization (aka small buffer optimization). Simply put, Small Object Optimization means that the std::string object has a small buffer for small strings, which saves dynamic allocations.

    • Recent GCC versions use a union of buffer (16 bytes) and capacity (8 bytes) to store small strings. Since reserve() is mandatory (more on this later), the internal pointer to the beginning of the string either points to this union or to the dynamically allocated string.

    • clang is by-far the smartest and coolest. While std::string has the size of 24 bytes, it allows strings up to 22 bytes(!!) with no allocation. To achieve this libc++ uses a neat trick: the size of the string is not saved as-is but rather in a special way: if the string is short (< 23 bytes) then it stores size() * 2. This way the least significant bit is always 0. The long form always bitwise-ors the LSB with 1, which in theory might have meant unnecessarily larger allocations, but this implementation always rounds allocations to be of form 16*n - 1 (where n is an integer). By the way, the allocated string is actually of form 16*n, the last character being '\0'

  • https://tastycode.dev/memory-layout-of-std-string/
    • Memory Layout of std::string

    • Discover how std::string is represented in the most popular C++ Standard Libraries, such as MSVC STL, GCC libstdc++, and LLVM libc++.

    • In this post of Tasty C++ series we’ll look inside of std::string, so that you can more effectively work with C++ strings and take advantage and avoid pitfalls of the C++ Standard Library you are using.

    • In C++ Standard Library, std::string is one of the three contiguous containers (together with std::array and std::vector). This means that a sequence of characters is stored in a contiguous area of the memory and an individual character can be efficiently accessed by its index at O(1) time. The C++ Standard imposes more requirements on the complexity of string operations, which we will briefly focus on later in this post.

    • If we are talking about the C++ Standard, it’s important to remember that it doesn’t impose exact implementation of std::string, nor does it specify the exact size of std::string. In practice, as we’ll see, the most popular implementations of the C++ Standard Library allocate 24 or 32 bytes for the same std::string object (excluding the data buffer). On top of that, the memory layout of string objects is also different, which is a result of a tradeoff between optimal memory and CPU utilization, as we’ll also see below.

    • For people just starting to work with strings in C++, std::string is usually associated with three data fields:

      • Buffer – the buffer where string characters are stored, allocated on the heap.
      • Size – the current number of characters in the string.
      • Capacity – the max number of character the buffer can fit, a size of the buffer.

      Talking C++ language, this picture could be expressed as the following class:

      class TastyString {
        char *    m_buffer;     //  string characters
        size_t    m_size;       //  number of characters
        size_t    m_capacity;   //  m_buffer size
      }
      

      This representation takes 24 bytes and is very close to the production code.

  • https://stackoverflow.com/questions/5058676/stdstring-implementation-in-gcc-and-its-memory-overhead-for-short-strings
    • std::string implementation in GCC and its memory overhead for short strings

    • At least with GCC 4.4.5, which is what I have handy on this machine, std::string is a typdef for std::basic_string<char>, and basic_string is defined in /usr/include/c++/4.4.5/bits/basic_string.h. There's a lot of indirection in that file, but what it comes down to is that nonempty std::strings store a pointer to one of these:

      struct _Rep_base
      {
        size_type       _M_length;
        size_type       _M_capacity;
        _Atomic_word        _M_refcount;
      };
      

      Followed in-memory by the actual string data. So std::string is going to have at least three words of overhead for each string, plus any overhead for having a higher capacity than length (probably not, depending on how you construct your strings -- you can check by asking the capacity() method).

      There's also going to be overhead from your memory allocator for doing lots of small allocations; I don't know what GCC uses for C++, but assuming it's similar to the dlmalloc allocator it uses for C, that could be at least two words per allocation, plus some space to align the size to a multiple of at least 8 bytes.

std::vector

Universal (Fat) Binaries

  • https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary
    • Building a Universal macOS Binary

    • Create macOS apps and other executables that run natively on both Apple silicon and Intel-based Mac computers.

    • https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary#Update-the-Architecture-List-of-Custom-Makefiles
      • To create a universal binary for your project, merge the resulting executable files into a single executable binary using the lipo tool.

      • lipo -create -output universal_app x86_app arm_app

    • https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary#Determine-Whether-Your-Binary-Is-Universal
      • Determine Whether Your Binary Is Universal To users, a universal binary looks no different than a binary built for a single architecture. When you build a universal binary, Xcode compiles your source files twice—once for each architecture. After linking the binaries for each architecture, Xcode then merges the architecture-specific binaries into a single executable file using the lipo tool. If you build the source files yourself, you must call lipo as part of your build scripts to merge your architecture-specific binaries into a single universal binary.

        To see the architectures present in a built executable file, run the lipo or file command-line tools. When running either tool, specify the path to the actual executable file, not to any intermediate directories such as the app bundle. For example, the executable file of a macOS app is in the Contents/MacOS/ directory of its bundle. When running the lipo tool, include the -archs parameter to see the architectures.

      • % lipo -archs /System/Applications/Mail.app/Contents/MacOS/Mail
        x86_64 arm64
      • To obtain more information about each architecture, pass the -detailed_info argument to lipo.

    • https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary#Specify-the-Launch-Behavior-of-Your-App
      • Specify the Launch Behavior of Your App For universal binaries, the system prefers to execute the slice that is native to the current platform. On an Intel-based Mac computer, the system always executes the x86_64 slice of the binary. On Apple silicon, the system prefers to execute the arm64 slice when one is present. Users can force the system to run the app under Rosetta translation by enabling the appropriate option from the app’s Get Info window in the Finder.

        If you never want users to run your app under Rosetta translation, add the LSRequiresNativeExecution key to your app’s Info.plist file. When that key is present and set to YES, the system prevents your app from running under translation. In addition, the system removes the Rosetta translation option from your app’s Get Info window. Don’t include this key until you verify that your app runs correctly on both Apple silicon and Intel-based Mac computers.

        If you want to prioritize one architecture, without preventing users from running your app under translation, add the LSArchitecturePriority key to your app’s Info.plist file. The value of this key is an ordered array of strings, which define the priority order for selecting an architecture.

  • https://ss64.com/osx/lipo.html
    • lipo Create or operate on a universal file: convert a universal binary to a single architecture file, or vice versa.

    • lipo produces one output file, and never alters the input file.

    • lipo can: list the architecture types in a universal file; create a single universal file from one or more input files; thin out a single universal file to one specified architecture type; and extract, replace, and/or remove architectures types from the input file to create a single new universal output file.

  • https://github.com/konoui/lipo
    • LIPO This lipo is designed to be compatible with macOS lipo, which is a utility for creating Universal Binary as known as Fat Binary.

Reverse Engineering Audio VST Plugins

Compiler Optimisations

Fast Division / Modulus

  • https://binary.ninja/2023/09/15/3.5-expanded-universe.html#moddiv-deoptimization
    • Mod/Div Deoptimization

    • One of the many things compilers do that can make reverse engineering harder is use a variety of algorithmic optimizations, in particular for modulus and division calculations. Instead of implementing them with the native CPU instructions, they will use shifts and multiplications with magic constants that when operating on a fixed integer size has the same effect as a native division instruction.

      There are several ways to try to recover the original division which is far more intuitive and easer to reason about.

  • https://lemire.me/blog/2020/02/26/fast-divisionless-computation-of-binomial-coefficients/
    • Fast divisionless computation of binomial coefficients

    • We would prefer to avoid divisions entirely. If we assume that k is small, then we can just use the fact that we can always replace a division by a known value with a shift and a multiplication. All that is needed is that we precompute the shift and the multiplier. If there are few possible values of k, we can precompute it with little effort.

    • I provide a full portable implementation complete with some tests. Though I use C, it should work as-is in many other programming languages. It should only take tens of CPU cycles to run. It is going to be much faster than implementations relying on divisions.

    • Another trick that you can put to good use is that the binomial coefficient is symmetric: you can replace k by n–k and get the same value. Thus if you can handle small values of k, you can also handle values of k that are close to n. That is, the above function will also work for n is smaller than 100 and k larger than 90, if you just replace k by n–k.

    • Is that the fastest approach? Not at all. Because n is smaller than 100 and k smaller than 10, we can precompute (memoize) all possible values. You only need an array of 1000 values. It should fit in 8kB without any attempt at compression. And I am sure you can make it fit in 4kB with a little bit of compression effort. Still, there are instances where relying on a precomputed table of several kilobytes and keeping them in cache is inconvenient. In such cases, the divisionless function would be a good choice.

    • Alternatively, if you are happy with approximations, you will find floating-point implementations.

    • https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2020/02/26/binom.c
    • https://github.com/dmikushin/binom/blob/master/include/binom.h
    • https://github.com/bmkessler/fastdiv
    • https://github.com/jmtilli/fastdiv/blob/master/fastdiv.c

Unsorted

  • https://github.com/mroi/apple-internals
    • Apple Internals This repository provides tools and information to help understand and analyze the internals of Apple’s operating system platforms.

    • https://mroi.github.io/apple-internals/
      • Collected knowledge about the internals of Apple’s platforms.

        Sorted by keyword, abbreviation, or codename.

  • https://opensource.apple.com/source/objc4/
  • https://github.com/smx-smx/ezinject
    • Modular binary injection framework, successor of libhooker

    • ezinject is a lightweight and flexible binary injection framework. it can be thought as a lightweight and less featured version of frida.

      It's main and primary goal is to load a user module (.dll, .so, .dylib) inside a target process. These modules can augment ezinject by providing additional features, such as hooks, scripting languages, RPC servers, and so on. They can also be written in multiple languages such as C, C++, Rust, etc... as long as the ABI is respected.

      NOTE: ezinject core is purposedly small, and only implements the "kernel-mode" (debugger) features it needs to run the "user-mode" program, aka the user module.

      It requires no dependencies other than the OS C library (capstone is optionally used only by user modules)

      Porting ezinejct is simple: No assembly code is required other than a few inline assembly statements, and an abstraction layer separates multiple OSes implementations.

  • https://github.com/evelyneee/ellekit
    • ElleKit yet another tweak injector / tweak hooking library for darwin systems

    • What this is

      • A C function hooker that patches memory pages directly
      • An Objective-C function hooker
      • An arm64 assembler
      • A JIT inline assembly implementation for Swift
      • A Substrate and libhooker API reimplementation
  • http://diaphora.re/
    • Diaphora A Free and Open Source Program Diffing Tool

    • Diaphora (διαφορά, Greek for 'difference') version 3.0 is the most advanced program diffing tool (working as an IDA plugin) available as of today (2023). It was released first during SyScan 2015 and has been actively maintained since this year: it has been ported to every single minor version of IDA since 6.8 to 8.3.

      Diaphora supports versions of IDA >= 7.4 because the code only runs in Python 3.X (Python 3.11 was the last version being tested).

    • https://github.com/joxeankoret/diaphora
      • Diaphora, the most advanced Free and Open Source program diffing tool.

      • Diaphora has many of the most common program diffing (bindiffing) features you might expect, like:

        • Diffing assembler.
        • Diffing control flow graphs.
        • Porting symbol names and comments.
        • Adding manual matches.
        • Similarity ratio calculation.
        • Batch automation.
        • Call graph matching calculation.
        • Dozens of heuristics based on graph theory, assembler, bytes, functions' features, etc...

        However, Diaphora has also many features that are unique, not available in any other public tool. The following is a non extensive list of unique features:

        • Ability to port structs, enums, unions and typedefs.
        • Potentially fixed vulnerabilities detection for patch diffing sessions.
        • Support for compilation units (finding and diffing compilation units).
        • Microcode support.
        • Parallel diffing.
        • Pseudo-code based heuristics.
        • Pseudo-code patches generation.
        • Diffing pseudo-codes (with syntax highlighting!).
        • Scripting support (for both the exporting and diffing processes).

See Also

My StackOverflow/etc answers

  • https://stackoverflow.com/questions/46802472/recursively-find-hexadecimal-bytes-in-binary-files/77706906#77706906
    • Recursively searching through binary files for hex strings (with potential wildcards) using radare2's rafind2
    • Crossposted: https://twitter.com/_devalias/status/1738458619958751630
    • SEARCH_DIRECTORY="./path/to/bins"
      GREP_PATTERN='\x5B\x27\x21\x3D\xE9'
      
      # Remove all instances of '\x' from PATTERN for rafind2
      # Eg. Becomes 5B27213DE9
      PATTERN="${GREP_PATTERN//\\x/}"
      
      grep -rl "$GREP_PATTERN" "$SEARCH_DIRECTORY" | while read -r file; do
        echo "$file:"
        rafind2 -x "$PATTERN" "$file"
      done
    • SEARCH_DIRECTORY="./path/to/bins"
      PATTERN='5B27213DE9'
      
      # Using find
      find "$SEARCH_DIRECTORY" -type f -exec sh -c 'output=$(rafind2 -x "$1" "$2"); [ -n "$output" ] && echo "$2:" && echo "$output"' sh "$PATTERN" {} \;
      
      # Using fd
      fd --type f --exec sh -c 'output=$(rafind2 -x "$1" "$2"); [ -n "$output" ] && (echo "$2:"; echo "$output")' sh "$PATTERN" {} "$SEARCH_DIRECTORY"
    • time ./test-grep-and-rafind2
      # ..snip..
      ./test-grep-and-rafind2  7.33s user 0.19s system 99% cpu 7.578 total
      
      ⇒ time ./test-find-and-rafind2
      # ..snip..
      ./test-find-and-rafind2  3.24s user 0.72s system 98% cpu 4.041 total
      
      ⇒ time ./test-fd-and-rafind2
      # ..snip..
      ./test-fd-and-rafind2  3.85s user 1.04s system 488% cpu 1.002 total

My Other Related Deepdive Gist's and Projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment