Using __got and __la_symbol_ptr for iOS Hacking / Tweak Development

a week ago 35

Using __got and __la_symbol_ptr for iOS Hacking / Tweak Development iDevice Central GeoSn0wI’ve been in the iOS hacking scene for quite some time now, and I’ve seen the iOS hacking / jailbreak community move from tweak injector to tweak injector, being it from Cydia Substrate (remember that?) to Substitute, then libhooker, ellekit, and whatever else is there these days. NOTE: This article IS NOT AI generated, it was written completely by me, … Using __got and __la_symbol_ptr for iOS Hacking / Tweak DevelopmentRead more The post Using __got and __la_symbol_ptr for iOS Hacking / Tweak Development first appeared on iDevice Central.

Using __got and __la_symbol_ptr for iOS Hacking / Tweak Development iDevice Central GeoSn0w

I’ve been in the iOS hacking scene for quite some time now, and I’ve seen the iOS hacking / jailbreak community move from tweak injector to tweak injector, being it from Cydia Substrate (remember that?) to Substitute, then libhooker, ellekit, and whatever else is there these days.

Of course there’s also a plethora of tools like Dobby, FishHook (by none other than Facebook, for some reason), and so much more, all designed to intercept and redirect / patch functions inside Mach-O binaries, like iOS Apps.

If there is something most of these have in common is that universally at some point they will mess with the __la_symbol_ptr section, or __DATA.__la_symbol_ptr as it is known on iOS.

What are GOT and __la_symbol_ptr on iOS?

First of all, you may already know about GOT and PLT from the ELF format used on Android and Linux. While the mechanism underneath is similar on iOS, the implementation is different.

Compared to Linux or Android, on iOS there is no named PLT (Procedure Linkage Table) section where the global offsets table (GOT) resides.

Instead, on iOS the GOT table resides in __DATA_CONST.__got and the PLT on iOS would be __TEXT.__stubs, where TEXT is the code segment, marked W^X (write XOR execute) so trying to write there trips CS_ENFORCEMENT.

What would be .got.plt on Linux or Android, maps to __DATA.__la_symbol_ptr on iOS.

Now that we have an understanding of how things map from Linux to iOS, it’s important to define what the GOT does.

Let’s say you have a C program that calls printf() from the libc (C Standard Library). Now you can technically include the whole implementation inside your C app and call it locally, but there is rarely a good reason you should do that.

What normally ends up happening is that you just include stdio.h and call printf() wherever you need it.

You can use otool to see exactly the segment and section names for a Mach-O like this:

You can use otool to see exactly the segment and section names for a Mach-O

This is all short, sweet and complete, but how does your app know where the printf() implementation is? You added the stdio.h header containing the function prototype, but the actual implementation is way outside of your binary.

This is where GOT comes into play. By using a trampoline for each of these functions that were lazy linked, you tell the binder (dyld_stub_binder on iOS, _dl_runtime_resolve on Linux): Hey! I need printf() which is supposed to be outside my binary.

The dyld_stub_binder then fetches the real address of printf() or any other external function you’ve called from the libsystem_c.dylib which Apple conveniently hides under the umbrella of libSystem.B.dylib.

Once the binder finds the address of printf(), it copies the address over to the __la_symbol_ptr table inside our binary. This only happens once per function.

Next time our binary calls printf() it will still hit the stub, but because this function was already mapped in by the binder, it can see the address inside the __la_symbol_ptr and call it directly.

Wait so why __DATA.__la_symbol_ptr and not __DATA_CONST.__got?

Whether GOT or la_symbol_ptr get patched is important because depending on when the patching happens, the binder will use one or the other.

The distinction comes from WHEN exactly is the reference resolved. The __DATA_CONST.__got is reserved for non-lazy bindings. These references are resolved by the binder right at the beginning of your app’s execution, before the main() function even has a chance to run.

You will see here stuff like C++ Vtable pointers, the ___stack_chk_guard stack cookies, Objective-C class references, etc. This MUST be valid before any code runs otherwise you die.

Then you have the __DATA.__la_symbol_ptr which is for lazy-linked references. Stuff coming from the standard C Library, like printf() would be populated here when they are first needed during the app runtime.

IMPORTANT: Notice how the GOT is inside __DATA_CONST which after dyld is done with it will become fully READ ONLY, while __la_symbol_ptr is in __DATA which remains writable because it gets patched by the binder on-demand.

The biggest difference is that if you try to write to the GOT table after the app initialized, maybe using a DYLIB hack loaded via Sideloadly, you will crash the app, while writing to __la_symbol_ptr would mostly go unnoticed by mem prot because it’s supposed to be writable.

So how does tweak injection on iOS work?

Whether loaded by a tweak injector via DYLD_INSERT_LIBRARIES on a jailbroken iOS, or with LC_LOAD_DYLIB via a sideloading tool like Sideloadly, an iOS tweak is usually just a DYLIB (Dynamic Link Library) which gets mapped into the process memory.

Once loaded, the tweak’s constructor will run before the main function does so it gets to patch whatever it needs.

The problem is, for GOT stuff it’s already too late. By the time our tweak dylib runs, DYLD has already finished mapping stuff to GOT and switched it to Read Only, so hooking libraries like FishHook use mprotect internally to temporarily switch the memory pages back to PROT_READ | PROT_WRITE so they can apply the patches.

mprotect(pageAlignedAddr, pageSize, PROT_READ | PROT_WRITE);
*(void**)gotEntry = hook;
mprotect(pageAlignedAddr, pageSize, PROT_READ);

As you can see, once the hook is placed the hooking lib will call mprotect again to restore the normal permissions for those memory pages.

But this method would only work well on jailbroken devices since many iOS security features have been neutered. On a sideloaded DYLIB mprotect calls to __DATA_CONST will likely kill the app so __DATA.__la_symbol_ptr hooks are prefered.

Using otool we can actually see the stubs section of a Mach-O binary such as Roblox in action. We will run:

otool -v -s __TEXT __stubs /Users/geosn0w/Desktop/Payload/Roblox.app/Roblox
/Users/geosn0w/Desktop/Payload/Roblox.app/Roblox:
Contents of (__TEXT,__stubs) section
000000010035b228	adrp	x16, 397 ; 0x1004e8000
000000010035b22c	ldr	x16, [x16]
000000010035b230	br	x16
000000010035b234	adrp	x16, 397 ; 0x1004e8000
000000010035b238	ldr	x16, [x16, #0x8]
000000010035b23c	br	x16
000000010035b240	adrp	x16, 397 ; 0x1004e8000
000000010035b244	ldr	x16, [x16, #0x10]
000000010035b248	br	x16
000000010035b24c	adrp	x16, 397 ; 0x1004e8000
000000010035b250	ldr	x16, [x16, #0x18]
000000010035b254	br	x16
000000010035b258	adrp	x16, 397 ; 0x1004e8000
000000010035b25c	ldr	x16, [x16, #0x20]
000000010035b260	br	x16
000000010035b264	adrp	x16, 397 ; 0x1004e8000
000000010035b268	ldr	x16, [x16, #0x28]
000000010035b26c	br	x16
000000010035b270	adrp	x16, 397 ; 0x1004e8000
000000010035b274	ldr	x16, [x16, #0x30]
000000010035b278	br	x16
000000010035b27c	adrp	x16, 397 ; 0x1004e8000
000000010035b280	ldr	x16, [x16, #0x38]
000000010035b284	br	x16
000000010035b288	adrp	x16, 397 ; 0x1004e8000
000000010035b28c	ldr	x16, [x16, #0x40]
000000010035b290	br	x16
000000010035b294	adrp	x16, 397 ; 0x1004e8000
000000010035b298	ldr	x16, [x16, #0x48]
000000010035b29c	br	x16
000000010035b2a0	adrp	x16, 397 ; 0x1004e8000
000000010035b2a4	ldr	x16, [x16, #0x50]
000000010035b2a8	br	x16
000000010035b2ac	adrp	x16, 397 ; 0x1004e8000
000000010035b2b0	ldr	x16, [x16, #0x58]
000000010035b2b4	br	x16
000000010035b2b8	adrp	x16, 397 ; 0x1004e8000
000000010035b2bc	ldr	x16, [x16, #0x60]
000000010035b2c0	br	x16
000000010035b2c4	adrp	x16, 397 ; 0x1004e8000
000000010035b2c8	ldr	x16, [x16, #0x68]
000000010035b2cc	br	x16
000000010035b2d0	adrp	x16, 397 ; 0x1004e8000
000000010035b2d4	ldr	x16, [x16, #0x70]
000000010035b2d8	br	x16
000000010035b2dc	adrp	x16, 397 ; 0x1004e8000
000000010035b2e0	ldr	x16, [x16, #0x78]
000000010035b2e4	br	x16
.... it goes on so I will keep it short because this article gets long af...

And the result is quite telling. You can see that in Roblox there is hundreds of references in the stubs section, but all of them follow the same mechanism in assembly:

adrp x16, 397              ; load page address of __la_symbol_ptr into x16 register
ldr  x16, [x16, #offset]   ; load the actual pointer from that page
br   x16                   ; jump through it

Every single imported function is just this same 3 instructions block, offset by 8 bytes per entry (#0x0, #0x8, #0x10…). The offset indexes into __la_symbol_ptr like an array.

The target page 0x1004e8000 is where __la_symbol_ptr lives in Roblox’s binary. Every stub points into that same page, just at increasing offsets.

Using otool we can actually see the stubs section of a Mach-O binary such as Roblox

So if you want to hook whatever function lives at stub 0x10035b234, you don’t touch the __TEXT at all (unless you wanna die a swift death by mem prot violation). You go to 0x1004e8000+0x8 in the __DATA segment and overwrite that pointer. The stub itself never changes, it just uses your address instead.

Dealing with PAC (Pointer Authentication Codes) on A12+

On A12+ (arm64e devices) like iPhone XS and later, Apple introduced PAC or Pointer Authentication Codes, which annoy jailbreak devs to no end…

Addresses stored in __got and the __la_symbol_ptr are now signed with a key which is embedded into the hardware.

When the CPU loads a signed address and branches through it, it will check the signature. So simply swapping it with an unsigned address into a GOT entry causes the CPU to fault and your app to kick the bucket.

To get around this, most newer hooking libraries sign the function pointer before writing it into the entry.

The signing has to use the correct key and the entry’s own address as the context (what Apple calls address diversity), otherwise the CPU will still reject it.

This is the main reason older tools like Substrate broke on A12+ and why the jailbreak scene had to move to libhooker and eventually ElleKit which are supporting PAC re-signing.

// on arm64e devices you can't just do this anymore:
*(void**)slotEntry = theHook;  // CPU will fault, pointer isn't signed, you died...

// you need to strip the old signature first, then sign yours
void* stripped = ptrauth_strip(*(void**)slotEntry, ptrauth_key_asia);
(void)stripped;

void* signedPtr = ptrauth_sign_unauthenticated(
    theHook,
    ptrauth_key_asia,
    slotEntry  // address diversity aka the slot's own address is the context
);

*(void**)slotEntry = signedPtr;

The ptrauth_key_asia you see there is the key used for function pointers / instruction address, A key, for use in data (IA stored in data).

The address diversity part (slotEntry as context) is important because Apple uses the pointer’s own storage address as part of the signing context, so a valid signed pointer from one slot is not a valid signed pointer for a different slot even with the same key. This is to prevent easy reuses.

Final Thoughts

I hope this helps shed some light into how the __DATA.__la_symbol_ptr as well as the __DATA_CONST.__got work on iOS, and especially how jailbreak developers have been abusing these for almost 2 decades to make jailbreak tweaks.

There are many similarities between ELF’s GOT and PLT and iOS’ __DATA_CONST.__got and __DATA.__la_symbol_ptr, but the differences are what makes it interesting to analyze. Thanks for reading, see you in the next one.

More iDevice Central Guides

The post Using __got and __la_symbol_ptr for iOS Hacking / Tweak Development first appeared on iDevice Central.


View Entire Post

Read Entire Article