steven-michaud / HookCase

Tool for reverse engineering macOS/OS X

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Interpose hooks largely broken on macOS 13 (Ventura)

sdwannfv opened this issue · comments

dtruss objective c code

        NSString *str = @"11111111111111111";
        NSError *error;
        [str writeToFile:@"/Users/abc/test.txt" atomically:TRUE encoding:NSUTF8StringEncoding error:&error];
 

fstatat64(0xFFFFFFFFFFFFFFFE, 0x7FF7BFCDA9D0, 0x7FF7BFCDA3C0) = -1 Err#2
getattrlist("/Users/abc\0", 0x7FF7BFCDA0E0, 0x7FF7BFCDA0D8) = 0 0
open_dprotected_np(0x7FF7BFCDA9D0, 0xA02, 0x3) = 3 0
write(0x3, "11111111111111111\0", 0x11) = 17 0
fsync(0x3, 0x0, 0x0) = 0 0
fsetxattr(0x3, 0x7FF7BFCDAC60, 0x600001B65190) = 0 0
rename("/Users/abc/.dat.nosync7b14.Ok7wlO\0", "/Users/abc/test.txt\0") = 0 0
close(0x3) = 0 0

but can't hook open_dprotected_np and rename in that objective c code indirect call them, meanwhile direct call open_dprotected_np/rename in objective c marh-o is ok.

ProductName: macOS
ProductVersion: 13.0.1
BuildVersion: 22A400

It's not at all clear what you're asking about.

Please start by showing the code from you hook library that you think isn't working correctly.

myapp is

  NSString *str = @"11111111111111111";
  NSError *error;
  [str writeToFile:@"/Users/abc/test.txt" atomically:TRUE encoding:NSUTF8StringEncoding error:&error];
  open_dprotected_np("/Users/abc/test.txt", O_RDWR, 0, 0);

my hook lib

int Hooked_open_dprotected_np(const char* path, int flags, int dpclass, int dpflags, ...)
{
    int mode = 0;
    LogWithFormat(true, "--> open_dprotected_np(%s)", path);
    if (flags & O_CREAT) {
        va_list ap;
        va_start(ap, dpflags);
        mode = va_arg(ap, int);
        va_end(ap);
    }
    return open_dprotected_np(path, flags, dpclass, dpflags, mode);
}
INTERPOSE_FUNCTION(open_dprotected_np),

then run HC_INSERT_LIBRARY=/path/to/myhooklib myapp, it's expected print open_dprotected_np twice, one for "str writeToFile" and another for "open_dprotected_np", but only once.

I can reproduce what you report on macOS 13.0.1, but not on macOS 12.6.1. Both times I used HookCase 7.1 (the current version).

Very odd. I will dig into this, and try to find out what's going on.

Thanks for your report! It's very unlikely that I would have noticed this on my own.

In the meantime, you can work around the bug by using PATCH_FUNCTION instead of INTERPOSE_FUNCTION:

int (*open_dprotected_np_caller)(const char* path, int flags, int dpclass, int dpflags, ...) = NULL;
int Hooked_open_dprotected_np(const char* path, int flags, int dpclass, int dpflags, ...)
{
    int mode = 0;
    LogWithFormat(true, "--> open_dprotected_np(%s)", path);
    if (flags & O_CREAT) {
        va_list ap;
        va_start(ap, dpflags);
        mode = va_arg(ap, int);
        va_end(ap);
    }
    return open_dprotected_np_caller(path, flags, dpclass, dpflags, mode);
}
PATCH_FUNCTION(open_dprotected_np, /usr/lib/system/libsystem_kernel.dylib),

By the way, it's the call from Foundation that doesn't get hooked (using an interpose hook) on macOS 13.0.1. The stack trace for that call (which you can see using a patch hook) is as follows.

(Tue Dec 13 11:52:19 2022) /Users/smichaud/Documents/ReverseEngineering/Bugs/HookCase-Issue40/hook/test[1110] [0x112eea2c0] --> open_dprotected_np(./.dat.nosync0456.p0eeBm)
    (hook.dylib) PrintStackTrace() + 0x68
    (hook.dylib) Hooked_open_dprotected_np(char const*, int, int, int, ...) + 0x8b
    (Foundation) _NSOpenFileDescriptor_Protected + 0x5f
    (Foundation) _NSCreateTemporaryFile_Protected + 0x2b0
    (Foundation) _NSWriteDataToFileWithExtendedAttributes + 0x20e
    (Foundation) writeStringToURLOrPath + 0xd9
    (test) main + 0x48
    (dyld) start + 0x980

Just to make it clear, here's the code for the app I tested with. It's a command line utility. That may make a difference. I haven't noticed any problems with interpose hooks on macOS 13.0.1 in GUI apps. I assume you've also been testing with a command line utility.

#include <stdio.h>
#import <Cocoa/Cocoa.h>

int main(int argc, char *argv[])
{
  NSString *str = @"11111111111111111";
  NSError *error;
  [str writeToFile:@"./test.txt" atomically:TRUE encoding:NSUTF8StringEncoding error:&error];
  open_dprotected_np("./test.txt", O_RDWR, 0, 0);

  return 0;
}

And here's the Makefile for this app (test) and my hook library (hook.dylib):

CC=/usr/bin/clang++
MOJAVE_OR_ABOVE=$(shell expr `uname -r | cut -c 1-2` \>= 18)
# 32-bit builds no longer work on Mojave with some versions of XCode
ifeq ($(MOJAVE_OR_ABOVE), 1)
ARCHS=-arch x86_64
else
ARCHS=-arch i386 -arch x86_64
endif

all : test hook.dylib

hook.dylib : hook.o
	$(CC) $(ARCHS) -o hook.dylib hook.o \
		-lobjc -framework Cocoa -framework Carbon \
		-Wl,-F/System/Library/PrivateFrameworks -framework CoreSymbolication \
		-dynamiclib

hook.o : hook.mm
	$(CC) $(ARCHS) -o hook.o \
		-Wno-deprecated-declarations -c hook.mm

test.o : test.mm
	$(CC) $(ARCHS) -o test.o \
		-Wno-deprecated-declarations -c test.mm

test : test.o
	$(CC) $(ARCHS) -o test test.o \
		-lobjc -framework Cocoa -framework Carbon

clean :
	rm hook.o hook.dylib test.o test.txt test

sdwannfv pointed out to me in email that the same problem exists (on macOS 13) with a GUI app, TextEdit. I worked up my own testcase that shows this. I still haven't had a chance to find out why. I've been busy with other things. With luck I'll have more time over the next few days. In the meantime it's best to use patch hooks on macOS 13.

int Hooked_open(const char *path, int flags, ...)
{
  int mode = 0;
  if (flags & O_CREAT) {
    va_list ap;
    va_start(ap, flags);
    mode = va_arg(ap, int);
    va_end(ap);
  }

  LogWithFormat(true, "--> open(%s, %x) InterposeHooked", path, flags);
  return open(path, flags, mode);
}

int (*__open_caller)(const char *path, int flags, ...) = NULL;

int Hooked___open(const char *path, int flags, ...)
{
  int mode = 0;
  if (flags & O_CREAT) {
    va_list ap;
    va_start(ap, flags);
    mode = va_arg(ap, int);
    va_end(ap);
  }

  int retval = __open_caller(path, flags, mode);
  LogWithFormat(true, "--> __open(%s, %x) PatchHooked", path, flags);

  reset_hook(reinterpret_cast<void*>(Hooked___open));

  return retval;
}
  INTERPOSE_FUNCTION(open),
  PATCH_FUNCTION(__open, /usr/lib/system/libsystem_kernel.dylib),

For reasons that I won't go into here (for the moment), this hook library works much better if you invoke it with HC_NOKIDS=1:

HC_NOKIDS=1 HC_INSERT_LIBRARY=/full/path/to/hook.dylib /System/Applications/TextEdit.app/Contents/MacOS/TextEdit

On macOS 12.6.2, almost every call to open() (via an interpose hook) is matched to a call to __open() (via a patch hook). But on macOS 13.0.1 and 13.1 there are many more calls to __open(), and many of them don't match any call to open(). It's a real puzzle. It may take me a while to figure out.

by the way, does patch hook support to hook functions in /usr/lib/system/libsystem_c.dylib which does not exist in file system?

As of macOS 11 (Big Sur), Apple removed most dylibs and frameworks from the file system. They still exist, but only in the "dyld shared cache". Some disassemblers (notably Hopper) can extract objects directly from the cache. But Apple's utilities (like nm) require actual files to work with. And (of course) Apple doesn't provide a utility to extract the contents of the dyld shared cache into files. It does, though, provide a /usr/lib/dsc_extractor.bundle that implements the required functionality. And there's a third party utility which uses it to perform the task:

https://github.com/keith/dyld-shared-cache-extractor

Use this to get a copy of libsystem_c.dylib and run nm on it. You'll see a bunch of open-related calls, all of which are imported from libsystem_kernel.dylib:

(undefined) external ___open (from libsystem_kernel)
(undefined) external ___open_extended (from libsystem_kernel)
(undefined) external ___open_nocancel (from libsystem_kernel)
(undefined) external _open$NOCANCEL (from libsystem_kernel)
(undefined) external _open_dprotected_np (from libsystem_kernel)
(undefined) external _openat$NOCANCEL (from libsystem_kernel)

This is generally how things work on macOS. There's usually just one implementation of a system call, which gets called by all the binary modules that use it. To set a patch hook on it, you just need to find it, using tools like grep and nm.

Another thing: Though libsystem_c.dylib doesn't exist in the file system, you can use it in a PATCH_FUNCTION invocation, like so:

PATCH_FUNCTION(fopen, /usr/lib/system/libsystem_c.dylib),

This works because the paths of the modules loaded into a process correspond to the locations where they would exist in the file system, if they still did.

And yet another thing:

On macOS 13 (Ventura), the dyld shared cache files are found in the following directory:

/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/

As of macOS 12 (Monterey), HookCase largely uses a binary module's __got section to implement interpose hooks. As best I can tell "GOT" stands for "global offset table". It contains the addresses of functions in other modules, referred to by entries in the "indirect symbol table". It's used to resolve calls to these functions.

As of macOS 13 (Ventura), Apple "optimizes" the __got sections of most binary modules in the dyld shared cache. This makes them unusable by HookCase, at least as currently written. I need to figure out how to make them usable, or to come up with some other workaround. This won't be easy. I'm stopping work for the holiday season, and won't resume it until January of next year.

In the meantime, you'll generally need to use patch hooks instead of interpose hooks on macOS 13.

Interpose hooks still work in some modules, which is why I didn't notice this problem earlier. But they do seem to be broken in the most heavily used modules -- like the Foundation and AppKit frameworks. Apple's "optimizations" are intended to speed up these modules' use of external functions.

I've just released a new version of HookCase (7.1.1) that fixes this bug. Please try it out.

I wasn't able to use directly the information in the "optimized" __got sections. But I did manage to find a way to get at their contents indirectly, via the "stubs table".