0xdevalias / poc-re-binsearch

Proof of Concept (PoC) code/notes exploring reverse engineering techniques for macOS fat binaries, focusing on binary searching and automatic offset identification
MIT License
5 stars 2 forks source link

MacOS 15 #2

Open skiman6010 opened 5 months ago

skiman6010 commented 5 months ago

Just wanted to report that this tool can't seem to get the offsets in the identityservicesd for MacOS 15. A couple of the addresses are blank.

❯ ./find_fat_binary_offsets.py /System/Library/PrivateFrameworks/IDS.framework/identityservicesd.app/Contents/MacOS/identityservicesd
-= Universal Binary Sections =-
Architecture 0 (x86_64):
  CPU Type: 16777223 (0x1000007)
  CPU Subtype: 3 (0x3)
  CPU Subtype Capability: 0 (0x0)
  Offset: 0x4000 (Valid Mach-O Header: Yes)
  Size: 10158960
  Align: 14
Architecture 1 (arm64e):
  CPU Type: 16777228 (0x100000c)
  CPU Subtype: 2 (0x2)
  CPU Subtype Capability: 128 (0x80)
  Offset: 0x9b8000 (Valid Mach-O Header: Yes)
  Size: 11717616
  Align: 14

-= Found Symbol Offsets =-
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture x86_64: 0x0ef611
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture arm64e: 0x0d1248

-= Found Hex Offsets (with pure python fixed sequence search + regex) =-
Architecture 0 (x86_64):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xef611
  NACInitAddress:
  NACKeyEstablishmentAddress: 0x618370
  NACSignAddress:
Architecture 1 (arm64e):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xd1248; 0x333688; 0x3607d4; 0x378da4
  NACInitAddress:
  NACKeyEstablishmentAddress:
  NACSignAddress:

Here is the binary. identityservicesd.zip

0xdevalias commented 5 months ago

@skiman6010 Thanks for raising this. I'd have to do some manual reversing to find the offsets and see what changed/how big the change is to understand the specifics of why; the patterns are a bit fragile in general, and it wouldn't surprise me if a major version release outright broke them.

Just so I can name the test binary appropriately, are you able to give the exact version + build this came from? I'm guessing probably this?

Assuming that is correct:

⇒ file samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd
samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e]
samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd (for architecture x86_64):   Mach-O 64-bit executable x86_64
samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd (for architecture arm64e):   Mach-O 64-bit executable arm64e
⇒ sha256sum samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd
fb40ddabafb508c6ad000560709abd40ac3d91aa212105278046c625c9fdf0b7  samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd

And the output of the current auto offset finder (which obviously gets basically none of them):

⇒ ./find_fat_binary_offsets.py samples/macos-15.0-beta-24A5264n-sequoia-identityservicesd

-= Universal Binary Sections =-
Architecture 0 (x86_64):
  CPU Type: 16777223 (0x1000007)
  CPU Subtype: 3 (0x3)
  CPU Subtype Capability: 0 (0x0)
  Offset: 0x4000 (Valid Mach-O Header: Yes)
  Size: 10158960
  Align: 14
Architecture 1 (arm64e):
  CPU Type: 16777228 (0x100000c)
  CPU Subtype: 2 (0x2)
  CPU Subtype Capability: 128 (0x80)
  Offset: 0x9b8000 (Valid Mach-O Header: Yes)
  Size: 11717616
  Align: 14

-= Found Symbol Offsets =-
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture x86_64: 0x0ef611
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture arm64e: 0x0d1248

-= Found Hex Offsets (with pure python fixed sequence search + regex) =-
Architecture 0 (x86_64):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xef611
  NACInitAddress:
  NACKeyEstablishmentAddress: 0x618370
  NACSignAddress:
Architecture 1 (arm64e):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xd1248; 0x333688; 0x3607d4; 0x378da4
  NACInitAddress:
  NACKeyEstablishmentAddress:
  NACSignAddress:
skiman6010 commented 5 months ago

Yes that is the correct build. Sorry I should have put that in the issue.

0xdevalias commented 5 months ago

Sorry I should have put that in the issue.

@skiman6010 No worries, thanks for confirming :)

skiman6010 commented 4 months ago

Hey I got a hash of the beta 2 build (24A5279h) and it seemed to be different from the build I uploaded earlier. Not sure if it is useful for you but I uploaded it just in case. Thanks! identityservicesd.zip

0xdevalias commented 4 months ago

beta 2 build (24A5279h)

@skiman6010 Thanks for that :) Not sure if/when I (or someone else) will be able to get to looking at reversing it, but will be helpful at that stage.

⇒ sha256sum samples/macos-15.0-beta2-24A5279h-sequoia-identityservicesd
42332e0f43bf82e20e14508bb3660e164a5d789c4257ddb0824107a81f941a3c  samples/macos-15.0-beta2-24A5279h-sequoia-identityservicesd

And the output of the current auto offset finder (which obviously gets basically none of them):

⇒ ./find_fat_binary_offsets.py samples/macos-15.0-beta2-24A5279h-sequoia-identityservicesd

-= Universal Binary Sections =-
Architecture 0 (x86_64):
  CPU Type: 16777223 (0x1000007)
  CPU Subtype: 3 (0x3)
  CPU Subtype Capability: 0 (0x0)
  Offset: 0x4000 (Valid Mach-O Header: Yes)
  Size: 10192848
  Align: 14
Architecture 1 (arm64e):
  CPU Type: 16777228 (0x100000c)
  CPU Subtype: 2 (0x2)
  CPU Subtype Capability: 128 (0x80)
  Offset: 0x9c0000 (Valid Mach-O Header: Yes)
  Size: 11768048
  Align: 14

-= Found Symbol Offsets =-
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture x86_64: 0x0eff2d
Offset of _IDSProtoKeyTransparencyTrustedServiceReadFrom in architecture arm64e: 0x0d4a28

-= Found Hex Offsets (with pure python fixed sequence search + regex) =-
Architecture 0 (x86_64):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xeff2d
  NACInitAddress:
  NACKeyEstablishmentAddress: 0x61f560
  NACSignAddress:
Architecture 1 (arm64e):
  ReferenceAddress (_IDSProtoKeyTransparencyTrustedServiceReadFrom): 0xd4a28; 0x337afc; 0x364cb8; 0x37d284
  NACInitAddress:
  NACKeyEstablishmentAddress:
  NACSignAddress:
skiman6010 commented 4 months ago

Yeah hopefully someone has the time/generosity to jump in and reverse this! I have been trying to keep the Go registration provider working for me as much as possible, but unfortunately I don't have the skills/knowledge for reversing these binaries for symbols.

0xdevalias commented 4 months ago

but unfortunately I don't have the skills/knowledge for reversing these binaries for symbols.

@skiman6010 If it's something you're interested in learning, it might not be too hard for you to learn enough to get by. For the x86 version I was using the free version of Binary Ninja:

By loading up one of the older binaries with known/correct offsets, we can see how the decompiled code looks at that point, and then we can try and find similar in the newer binary. Usually this was by searching for part of a log message/similar, and then finding the pattern we were looking for after that.

For the ARM version, Binary Ninja free doesn't support it (would need a paid licence), so I was using Ghidra (which is also free, but IMO a bit harder to use/not as nice GUI/etc):

Then we basically want to locate functions using log strings:

  1. nac_init:
    • Search for the string Calling NACInit with:.
    • The next function call after this log line is the NACInit function.
  2. nac_key_establishment:
    • Search for the string Received validation initialization request.
    • Look above for IMRGLog to identify the function call.
  3. nac_sign:
    • Search for the string failed building validation.
    • The function called with two 0 parameters is nac_sign.

I recommend doing this on one of the older binaries with known offsets first, as that way you can 'double check' the offsets you find with the 'correct answer' to know you've got the process figured out; then you can apply that same process to the newer binaries.


Some other random notes I have locally, not sure if they will help or just add noise, but in case they are useful..

Load the binary, then go to Analysis -> Rebase -> 0x0 to ensure all of the memory addresses are using the proper offsets directly (Binary Ninja loads at 0x100000000 by default). You could also just set the load address to 0x0 in the first analysis options screen too.

ReferenceSymbol

Find IDSProtoKeyTransparencyTrustedServiceReadFrom symbol, get the address of that function

nac_init

Find the string Calling NACInit with:, this is referenced in the code that’s about to call NACInit, there is no symbol so you just have to look for the next function call after that log line is called. If I remember correctly, it will be in an if statement, with a bunch of _objc_msgSend's, and some strings like bytes/length/etc in it:

x86:

if (sub_1004132e0(_objc_msgSend(rax_16, "bytes"), zx.q(_objc_msgSend(rax_16, "length")), var_50.q + 0x18, &var_88, &var_7c) != 0)

arm64:

    if (iVar2 != 0) {
      local_c0 = 0x8400102;
      local_bc = lVar7;
      __os_log_impl(0x100000000,uVar6,0,"Calling NACInit with: %@",&local_c0,0xc);
    }
    _objc_release(uVar6);
    uVar6 = _objc_retainAutorelease(lVar7);
    uVar9 = _objc_msgSend(uVar6,"bytes");
    uVar6 = _objc_msgSend(uVar6,"length");
    uVar6 = FUN_10043d408(uVar9,uVar6,CONCAT62(uStack_a6,local_a8) + 0x18,&local_f8,&local_fc);

nac_key_establishment

Find the string Received validation initialization request, look above IMRGLog:

x86:

if (sub_100465e00(r12_1, _objc_msgSend(rax_22, "bytes"), zx.q(_objc_msgSend(rax_22, "length"))) != 0)

arm64:

  if (iVar3 != 0) {
    local_9c = *(cfstringStruct **)(param_1 + 0x20);
    local_a0 = 0x8400202;
    local_94 = 0x840;
    local_92 = pcVar5;
    __os_log_impl(0x100000000,uVar7,0,
                  "  Received validation initialization request response: %@   error: %@",&local_a0,
                  0x16);
  }
  _objc_release(uVar7);
  if ((param_4 < 2) || (param_4 == 200)) {
    if (*(long *)(*(long *)(*(long *)(param_1 + 0x38) + 8) + 0x18) == 0) goto LAB_100221d3c;
    if ((*(char *)(*(long *)(*(long *)(param_1 + 0x40) + 8) + 0x18) == '\0') &&
       (lVar10 = _objc_msgSend(uVar6,"length"), lVar10 != 0)) {
      uVar15 = *(undefined8 *)(*(long *)(*(long *)(param_1 + 0x38) + 8) + 0x18);
      uVar7 = _objc_retainAutorelease(uVar6);
      uVar14 = _objc_msgSend(uVar7,"bytes");
      uVar7 = _objc_msgSend(uVar7,"length");
      uVar14 = FUN_1003fdafc(uVar15,uVar14,uVar7);

nac_sign

In the 14.2 binary (x86), the instructions for finding nac_sign via failed building validation doesn't seem to work; but working backwards from the offsets you already have for this one, seems there are some other strings like _sendValidationRequestForSubsystem: that can get us in the right function to find the call we need. If I remember correctly, searching for failed building validation, then go to the caller of that function, and look a few lines up for something like this:

x86:

var_a8[0].d = sub_10049f390(_objc_msgSend(rax_13, "validationContext"), 0, 0, &var_58, &var_ac)

arm64:

          if (iVar1 == 0) {
            local_90 = 0;
            uStack_8e = 0;
            uStack_8c._0_4_ = 0;
            local_94 = 0;
            uVar10 = _objc_msgSend(uVar7,"validationContext");
            uVar11 = FUN_1003f2844(uVar10,0,0,&local_90,&local_94);

Then not sure exactly which version/architecture/etc these are from, but I think I was trying to point out to myself exactly which bit of those above code blocks was what I was meant to be using for the offsets, or something like that?

Calling NACInit with:: 0x4b55a0 (FUN_1004b55a0(uVar8,uVar5,p_Var16,&local_f8,&local_fc))

      if (iVar1 != 0) {
        local_c0 = 0x8400102;
        local_bc = lVar6;
        __os_log_impl(0x100000000,uVar5,0,"Calling NACInit with: %@",&local_c0,0xc);
      }
      _objc_release(uVar5);
      uVar5 = _objc_retainAutorelease(lVar6);
      uVar8 = FUN_10063b7a0();
      uVar5 = FUN_10064cb60(uVar5);
      p_Var16 = (_NSZone *)(CONCAT62(uStack_a6,local_a8) + 0x18);
      auVar17 = FUN_1004b55a0(uVar8,uVar5,p_Var16,&local_f8,&local_fc);
      SVar15 = auVar17._8_8_;

Received validation initialization request: 0x4a2e04 (FUN_1004a2e04(uVar15,uVar13,uVar7);)

    if ((*(char *)(*(long *)(*(long *)(param_1 + 0x40) + 8) + 0x18) == '\0') &&
       (auVar16 = FUN_10064cb60(uVar6), uVar7 = auVar16._8_8_, auVar16._0_8_ != 0)) {
      uVar15 = *(undefined8 *)(*(long *)(*(long *)(param_1 + 0x38) + 8) + 0x18);
      uVar7 = _objc_retainAutorelease(uVar6);
      uVar13 = FUN_10063b7a0();
      uVar7 = FUN_10064cb60(uVar7);
      uVar13 = FUN_1004a2e04(uVar15,uVar13,uVar7);
      if ((int)uVar13 != 0) {
        FUN_100668640(&_OBJC_CLASS_$_IMRGLog);

failed building validation: 0x47d010 (FUN_10047d010(uVar5,0,0,&local_90,&local_94);)

          if (iVar1 == 0) {
            local_90 = 0;
            uStack_8e = 0;
            uStack_8c._0_4_ = 0;
            local_94 = 0;
            uVar5 = FUN_100667e60(IVar7);
            auVar20 = FUN_10047d010(uVar5,0,0,&local_90,&local_94);
skiman6010 commented 4 months ago

Also interesting app I found that seems to "ask nicely" for registration data instead of reversing binaries. https://github.com/TaeHagen/Mac-Hardware-Info

0xdevalias commented 4 months ago

Also interesting app I found that seems to "ask nicely" for registration data instead of reversing binaries. TaeHagen/Mac-Hardware-Info

@skiman6010 From a quick skim through the code, it looks like this is probably the file doing the 'main work' of gathering system info:

Which then is seemingly encoded into a QR code, which I imagine may then be passed back to whatever app consumes it.

Looking at the BlueBubbles FAQ, it seems like the basic functionality just reads from the iMessage database using full disk access. It also briefly mentions the QR code part:

Then it goes deeper into enabling the 'private API' by disabling SIP; which from a quick skim, sounds like that allows it to inject a helper process into the iMesssage app that allows calling internal functions:

Based on the fact that it reads the database in the 'normal' mode, or injects a co-process in the 'private API' mode; I wonder if this is actually doing the same sort of things as the Beeper version was; as I believe that was actually talking directly to Apple's remote API once it had the required auth creds; whereas BlueBubbles seems as though it's just piggybacking off/manipulating the Messages app itself.

skiman6010 commented 4 months ago

Probably out of scope of this issue but I did find it interesting. This app is actually used for a fork of BlueBubbles, that's currently being called OpenBubbles. I should have added the extra context in my original comment.

You are correct, BlueBubbles by itself uses a Mac as a bridge like the old Beeper bridge.

OpenBubbles uses the rustpush repo to authenticate itself as a Mac. It is also supposedly compatible with Beeper registration codes. https://github.com/TaeHagen/bluebubbles-app/tree/rustpush

0xdevalias commented 4 months ago

OpenBubbles uses the rustpush repo to authenticate itself as a Mac. It is also supposedly compatible with Beeper registration codes. TaeHagen/bluebubbles-app@rustpush

@skiman6010 Ah, true; fair enough, that makes sense.