From 41144ff0dcc54dc0f041d2a57c0752902e1c1eaa Mon Sep 17 00:00:00 2001 From: nganhkhoa Date: Wed, 10 Jan 2024 15:56:32 +0700 Subject: [PATCH 1/5] go fmt --- .../internal/wrapper/action/save_imports.go | 16 +- macho-go/internal/wrapper/cli.go | 12 +- macho-go/pkg/ios/macho/dyld_info.go | 2 +- macho-go/pkg/ios/macho/edit.go | 4 +- macho-go/pkg/ios/macho/objc.go | 180 +++++++++--------- 5 files changed, 106 insertions(+), 108 deletions(-) diff --git a/macho-go/internal/wrapper/action/save_imports.go b/macho-go/internal/wrapper/action/save_imports.go index efa7b58..fc87088 100644 --- a/macho-go/internal/wrapper/action/save_imports.go +++ b/macho-go/internal/wrapper/action/save_imports.go @@ -139,14 +139,14 @@ func (action *saveImports) saveToInfo(mf *MachoFile) error { mf.Info().Main = mc.Main() - selectors_list := []*protomodel.MachoInfo_Selector{} - for _, sel := range mc.CollectSpecialSelectors() { - selectors_list = append(selectors_list, &protomodel.MachoInfo_Selector{ - Idx: uint32(sel.Idx()), - Name: sel.Name(), - }) - } - mf.Info().SpecialSelectors = selectors_list + selectors_list := []*protomodel.MachoInfo_Selector{} + for _, sel := range mc.CollectSpecialSelectors() { + selectors_list = append(selectors_list, &protomodel.MachoInfo_Selector{ + Idx: uint32(sel.Idx()), + Name: sel.Name(), + }) + } + mf.Info().SpecialSelectors = selectors_list return nil } diff --git a/macho-go/internal/wrapper/cli.go b/macho-go/internal/wrapper/cli.go index 3e7efda..fffc048 100644 --- a/macho-go/internal/wrapper/cli.go +++ b/macho-go/internal/wrapper/cli.go @@ -302,16 +302,16 @@ func bcell2header(bfile string, header string) { fmt.Fprintf(w, "__attribute__((section(\"__DATA,bshield\")))\n") fmt.Fprintf(w, "uint32_t special_selectors_idx[] = {\n") - for _, selector := range info.GetSpecialSelectors() { - fmt.Fprintf(w, "%x,\n", selector.Idx) - } + for _, selector := range info.GetSpecialSelectors() { + fmt.Fprintf(w, "%x,\n", selector.Idx) + } fmt.Fprintf(w, "};\n") fmt.Fprintf(w, "__attribute__((section(\"__DATA,bshield\")))\n") fmt.Fprintf(w, "const char* special_selectors_name[] = {\n") - for _, selector := range info.GetSpecialSelectors() { - fmt.Fprintf(w, "\"%s\",\n", selector.Name) - } + for _, selector := range info.GetSpecialSelectors() { + fmt.Fprintf(w, "\"%s\",\n", selector.Name) + } fmt.Fprintf(w, "};\n") fmt.Fprintf(w, "uint32_t n_selectors = %d;\n", len(info.GetSpecialSelectors())) } diff --git a/macho-go/pkg/ios/macho/dyld_info.go b/macho-go/pkg/ios/macho/dyld_info.go index 21dd92c..a616392 100644 --- a/macho-go/pkg/ios/macho/dyld_info.go +++ b/macho-go/pkg/ios/macho/dyld_info.go @@ -43,7 +43,7 @@ func (sym *ImportSymbol) Type() string { } func (sym *ImportSymbol) SafeForRemoval() bool { - return sym.typ == "lazy" || sym.typ == "fixups" + return sym.typ == "lazy" || sym.typ == "fixups" } func (sym *ImportSymbol) Dylib() string { diff --git a/macho-go/pkg/ios/macho/edit.go b/macho-go/pkg/ios/macho/edit.go index 67a2d8a..1d75c04 100644 --- a/macho-go/pkg/ios/macho/edit.go +++ b/macho-go/pkg/ios/macho/edit.go @@ -280,8 +280,8 @@ func (mc *MachoContext) RemoveBindSymbols() { rand.Seed(time.Now().UnixNano()) - isModernSymbol := mc.dyldinfo == nil - isLegacySymbol := !isModernSymbol + isModernSymbol := mc.dyldinfo == nil + isLegacySymbol := !isModernSymbol if isModernSymbol { mc.removeBindSymbolsModern() diff --git a/macho-go/pkg/ios/macho/objc.go b/macho-go/pkg/ios/macho/objc.go index ec01d8d..779ce27 100644 --- a/macho-go/pkg/ios/macho/objc.go +++ b/macho-go/pkg/ios/macho/objc.go @@ -1,27 +1,26 @@ package macho import ( - "fmt" - "bytes" - "encoding/binary" - "io" - "strings" + "bytes" + "encoding/binary" + "fmt" + "io" + "strings" . "ios-wrapper/pkg/ios" ) - type SpecialSelector struct { - idx uint - name string + idx uint + name string } func (sel *SpecialSelector) Idx() uint { - return sel.idx + return sel.idx } func (sel *SpecialSelector) Name() string { - return sel.name + return sel.name } // collect the index and the name in selector list of special method names @@ -33,9 +32,9 @@ func (sel *SpecialSelector) Name() string { // - retain func (mc *MachoContext) CollectSpecialSelectors() []*SpecialSelector { - var special_selectors []*SpecialSelector - var methods []byte - var methname_offset uint32 + var special_selectors []*SpecialSelector + var methods []byte + var methname_offset uint32 for _, cmd := range mc.commands { if cmd.Cmd() == LC_MAIN { @@ -49,50 +48,50 @@ func (mc *MachoContext) CollectSpecialSelectors() []*SpecialSelector { if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__TEXT")) == 0 { for _, section := range segment.Sections() { if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methname")) == 0 { - methname_offset = section.Offset() - methods = make([]byte, section.Size()) - mc.file.ReadAt(methods, int64(section.Offset())) + methname_offset = section.Offset() + methods = make([]byte, section.Size()) + mc.file.ReadAt(methods, int64(section.Offset())) } } } if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__DATA")) == 0 { for _, section := range segment.Sections() { if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_selrefs")) == 0 { - selectors_buffer := make([]byte, section.Size()) - mc.file.ReadAt(selectors_buffer, int64(section.Offset())) + selectors_buffer := make([]byte, section.Size()) + mc.file.ReadAt(selectors_buffer, int64(section.Offset())) - buffer := bytes.NewReader(selectors_buffer) + buffer := bytes.NewReader(selectors_buffer) - for i := uint(0); i < uint(section.Size()) / 8; i++ { - // this field is actually a Rebase - // we assume that no rebase is needed - // so everything sticks to its file offset - var offset uint32 - binary.Read(buffer, mc.byteorder, &offset) // first 4 bytes is offset + for i := uint(0); i < uint(section.Size())/8; i++ { + // this field is actually a Rebase + // we assume that no rebase is needed + // so everything sticks to its file offset + var offset uint32 + binary.Read(buffer, mc.byteorder, &offset) // first 4 bytes is offset - var name_builder strings.Builder - for j := uint32(0); ; j++ { - c := methods[offset - methname_offset + j] - if c == 0 { - break - } - name_builder.WriteByte(c) - } - name := name_builder.String() - if name == "load" { - special_selectors = append(special_selectors, &SpecialSelector{ - idx: i, - name: name, - }) - } + var name_builder strings.Builder + for j := uint32(0); ; j++ { + c := methods[offset-methname_offset+j] + if c == 0 { + break + } + name_builder.WriteByte(c) + } + name := name_builder.String() + if name == "load" { + special_selectors = append(special_selectors, &SpecialSelector{ + idx: i, + name: name, + }) + } - binary.Read(buffer, mc.byteorder, &offset) // ignore rebase arguments - } + binary.Read(buffer, mc.byteorder, &offset) // ignore rebase arguments + } } } } } - return special_selectors + return special_selectors } func (mc *MachoContext) ReworkForObjc() { @@ -131,14 +130,14 @@ func (mc *MachoContext) ReworkForObjc() { mc.file.WriteAt([]byte{0, 0, 0, 0}, section_ptr+0x40) } - // erases all objc method names - // this should still works because the cache inserts the pointer value not string - // but some symbols relies on pre-defined implementations, such as **load** method - // load method is the same across all classes and so objc define an implementation - // selector should points to this load selector to make objc thinks that it's "load" + // erases all objc method names + // this should still works because the cache inserts the pointer value not string + // but some symbols relies on pre-defined implementations, such as **load** method + // load method is the same across all classes and so objc define an implementation + // selector should points to this load selector to make objc thinks that it's "load" if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methname")) == 0 { // mc.file.WriteAt([]byte("__objc_methbruh"), section_ptr) - mc.file.WriteAt(make([]byte, section.Size()), int64(section.Offset())) + mc.file.WriteAt(make([]byte, section.Size()), int64(section.Offset())) } section_ptr += 16*2 + 8*2 + 4*8 } @@ -167,7 +166,7 @@ func (mc *MachoContext) ReworkForObjc() { last := sections[len(sections)-1] data_end = int(last.Addr() - segment.Vmaddr() + segment.Fileoff() + last.Size()) - // do not register selector and see what happens + // do not register selector and see what happens section_ptr := ptr + 0x40 + 8 for _, section := range segment.Sections() { if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_selrefs")) == 0 { @@ -196,24 +195,24 @@ func (mc *MachoContext) ReworkForObjc() { // must recover stack to same value before calling main // must recover link register before calling main - // ┌─────────────────┐ - // │ │ - // shellcode starts ────┼─────────────────┼───── │ │instruction - // │ │ │ │fetch RIP size - // RIP returns ────┼─────────────────┼───── ▲ │ │ - // │ │ │ │ - // │ │ │ │ shellcode length - // shellcode ends │ │ │ offset │ - // __text ────┼─────────────────┼───── │ range │ - // │ │ │ │ __DATA ends - __text - // │ │ │ │ - // __DATA ends ────┼─────────────────┼───── ▼ │ - // │ │ - // │ │ - // │ │ - // │ │ - // │ │ - // └─────────────────┘ + // ┌─────────────────┐ + // │ │ + // shellcode starts ────┼─────────────────┼───── │ │instruction + // │ │ │ │fetch RIP size + // RIP returns ────┼─────────────────┼───── ▲ │ │ + // │ │ │ │ + // │ │ │ │ shellcode length + // shellcode ends │ │ │ offset │ + // __text ────┼─────────────────┼───── │ range │ + // │ │ │ │ __DATA ends - __text + // │ │ │ │ + // __DATA ends ────┼─────────────────┼───── ▼ │ + // │ │ + // │ │ + // │ │ + // │ │ + // │ │ + // └─────────────────┘ shellcode := []uint32{} ins_size_byte := 4 @@ -223,32 +222,32 @@ func (mc *MachoContext) ReworkForObjc() { isArm := (mc.header.cputype & 0xff) == 12 if isArm { - // we use shorthand store/load multiple - // arm also has different indexing instruction, so be careful - // https://developer.arm.com/documentation/102374/0101/Loads-and-stores---addressing - /* - adr x8, 0 - # x9 = (offset end of __DATA) - (offset shellcode) - movz x9, #0x9999 - add x8, x8, x9 + // we use shorthand store/load multiple + // arm also has different indexing instruction, so be careful + // https://developer.arm.com/documentation/102374/0101/Loads-and-stores---addressing + /* + adr x8, 0 + # x9 = (offset end of __DATA) - (offset shellcode) + movz x9, #0x9999 + add x8, x8, x9 - stp x30, x8, [sp], #-0x10 - stp x3, x2, [sp], #-0x10 - stp x1, x0, [sp], #-0x10 + stp x30, x8, [sp], #-0x10 + stp x3, x2, [sp], #-0x10 + stp x1, x0, [sp], #-0x10 - # custom intializer - ldr x9, [x8] - blr x9 + # custom intializer + ldr x9, [x8] + blr x9 - ldp x1, x0, [sp, #0x10]! - ldp x3, x2, [sp, #0x10]! - ldp x30, x8, [sp, #0x10]! + ldp x1, x0, [sp, #0x10]! + ldp x3, x2, [sp, #0x10]! + ldp x30, x8, [sp, #0x10]! - # original main - # link register is set so jump only - ldr x9, [x8, #8] - br x9 - */ + # original main + # link register is set so jump only + ldr x9, [x8, #8] + br x9 + */ shellcode = []uint32{ 0x10000008, 0, // x9 = (offset end of __DATA) - (offset shellcode) @@ -362,4 +361,3 @@ func (mc *MachoContext) ReworkForObjc() { offset += 4 } } - From 901f1ed819843ec9354fd184e938523c36ea7ae4 Mon Sep 17 00:00:00 2001 From: nganhkhoa Date: Wed, 10 Jan 2024 15:56:55 +0700 Subject: [PATCH 2/5] add rule for go format --- macho-go/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macho-go/Makefile b/macho-go/Makefile index e08d340..3404aa9 100644 --- a/macho-go/Makefile +++ b/macho-go/Makefile @@ -9,3 +9,6 @@ build-linux: module: go get -u google.golang.org/protobuf/cmd/protoc-gen-go + +format: + go fmt ./... From f795e9b99df9b5e61dc977d565bc1e3d6d8fb59a Mon Sep 17 00:00:00 2001 From: nganhkhoa Date: Fri, 9 Feb 2024 14:01:34 +0700 Subject: [PATCH 3/5] add simple objc hooking by modifying the method pointer --- macho-go/internal/wrapper/info.go | 2 + macho-go/pkg/ios/macho/objc.go | 166 ++++++++++++++++++++++- research/custom_loader/a.mm | 90 ++++++++++--- research/custom_loader/b.cc | 216 +++++++++++++++++++++++++++++- research/custom_loader/build.sh | 6 +- 5 files changed, 453 insertions(+), 27 deletions(-) diff --git a/macho-go/internal/wrapper/info.go b/macho-go/internal/wrapper/info.go index e3fef8e..45a921c 100644 --- a/macho-go/internal/wrapper/info.go +++ b/macho-go/internal/wrapper/info.go @@ -46,6 +46,8 @@ func (printer *InfoPrinter) Print() { ) } + mc.CollectObjectiveCClasses() + fmt.Println("======") } } diff --git a/macho-go/pkg/ios/macho/objc.go b/macho-go/pkg/ios/macho/objc.go index 779ce27..a699e79 100644 --- a/macho-go/pkg/ios/macho/objc.go +++ b/macho-go/pkg/ios/macho/objc.go @@ -6,10 +6,164 @@ import ( "fmt" "io" "strings" + "unsafe" . "ios-wrapper/pkg/ios" ) +// #include "fixups.h" +import "C" + +func (mc *MachoContext) CollectObjectiveCClasses() { + var objc_const *bytes.Reader + var objc_const_start uint64 + var objc_const_end uint64 + // var objc_methname []byte + + for _, cmd := range mc.commands { + if cmd.Cmd() == LC_MAIN { + continue + } + if cmd.Cmd() != LC_SEGMENT_64 { + continue + } + var segment = cmd.(*Segment64) + + // we assume the binary comes in perfect ordering, that is as laid out below + + if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__TEXT")) == 0 { + for _, section := range segment.Sections() { + buffer := make([]byte, section.Size()) + mc.file.ReadAt(buffer, int64(section.Offset())) + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_stubs")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methlist")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methname")) == 0 { + // objc_methname := buffer + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_classname")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methtype")) == 0 { + } + } + } + if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__DATA_CONST")) == 0 { + for _, section := range segment.Sections() { + buffer := make([]byte, section.Size()) + mc.file.ReadAt(buffer, int64(section.Offset())) + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_classlist")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_nlclslist")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_imageinfo")) == 0 { + } + } + } + if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__DATA")) == 0 { + for _, section := range segment.Sections() { + buffer := make([]byte, section.Size()) + mc.file.ReadAt(buffer, int64(section.Offset())) + reader := bytes.NewReader(buffer) + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_const")) == 0 { + objc_const = reader + objc_const_start = uint64(section.Offset()) + objc_const_end = objc_const_start + section.Size() + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_selrefs")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_classrefs")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_superrefs")) == 0 { + } + if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_data")) == 0 { + // this section contains a series of class_t + // struct _class_t { + // struct _class_t *isa; + // struct _class_t * const superclass; + // void *cache; + // IMP *vtable; + // struct class_ro_t *ro; + // }; + + for i := uint64(0); i < (section.Size() / uint64(mc.pointersize * 5)); i++ { + var isa uint64 + var superclass uint64 + var cache uint64 + var vtable uint64 + var ro uint64 + binary.Read(reader, mc.byteorder, &isa) + binary.Read(reader, mc.byteorder, &superclass) + binary.Read(reader, mc.byteorder, &cache) + binary.Read(reader, mc.byteorder, &vtable) + binary.Read(reader, mc.byteorder, &ro) + + + fmt.Printf("at=0x%x\n", section.Offset() + uint32(i) * mc.pointersize * 5) + fmt.Printf("isa=0x%x superclass=0x%x\n", isa, superclass) + fmt.Printf("cache=0x%x vtable=0x%x\n", cache, vtable) + fmt.Printf("ro=0x%x\n", ro) + + var bind int + var ret1 uint64 + var ret2 uint64 + C.ParseFixValue(C.int(2), C.uint64_t(ro), + (*C.int)(unsafe.Pointer(&bind)), + (*C.uint64_t)(unsafe.Pointer(&ret1)), + (*C.uint64_t)(unsafe.Pointer(&ret2)), + ) + + // is rebase, because ro points to objc_const + // and address is in range + if (bind != 1 && ret1 >= objc_const_start && ret1 < objc_const_end) { + offset := ret1 - objc_const_start + objc_const.Seek(int64(offset), 0) + + // struct _class_ro_t { + // uint32_t const flags; + // uint32_t const instanceStart; + // uint32_t const instanceSize; + // uint32_t const reserved; // only when building for 64bit targets + // const uint8_t * const ivarLayout; + // const char *const name; + // const struct _method_list_t * const baseMethods; + // const struct _protocol_list_t *const baseProtocols; + // const struct _ivar_list_t *const ivars; + // const uint8_t * const weakIvarLayout; + // const struct _prop_list_t * const properties; + // }; + + var tmp uint32 + var ivarLayout uint64 // ptr + var name uint64 // ptr + var baseMethods uint64 // ptr + var baseProtocols uint64 // ptr + var ivars uint64 // ptr + var weakIvarLayout uint64 // ptr + var properties uint64 // ptr + binary.Read(objc_const, mc.byteorder, &tmp) + binary.Read(objc_const, mc.byteorder, &tmp) + binary.Read(objc_const, mc.byteorder, &tmp) + binary.Read(objc_const, mc.byteorder, &tmp) + binary.Read(objc_const, mc.byteorder, &ivarLayout) + binary.Read(objc_const, mc.byteorder, &name) + binary.Read(objc_const, mc.byteorder, &baseMethods) + binary.Read(objc_const, mc.byteorder, &baseProtocols) + binary.Read(objc_const, mc.byteorder, &ivars) + binary.Read(objc_const, mc.byteorder, &weakIvarLayout) + binary.Read(objc_const, mc.byteorder, &properties) + + fmt.Printf("method list: %x\n", baseMethods) + } + fmt.Printf("========\n") + } + } + } + } + } + +} + type SpecialSelector struct { idx uint name string @@ -30,8 +184,12 @@ func (sel *SpecialSelector) Name() string { // we currently have the following symbols guaranteed to be in this list: // - load // - retain +// +// besides special selectors, selectors of outside classes must also be +// registered through the cache +// selectors of outside classes are defined as not being referenced by +// internal classes in __objc_data func (mc *MachoContext) CollectSpecialSelectors() []*SpecialSelector { - var special_selectors []*SpecialSelector var methods []byte var methname_offset uint32 @@ -137,7 +295,7 @@ func (mc *MachoContext) ReworkForObjc() { // selector should points to this load selector to make objc thinks that it's "load" if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_methname")) == 0 { // mc.file.WriteAt([]byte("__objc_methbruh"), section_ptr) - mc.file.WriteAt(make([]byte, section.Size()), int64(section.Offset())) + // mc.file.WriteAt(make([]byte, section.Size()), int64(section.Offset())) } section_ptr += 16*2 + 8*2 + 4*8 } @@ -360,4 +518,8 @@ func (mc *MachoContext) ReworkForObjc() { mc.file.WriteAt(bs, offset) offset += 4 } + + // make __TEXT writable lol + mc.file.Seek(0, 0) + mc.file.WriteAt([]byte{0x7}, 0xa0) } diff --git a/research/custom_loader/a.mm b/research/custom_loader/a.mm index 5eb730e..3eb2ec8 100644 --- a/research/custom_loader/a.mm +++ b/research/custom_loader/a.mm @@ -2,12 +2,19 @@ #include #include +void* sel_lookUpByName(const char* name); + + @interface Foo : NSObject @end @implementation Foo - (void)bar { - NSLog(@"[Foo bar]: %@", self); + NSLog(@"Invoke instance method %@", self); +} + +- (void)tobehijacked:(NSString*)input { + NSLog(@"Invoke tobehijacked method %@", input); } @end @@ -18,48 +25,99 @@ static int x; + (void)load { - NSLog(@"%@", self); - // NSLog(@"x=%d", x) - printf("printf in [Bar load]\n"); x = 1; + printf("Invoke +load method\n"); } - (void)dummy { - NSLog(@"dummy bar x=%d", x); + NSLog(@"Static value check after +load should be 1: x=%d", x); +} +@end + + + +@interface FakeNSDateFormatter : NSDateFormatter { +} +@end + +@implementation FakeNSDateFormatter +- (NSDate*)dateFromString:(NSString*)dateString { + NSLog(@"Hijacked the NSDateFormatter"); + return [super dateFromString:dateString]; } @end __attribute__((constructor)) static void hmmge(int argc, char** argv) { - // create a dummy blank function to be replaced to call OBJC load - printf("hmmge=%p\n", hmmge); - printf("hmmge argc=%d\n", argc); + printf("Invoke C constructor\n"); + printf("Checking for arguments to be passed correctly\n"); + printf(" argc=%d\n", argc); for (int i = 0; i < argc; i++) { - printf(" hmmge argv[%d]=%s\n", i, argv[i]); + printf(" argv[%d]=%s\n", i, argv[i]); } - NSLog(@"hmmge in objc-c"); + NSLog(@"Using Objective-C in C constructor"); + NSLog(@"Test static Objective-C class is initialized and +load completed"); Bar *bar = [[Bar alloc] init]; [bar dummy]; } -int main(int argc, const char * argv[]) { +int main(int argc, const char * argv[], char* envp[]) { @autoreleasepool { - NSLog(@"main()"); - NSLog(@"selector for \"bar:\" %p", @selector(bar:)); + NSLog(@"Invoke main()"); + // Foo bar using Objective-C syntax Foo *foo = [[Foo alloc] init]; [foo bar]; - NSLog(@"directly call \"bar\" %p through objc_msgSend %p with object foo %p\n", @selector(bar), objc_msgSend, foo); + // Foo bar with selector and msgSend + NSLog(@"Directly call \"bar\" %p through objc_msgSend %p with object foo %p", @selector(bar), objc_msgSend, foo); typedef void (*barfunc)(id, SEL); barfunc bar_ = (barfunc)&objc_msgSend; bar_(foo, @selector(bar)); + + NSString *dummyinput = @"dummy input"; + [foo tobehijacked:dummyinput]; + NSLog(@"The above invocation should be hijacked with input at %p", dummyinput); + + NSString *dateString = @"2024-01-01T00:00:00.000Z"; + + // NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + // [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"]; + // NSDate *date = [dateFormatter dateFromString:dateString]; + + // this is to test the idea for hooking, + // basically, we create a middle-class inherits the class to be used + // + // example using NSDateFormatter: + // - Create a FakeNSDateFormatter inherits NSDateFormatter + // - Have an overloaded function that calls [super inherited] + // - The internal struct class_t has superclass points to NSDateFormatter + // FakeNSDateFormatter *dateFormatter = [[FakeNSDateFormatter alloc] init]; + // [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"]; + // NSDate *date = [dateFormatter dateFromString:dateString]; + // NSLog(@"Test hijacked/hooked Objective-C result %@", date); + + NSLog(@"Selector \"dateFromString:\" using @selector %p", @selector(dateFromString:)); + NSLog(@"Selector \"bar:\" using @selector %p", @selector(bar:)); + NSLog(@"Selector \"dummy\" using @selector %p", @selector(dummy)); + + NSLog(@"[Bar dummy] implementation is at %p\n", [foo methodForSelector:@selector(bar:)]); } - printf("argc=%d\n", argc); + printf("Selector lookup 'dateFromString:' addr: %p\n", sel_lookUpByName("dateFromString:")); + printf("Selector lookup 'bar:' addr: %p\n", sel_lookUpByName("bar:")); + printf("Selector lookup 'dummy' addr: %p\n", sel_lookUpByName("dummy")); + + printf("Test if arguments are passed correctly to main(argc, argv, env)\n"); + printf(" argc=%d\n", argc); for (int i = 0; i < argc; i++) { - printf(" argv[%d]=%s\n", i, argv[i]); + printf(" argv[%d]=%s\n", i, argv[i]); + } + + while (*envp) { + printf(" env[]=%s\n", *envp); + envp++; } return 0; } diff --git a/research/custom_loader/b.cc b/research/custom_loader/b.cc index c6a4e83..e3aba95 100644 --- a/research/custom_loader/b.cc +++ b/research/custom_loader/b.cc @@ -4,6 +4,9 @@ #include #include +// #include +#include + #include "out/b.h" char *pwd; @@ -906,16 +909,16 @@ void build_cache(struct libcache &cache, void *main) { typedef char *(*dyld_get_image_name_t)(int); typedef void *(*dyld_get_image_header_t)(int); - char *dyld_image_count_s = (char*)"__dyld_image_count"; + char *dyld_image_count_s = (char *)"__dyld_image_count"; int (*dyld_image_count_func)(void) = (dyld_image_count_t)find_in_export_trie( libdyld, libdyld_export_trie, dyld_image_count_s); - char *dyld_get_image_header_s = (char*)"__dyld_get_image_header"; + char *dyld_get_image_header_s = (char *)"__dyld_get_image_header"; void *(*dyld_get_image_header_func)(int) = (dyld_get_image_header_t)find_in_export_trie(libdyld, libdyld_export_trie, dyld_get_image_header_s); - char *dyld_get_image_name_s = (char*)"__dyld_get_image_name"; + char *dyld_get_image_name_s = (char *)"__dyld_get_image_name"; char *(*dyld_get_image_name_func)(int) = (dyld_get_image_name_t)find_in_export_trie(libdyld, libdyld_export_trie, dyld_get_image_name_s); @@ -1181,11 +1184,13 @@ void fix(struct libcache &cache) { // for (int i = 0; i < 0x2ac; i++) { // text_start[0xb8c + i] = text_start[0xb8c + i] ^ 0xcc; // } - // vm_protect_func(mach_task_self_func(), (uint64_t)text_start, 0x1000, 0, - // VM_PROT_READ | VM_PROT_EXECUTE); fix_objc(libfixing, cache); fix_initializer(libfixing, cache); + + // _TEXT must be RX or RW no RWX + // vm_protect_func(mach_task_self_func(), (uint64_t)text_start, 0x1000, 0, + // VM_PROT_READ | VM_PROT_EXECUTE); } void volatile custom_initializer(int argc, const char *const argv[], @@ -1264,7 +1269,199 @@ void volatile custom_initializer(int argc, const char *const argv[], free(custom_initializer_i); } +void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache); +void run_objc_readclass(struct libcache_item *libfixing, struct libcache &cache); + void fix_objc(struct libcache_item *libfixing, struct libcache &cache) { + printf("[+] dealing with Objective-C\n"); + fix_objc_classdata(libfixing, cache); + run_objc_readclass(libfixing, cache); +} + +void test_objc_hijack(void* self, void* selector, void* input) { + printf("[Foo tobehijacked] function is HIJACKED\n"); + printf("arg1=%p arg2=%p arg3=%p\n", self, selector, input); +} + +// a subroutine to perform hooking of fixed-binary classes +// by iterating in the __objc_classref which internally points to +// __objc_data for a list of _class_t structs +// each _classt_t has a _class_ro_t containing pointers to +// the components of an instance, including methods, properties, ivars, ... +// +// in this function, we only work on hooking/hijacking of class methods +// by fixing the method list which to be read by Objective-C runtime during readClass +// the method list is a list of {selector, type, implementation} (all pointers) +// by fixing the implementation (should point to a function) the readClass +// thinks that it is the function associated with the method name/selector +// +// by now, all rebases have been rebased and pointers should be pointing correctly +// however, selectors are to be constructed, unless erased +void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache) { + void *header = libfixing->header; + const uint32_t magic = *(uint32_t *)header; + char *ptr = (char *)header; + if (magic == magic64) { + ptr += 0x20; + } else { + ptr += 0x20 - 0x4; + } + + const uint32_t ncmds = *((uint32_t *)header + 4); + char *command_ptr = ptr; + + uint64_t linkedit_vmaddr; + uint64_t linkedit_fileoffset; + uint64_t slide; + + uint64_t methlist_start; + uint64_t methlist_size; + + uint32_t libsystem_hash = + calculate_libname_hash(&cache, "/usr/lib/libSystem.B.dylib"); + typedef void *(*vm_protect_t)(void *, uint64_t, uint64_t, int, int); + typedef void *(*mach_task_self_t)(); + mach_task_self_t mach_task_self_func = + (mach_task_self_t)custom_dlsym(&cache, libsystem_hash, "_mach_task_self"); + vm_protect_t vm_protect_func = + (vm_protect_t)custom_dlsym(&cache, libsystem_hash, "_vm_protect"); + + for (int i = 0; i < ncmds; i++) { + const uint32_t cmd = *((uint32_t *)ptr + 0); + const uint32_t cmdsize = *((uint32_t *)ptr + 1); + if (cmd == LC_SEGMENT_64) { + char *name = (char *)((uint64_t *)ptr + 1); + uint64_t vmaddr = *((uint64_t *)ptr + 3); + uint64_t fileoffset = *((uint64_t *)ptr + 5); + // this assumes that __TEXT comes before __DATA_CONST + if (custom_strcmp(name, "__TEXT") == 0) { + slide = (uint64_t)header - vmaddr; + + uint64_t nsect = *((uint32_t *)ptr + 8 * 2); + char *sections_ptr = (char *)((uint32_t *)ptr + 18); + for (int sec = 0; sec < nsect; sec++) { + char *secname = sections_ptr; + // to be able to fix method list for hooking, we need this section + // to be writable + if (custom_strncmp(secname, "__objc_methlist", 16) == 0) { + uint64_t addr = *((uint64_t *)sections_ptr + 4); + uint64_t size = *((uint64_t *)sections_ptr + 5); + + methlist_start = addr + slide; + methlist_size = size; + + printf("setting __objc_methlist to RW: addr=%p size=%x\n", addr + slide, size); + vm_protect_func(mach_task_self_func(), methlist_start, methlist_size, 0, VM_PROT_READ | VM_PROT_WRITE); + } + sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; + } + + } else if (custom_strcmp(name, "__DATA") == 0) { + uint64_t nsect = *((uint32_t *)ptr + 8 * 2); + char *sections_ptr = (char *)((uint32_t *)ptr + 18); + for (int sec = 0; sec < nsect; sec++) { + char *secname = sections_ptr; + // we can iterate in the __objc_data rather than __objc_classref + // classref can also point to outside classes that are imported + if (custom_strncmp(secname, "__objc_data", 16) == 0) { + + // method are splited into 3 kinds, but for simplicity, we think of it as + // 2 kinds: big and small + // our example are small method list, which all pointers are relative and 32-bit + // the size should be 0xc == 12 but we have padding 4-byte 0x0 for some reason? + struct _objc_method { + int32_t sel_offset; + int32_t typ_offset; + int32_t imp_offset; + }; + + // entsize & 0x80000000 is small method kind + // entsize = kind | sizeof(_objc_method) + struct _method_list_t { + uint32_t entsize; // sizeof(struct _objc_method) + uint32_t method_count; + struct _objc_method method_list[]; + }; + + struct _class_ro_t { + uint32_t const flags; + uint32_t const instanceStart; + uint32_t const instanceSize; + uint32_t const reserved; // only when building for 64bit targets + const uint8_t * const ivarLayout; + const char *const name; + struct _method_list_t * baseMethods; + const /*struct _protocol_list_t*/void *const baseProtocols; + const /*struct _ivar_list_t*/void *const ivars; + const uint8_t * const weakIvarLayout; + const /*struct _prop_list_t*/void *const properties; + }; + + struct _class_t { + struct _class_t *isa; + struct _class_t * const superclass; + void *cache; + void *vtable; + struct _class_ro_t *ro; + }; + + uint64_t addr = *((uint64_t *)sections_ptr + 4); + uint64_t size = *((uint64_t *)sections_ptr + 5); + struct _class_t *data_ptr = (struct _class_t *)(addr + slide); + + for (int nclass = 0; nclass < size / sizeof(struct _class_t); nclass++, data_ptr++) { + // ro can be null for some reasons + // baseMethods is null if the class is a metaclass + if (!(data_ptr->ro && data_ptr->ro->baseMethods)) { + continue; + } + const char* class_name = data_ptr->ro->name; + struct _method_list_t* methods = data_ptr->ro->baseMethods; + for (int i_method = 0; i_method < methods->method_count; i_method++) { + // have to use reference because the relative offset is calculated with the variable address + // if not using reference, then the variable will be a COPY value and the address is localized + struct _objc_method* method = &methods->method_list[i_method]; + if (methods->entsize & 0x80000000) { + const char* imp = *(char**)((char*)(&method->sel_offset) + method->sel_offset); + if (custom_strcmp(class_name, "Foo") == 0 && custom_strcmp(imp, "tobehijacked:") == 0) { + // char* current_imp = (char*)(&method->imp_offset) + method->imp_offset; + + // encode the relative pointer + uint64_t replace = (uint64_t)test_objc_hijack; + uint64_t original = (uint64_t)&method->imp_offset; + printf("modify the Objective-C method at %p\n", &method->imp_offset); + if (replace > original) { + method->imp_offset = (int32_t)(replace - original); + } else { + method->imp_offset = -(int32_t)(original - replace); + } + } + + printf(" method=%p\n", method); + printf(" sel=%x --> %p\n", method->sel_offset, (char*)(&method->sel_offset) + method->sel_offset); + printf(" %s\n", name); + printf(" typ=%x --> %s\n", method->typ_offset, (char*)&method->typ_offset + method->typ_offset); + printf(" fun=%x --> %p\n", method->imp_offset, (char*)(&method->imp_offset) + method->imp_offset); + } + } + } + } + sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; + } + } else if (custom_strcmp(name, "__LINKEDIT") == 0) { + linkedit_vmaddr = vmaddr; + linkedit_fileoffset = fileoffset; + } + } + ptr += cmdsize; + } + + // _TEXT must be RX or RW no RWX + vm_protect_func(mach_task_self_func(), methlist_start, methlist_size, 0, + VM_PROT_READ | VM_PROT_EXECUTE); +} + +void run_objc_readclass(struct libcache_item *libfixing, struct libcache &cache) { // Manually run the Objective-C runtime for each class // @@ -1332,7 +1529,7 @@ void fix_objc(struct libcache_item *libfixing, struct libcache &cache) { uint64_t *data_ptr = (uint64_t *)(addr + slide); uint32_t trie_size; - char* symbol = (char*)"__dyld_get_objc_selector"; + char *symbol = (char *)"__dyld_get_objc_selector"; void *libdyld = cache.libdyld; void *libdyld_export_trie = get_export_trie(libdyld, trie_size); typedef void *(*dyld_get_objc_selector_t)(const char *); @@ -1346,6 +1543,13 @@ void fix_objc(struct libcache_item *libfixing, struct libcache &cache) { const char *name = bshield_data::special_selectors_name[i]; data_ptr[idx] = (uint64_t)dyld_get_objc_selector_func(name); } + + typedef void *(*sel_lookUpByName_t)(const char *); + sel_lookUpByName_t sel_lookUpByName = + (sel_lookUpByName_t)custom_dlsym( + &cache, "/usr/lib/libobjc.A.dylib", "_sel_lookUpByName"); + printf("selector gogogo: %p\n", + sel_lookUpByName("dateFromString:")); } sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; } diff --git a/research/custom_loader/build.sh b/research/custom_loader/build.sh index 491460b..5ea383b 100755 --- a/research/custom_loader/build.sh +++ b/research/custom_loader/build.sh @@ -1,4 +1,4 @@ -# set -ex +set -e VERSION=${1:-14} OUT=./out @@ -75,11 +75,11 @@ clang++ -mmacosx-version-min=$VERSION -o $OUT/libc.dylib -shared c.cc # create our dummy lib first clang++ -mmacosx-version-min=$VERSION -o $OUT/libb.dylib -shared -Wl,-reexport_library out/libc.dylib dummy.cc # build a references libb -clang -fobjc-arc -ObjC -mmacosx-version-min=$VERSION -o $OUT/a -L"./out" -lb a.mm +clang -fobjc-arc -ObjC -mmacosx-version-min=$VERSION -o $OUT/a a.mm # extract symbols from a # ../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports --remove-symbol-table --keep-imports _printf $OUT/a -../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports --remove-symbol-table --remove-others $OUT/a +../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --dylibs=./out/libb.dylib --remove-imports --remove-exports --remove-symbol-table --remove-others $OUT/a ../../macho-go/bin/ios-wrapper bcell2header -b $OUT/b.bcell -o $OUT/b.h # build libb with symbols extracted from a clang++ -mmacosx-version-min=$VERSION -o $OUT/libb.dylib -shared -Wl,-reexport_library out/libc.dylib b.cc From 57b0ae26a7a50f026b8d416fb236d5ee30fab3c4 Mon Sep 17 00:00:00 2001 From: cocay Date: Thu, 28 Mar 2024 01:58:54 +0700 Subject: [PATCH 4/5] fix shellcode x86_64 --- macho-go/pkg/ios/macho/objc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macho-go/pkg/ios/macho/objc.go b/macho-go/pkg/ios/macho/objc.go index a699e79..6431a4c 100644 --- a/macho-go/pkg/ios/macho/objc.go +++ b/macho-go/pkg/ios/macho/objc.go @@ -482,7 +482,7 @@ func (mc *MachoContext) ReworkForObjc() { } } - encode_movz((data_end - text_start) + (shellcode_size - len(shellcode_start))) + encode_movz((data_end - text_start) + (shellcode_size - len(shellcode_start))+3) shellcode_offset = text_start - shellcode_size shellcode_bytes := append(shellcode_start, offset...) From 06525b8a5eec5963d6c6a7e9ce9ab09e8f6ed20e Mon Sep 17 00:00:00 2001 From: cocay Date: Thu, 28 Mar 2024 01:59:55 +0700 Subject: [PATCH 5/5] add method 1 hooking for x86_64; method 3 first commit --- research/custom_loader/a.mm | 4 +- research/custom_loader/b.cc | 228 ++++++++++++++++++++++++------ research/custom_loader/build.sh | 19 ++- research/custom_loader/hooking.mm | 31 ++++ 4 files changed, 233 insertions(+), 49 deletions(-) create mode 100644 research/custom_loader/hooking.mm diff --git a/research/custom_loader/a.mm b/research/custom_loader/a.mm index 3eb2ec8..c5407ea 100644 --- a/research/custom_loader/a.mm +++ b/research/custom_loader/a.mm @@ -10,11 +10,11 @@ void* sel_lookUpByName(const char* name); @implementation Foo - (void)bar { - NSLog(@"Invoke instance method %@", self); + NSLog(@"Invoke instance method original bar in Foo"); } - (void)tobehijacked:(NSString*)input { - NSLog(@"Invoke tobehijacked method %@", input); + NSLog(@"Invoke tobehijacked method %@ from Foo", input); } @end diff --git a/research/custom_loader/b.cc b/research/custom_loader/b.cc index e3aba95..c0bab50 100644 --- a/research/custom_loader/b.cc +++ b/research/custom_loader/b.cc @@ -3,6 +3,7 @@ #include #include #include +#include // #include #include @@ -11,6 +12,8 @@ char *pwd; uint32_t pwd_len; +clock_t start, end; +#define ISARM(header) ((*((uint32_t *)(header)+1) & 0xff) == 0xc) int custom_strcmp(const char *p1, const char *p2) { const unsigned char *s1 = (const unsigned char *)p1; @@ -816,6 +819,7 @@ void test(struct libcache &cache); __attribute__((constructor)) static void bruh(int argc, const char *const argv[], const char *const envp[], const char *const apple[], const struct ProgramVars *vars) { + start = clock(); printf("=== manual symbol bind starts ===\n"); // set_cwd(envp); @@ -931,7 +935,7 @@ void build_cache(struct libcache &cache, void *main) { char *name = dyld_get_image_name_func(i); bootstrap_libcache_item(&cache.libs[i], header, name); cache.libs[i].hash = calculate_libname_hash(&cache, name); - printf("%p %s\n", header, name); + // printf("%p %s\n", header, name); } } @@ -1267,14 +1271,76 @@ void volatile custom_initializer(int argc, const char *const argv[], printf("[+] initializers completed\n"); free(custom_initializer_i); + end = clock(); + double cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC; + printf("restoration library time: %lf\n", cpu_time_used); } void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache); +void fix_class_refs(struct libcache_item *libfixing, struct libcache &cache); void run_objc_readclass(struct libcache_item *libfixing, struct libcache &cache); +// method are splited into 3 kinds, but for simplicity, we think of it as +// 2 kinds: big and small +// our example are small method list, which all pointers are relative and 32-bit +// the size should be 0xc == 12 but we have padding 4-byte 0x0 for some reason? +union _objc_method{ + struct { + const char* name; + const char* types; + void* imp; + }; + struct { + int32_t sel_offset; + int32_t typ_offset; + int32_t imp_offset; + }; +}; + +struct method_t { + const char* name; /* Pointer to name (or selector reference?) */ + const char* types; /* Pointer to type info */ + void* imp; /* Pointer to implementation (code) */ +}; + +// entsize & 0x80000000 is small method kind +// entsize = kind | sizeof(_objc_method) +struct _method_list_t { + uint32_t entsize; // sizeof(struct _objc_method) + uint32_t method_count; + union _objc_method method_list[]; +}; + +struct _class_ro_t { + uint32_t flags; + uint32_t const instanceStart; + uint32_t const instanceSize; + uint32_t const reserved; // only when building for 64bit targets + const uint8_t * const ivarLayout; + const char *const name; + struct _method_list_t * baseMethods; + const /*struct _protocol_list_t*/void *const baseProtocols; + const /*struct _ivar_list_t*/void *const ivars; + const uint8_t * const weakIvarLayout; + const /*struct _prop_list_t*/void *const properties; +}; + +struct _class_t { + struct _class_t *isa; + struct _class_t * superclass; + void *cache; + void *vtable; + struct _class_ro_t *ro; +}; void fix_objc(struct libcache_item *libfixing, struct libcache &cache) { printf("[+] dealing with Objective-C\n"); +#ifdef METH1 fix_objc_classdata(libfixing, cache); +#endif +#ifdef METH3 + printf("METH3\n"); + fix_class_refs(libfixing, cache); +#endif run_objc_readclass(libfixing, cache); } @@ -1365,46 +1431,6 @@ void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache) // classref can also point to outside classes that are imported if (custom_strncmp(secname, "__objc_data", 16) == 0) { - // method are splited into 3 kinds, but for simplicity, we think of it as - // 2 kinds: big and small - // our example are small method list, which all pointers are relative and 32-bit - // the size should be 0xc == 12 but we have padding 4-byte 0x0 for some reason? - struct _objc_method { - int32_t sel_offset; - int32_t typ_offset; - int32_t imp_offset; - }; - - // entsize & 0x80000000 is small method kind - // entsize = kind | sizeof(_objc_method) - struct _method_list_t { - uint32_t entsize; // sizeof(struct _objc_method) - uint32_t method_count; - struct _objc_method method_list[]; - }; - - struct _class_ro_t { - uint32_t const flags; - uint32_t const instanceStart; - uint32_t const instanceSize; - uint32_t const reserved; // only when building for 64bit targets - const uint8_t * const ivarLayout; - const char *const name; - struct _method_list_t * baseMethods; - const /*struct _protocol_list_t*/void *const baseProtocols; - const /*struct _ivar_list_t*/void *const ivars; - const uint8_t * const weakIvarLayout; - const /*struct _prop_list_t*/void *const properties; - }; - - struct _class_t { - struct _class_t *isa; - struct _class_t * const superclass; - void *cache; - void *vtable; - struct _class_ro_t *ro; - }; - uint64_t addr = *((uint64_t *)sections_ptr + 4); uint64_t size = *((uint64_t *)sections_ptr + 5); struct _class_t *data_ptr = (struct _class_t *)(addr + slide); @@ -1420,7 +1446,7 @@ void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache) for (int i_method = 0; i_method < methods->method_count; i_method++) { // have to use reference because the relative offset is calculated with the variable address // if not using reference, then the variable will be a COPY value and the address is localized - struct _objc_method* method = &methods->method_list[i_method]; + union _objc_method* method = &methods->method_list[i_method]; if (methods->entsize & 0x80000000) { const char* imp = *(char**)((char*)(&method->sel_offset) + method->sel_offset); if (custom_strcmp(class_name, "Foo") == 0 && custom_strcmp(imp, "tobehijacked:") == 0) { @@ -1443,6 +1469,18 @@ void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache) printf(" typ=%x --> %s\n", method->typ_offset, (char*)&method->typ_offset + method->typ_offset); printf(" fun=%x --> %p\n", method->imp_offset, (char*)(&method->imp_offset) + method->imp_offset); } + else { + const char* imp = method->name; + if (custom_strcmp(class_name, "Foo") == 0 && custom_strcmp(imp, "tobehijacked:") == 0) { + void* replace = (void*)test_objc_hijack; + printf("modify the Objective-C method at %p with legacy format.\n", &method->imp); + method->imp = replace; + } + printf(" method=%p\n", method); + printf(" sel=%s\n", method->name); + printf(" typ=%p\n", method->types); + printf(" fun=%p\n", method->imp); + } } } } @@ -1461,6 +1499,112 @@ void fix_objc_classdata(struct libcache_item *libfixing, struct libcache &cache) VM_PROT_READ | VM_PROT_EXECUTE); } +uint64_t find_replace_cls_refs(struct libcache cache) { + void *header = cache.thislib; + const uint32_t magic = *(uint32_t *)header; + char *ptr = (char *)header; + if (magic == magic64) { + ptr += 0x20; + } else { + ptr += 0x20 - 0x4; + } + + const uint32_t ncmds = *((uint32_t *)header + 4); + char *command_ptr = ptr; + uint64_t slide; + for (int i = 0; i < ncmds; i++){ + const uint32_t cmd = *((uint32_t *)ptr + 0); + const uint32_t cmdsize = *((uint32_t *)ptr + 1); + if (cmd == LC_SEGMENT_64){ + char* name = (char*)((uint64_t*)ptr + 1); + uint64_t vmaddr = *((uint64_t*)ptr + 3); + if (custom_strcmp(name, "__TEXT") == 0) + slide = (uint64_t)header - vmaddr; + + if (custom_strcmp(name, "__DATA") == 0){ + uint64_t nsect = *((uint32_t*)ptr + 8 * 2); + char* sections_ptr = (char*)((uint32_t*)ptr + 18); + for (int sec = 0; sec < nsect; sec++){ + char* secname = sections_ptr; + if (custom_strncmp(secname, "__objc_data", 11) == 0){ + uint64_t addr = *((uint64_t *)sections_ptr + 4); + uint64_t size = *((uint64_t *)sections_ptr + 5); + struct _class_t *data_ptr = (struct _class_t *)(addr + slide); + for (int nclass = 0; nclass < size / sizeof(struct _class_t); nclass++, data_ptr++) { + if (!data_ptr->ro) + continue; + if (data_ptr->ro->flags & 0x01) { continue; } + if (custom_strcmp(data_ptr->ro->name, "Hooker") == 0){ + printf("Found Hooker @ %p\n", data_ptr); + return (uint64_t)data_ptr; + } + } + } + sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; + } + } + } + ptr += cmdsize; + } +} + +void fix_class_refs(struct libcache_item *libfixing, struct libcache &cache) { + uint64_t replace = find_replace_cls_refs(cache); + void *header = libfixing->header; + const uint32_t magic = *(uint32_t *)header; + char *ptr = (char *)header; + if (magic == magic64) { + ptr += 0x20; + } else { + ptr += 0x20 - 0x4; + } + + const uint32_t ncmds = *((uint32_t *)header + 4); + char *command_ptr = ptr; + uint64_t slide; + for (int i = 0; i < ncmds; i++){ + const uint32_t cmd = *((uint32_t *)ptr + 0); + const uint32_t cmdsize = *((uint32_t *)ptr + 1); + if (cmd == LC_SEGMENT_64){ + char* name = (char*)((uint64_t*)ptr + 1); + uint64_t vmaddr = *((uint64_t*)ptr + 3); + if (custom_strcmp(name, "__TEXT") == 0) + slide = (uint64_t)header - vmaddr; + + if (custom_strcmp(name, "__DATA") == 0){ + uint64_t nsect = *((uint32_t*)ptr + 8 * 2); + char* sections_ptr = (char*)((uint32_t*)ptr + 18); + for (int sec = 0; sec < nsect; sec++){ + char* secname = sections_ptr; + if (custom_strncmp(secname, "__objc_classrefs", 16) == 0){ + uint64_t addr = *((uint64_t*)sections_ptr + 4) + slide; + uint64_t size = *((uint64_t*)sections_ptr + 5); + struct _class_t* target_clsref = NULL; + for (int nclass = 0; nclass < size / sizeof(uint64_t*); nclass++){ + target_clsref = (_class_t*)(*((uint64_t *)addr + nclass)); + // printf("Target clasref @ %p: %p\n", (uint64_t *)addr + nclass, target_clsref); + if (custom_strcmp(target_clsref->ro->name, "Foo") == 0){ + // TODO + printf("Target clasref @ %p: %p\n", (uint64_t *)addr + nclass, target_clsref); + *((uint64_t *)addr + nclass) = replace; + printf("New clasref @ %p: %p\n", (uint64_t *)addr + nclass, *((uint64_t *)addr + nclass)); + struct _class_t* hooker = (struct _class_t*)replace; + printf("superclass hooker: %p\n", target_clsref->superclass); + hooker->superclass = target_clsref; + printf("New superclass hooker: %p\n", hooker->superclass); + break; + } + } + + } + sections_ptr += 16 * 2 + 8 * 2 + 4 * 8; + } + } + } + ptr += cmdsize; + } +} + void run_objc_readclass(struct libcache_item *libfixing, struct libcache &cache) { // Manually run the Objective-C runtime for each class // diff --git a/research/custom_loader/build.sh b/research/custom_loader/build.sh index 5ea383b..ba853a1 100755 --- a/research/custom_loader/build.sh +++ b/research/custom_loader/build.sh @@ -1,9 +1,10 @@ set -e - +clear VERSION=${1:-14} +METH=${2} OUT=./out -LOGIC=${2} - +LOGIC=3 +make -C ../../macho-go mkdir -p $OUT echo "using mach-o version $VERSION" @@ -81,9 +82,17 @@ clang -fobjc-arc -ObjC -mmacosx-version-min=$VERSION -o $OUT/a a.mm # ../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports --remove-symbol-table --keep-imports _printf $OUT/a ../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --dylibs=./out/libb.dylib --remove-imports --remove-exports --remove-symbol-table --remove-others $OUT/a ../../macho-go/bin/ios-wrapper bcell2header -b $OUT/b.bcell -o $OUT/b.h + +if [ "$METH" = "METH1" ]; then # build libb with symbols extracted from a -clang++ -mmacosx-version-min=$VERSION -o $OUT/libb.dylib -shared -Wl,-reexport_library out/libc.dylib b.cc -../../macho-go/bin/ios-wrapper pepe -o $OUT/libb.dylib -b $OUT/libb.bcell --remove-imports --remove-exports --remove-symbol-table --remove-others --keep-imports _dyld_get_sdk_version --keep-imports _malloc --keep-imports ___stack_chk_guard --keep-imports _printf $OUT/libb.dylib +clang++ -D $METH -mmacosx-version-min=$VERSION -o $OUT/libb.dylib -shared -Wl,-reexport_library out/libc.dylib b.cc +# ../../macho-go/bin/ios-wrapper pepe -o $OUT/libb.dylib -b $OUT/libb.bcell --remove-imports --remove-exports --remove-symbol-table --remove-others --keep-imports _dyld_get_sdk_version --keep-imports _malloc --keep-imports ___stack_chk_guard --keep-imports _printf $OUT/libb.dylib + +elif [ "$METH" = "METH3" ]; then +clang -mmacosx-version-min=$VERSION -fobjc-arc -ObjC -c -o $OUT/hooking.o hooking.mm +clang++ -mmacosx-version-min=$VERSION -D $METH -c -o $OUT/b.o b.cc +clang++ -fobjc-arc -ObjC -shared -Wl,-reexport_library -o $OUT/libb.dylib $OUT/b.o $OUT/hooking.o +fi # resign codesign --force --deep -s - $OUT/a-fixed diff --git a/research/custom_loader/hooking.mm b/research/custom_loader/hooking.mm new file mode 100644 index 0000000..64af167 --- /dev/null +++ b/research/custom_loader/hooking.mm @@ -0,0 +1,31 @@ +#import +#include +#include + +@interface Hehe : NSObject +- (void)bar; +- (void)tobehijacked:(NSString*)input; +@end + +@interface Hooker : Hehe +@end + +@implementation Hehe +- (void)bar { + NSLog(@"Invoke instance method %@", self); +} + - (void)tobehijacked:(NSString*)input { + NSLog(@"Invoke tobehijacked method %@", input); + } +@end + +@implementation Hooker +- (void)tobehijacked:(NSString*)input { + NSLog(@"Hijacked tobehijacked method %@ from Hooker", input); +} + +- (void)bar { + [super bar]; +} + +@end