[MUSIC]
>> Good morning.
Welcome back on the Millivay stage for the second talk on day three.
We welcome here our speaker,
Gary Ginn and he will talk about demystifying Mach-O.
Please give him a warm welcome and enjoy the talk.
>> Howdy. Everyone can hear me?
Cool. Let's start with who I am.
I am Gary Ginn. I work on
an iOS cybersecurity framework over in states.
This isn't too important though.
Let's talk about what we're going to talk about.
So first, we're going to look at what is Mach-O,
and why you should maybe look at it if you want to.
Then we're going to go into the deep dive into
the file format and the specification for it,
and then in with a little code example about how you
could parse a Mach-O file to get some information from it.
So first, Mach-O is the executable file format for Darwin systems,
think iOS, Mac OS, TV OS, all of those systems.
It's very similar to ELF if you've ever used that for Linux,
but it's a bit different.
It's an architecture independent file format,
and essentially any iOS app or
Mac OS app that you create or want to reverse engineer or look at,
will end up as this file format,
generally speaking on the device or on your machine.
So there's a few reasons why you might want to look at these files.
First is if you want to read executables at runtime,
such as doing metaprogramming on your own code,
or if you want to do integrity checks in your own code,
or maybe you want to do something like Frida and
install trampolines on other people's code,
and that requires being able to parse through these files.
Custom build tooling, so if you want to inject data into
your executables in specific parts of the segments,
so that you can have the loader
automatically map data really quickly for you,
or you can have special hashes for
integrity checks inside your executable files.
Any disassembly or reverse engineering,
you're going to have to look at these things.
Like for example, you want to see what Apple code looks like.
A really good way to do that is to load one of
these files by linking the dial-up into memory,
and then just dumping this executable file and disassembling it yourself,
so you can see what's under the hood for
these types of system libraries that Apple usually hides pretty well.
So yeah, those are various reasons why you
might want to look at these files and how you would get to them.
So it's a binary format,
so as we go through we're going to fill in just this block of bytes to
show what the structure of the file looks like,
and how you might get to various parts of it.
So first, we have to talk about what are called fat binaries.
So fat binaries are essentially a way to contain
multiple executables of different architectures in the same file.
So essentially you have like a program,
and you want it to have x86 and arm64 instructions on it,
so you can run it or maybe 32-bit and 64-bit applications on the same space.
You just put them right next to each other.
Once it's loaded, it only actually loads the architecture for the current machine,
so the loader will know not to pick up the entire file,
but only the one for the current machine you're on.
You can have a fat binary that is only one architecture,
but the existence of the fat header is very optional.
It's not required, but it is possible for it to be there,
even if there's only one architecture.
It is important to note that these are kind of going away.
Fat binaries were used for a very long time.
You still use them on applications you find on the internet for Mac,
but most iOS applications do not use fat binaries anymore
because there was a conflict in the iOS 16 arm64 simulator
that made it so that they couldn't,
essentially there was a naming conflict.
So they had to reintroduce Xe frameworks,
which are essentially a better version of this.
But yeah, this is a way that you can have one file
represent multiple architectures and not have to worry about
giving people the x86 or the arm64 version.
So a fat binary starts with a fat header.
You can define these definitions.
I'll list the headers as we go along.
They're all in the system headers for iOS and macOS and all that.
If the file is a fat binary, it'll start with a fat header,
and you check that by looking at the magic number there.
It gives you both fat magic and fat SIGAM,
so you can determine the Indian-ness of the data
that you're about to look at.
That's very important for the fat header
because it may be produced on a machine completely different.
Like for example, you could be reading this on iOS,
but it was produced on an x86, 64 Intel machine like a Mac.
And so you use that to determine India,
so you can actually read the header correctly.
It then tells you the type of the header,
tells you how many architectures there are.
Each architecture tells you the CPU type and subtype
that it belongs to, as well as the offset into the file
that architecture belongs.
And then it also tells you the size and the alignment.
The size will always be an integer multiple
of the underlying page size of that architecture, right?
So I think for arm64, that's 512 bytes, or yeah, I think so.
Yeah, and then of course, it's a 64-bit variant.
There's also APIs you can use in machine.h
to determine which CPU type belongs on either your machine
or if you want to try and grade-- it's the same thing
that all operating system uses to choose
which architecture to load, but it'll grade slices
and then determine which one to load.
You can dab those available in machine.h.
So if you're opening a framework up on disk
and you want to check which one to use,
you can use those to make sure you're loading the correct one,
or at least looking at the correct one.
You can use lipo and oTool.
These are two little command line utilities
that come in the box.
And you can use them to kind of see fat headers.
And also lipo lets you inject or remove various slices
into a universal or fat binary.
You can see here, we can see the header and all the type
of stuff within it.
So yeah, so this is the structure at this point.
We have a fat header, two fat architectures.
You could say this is 32-bit, 64-bit.
And then we have two Mach-O objects,
which define executables at the same time.
Theoretically, these are two programs,
just different architectures-- same program, two architectures.
So a Mach-O object is then kind of the way
that if you are familiar with how segmentation originally
worked in memory for operating systems,
this influences how this works here.
The object is split into segments,
which are effectively memory ranges that
will exist in virtual memory when your program is running.
Each of those have their own memory protections and a space
that the loader will then map into your process's
virtual memory space.
They all begin with the header, and they're
followed by a list of load commands.
Load commands define everything else about the file.
So once you read the load commands,
you know where everything is.
Each segment can have sections inside of it
so that it can have a little more
information on where things are.
And we'll go through the definition of a lot of these
going through.
We're not going to look at every segment in every section,
because technically, you can make your own segments
and your own sections.
There's also just a lot of them.
You can use O-Tool to look through these on various files
that you already have.
And yeah.
So if we look at some common segments--
so these probably should be familiar.
Page 0 is a null pointer trap.
On Mac, I know different OS is handled a little differently.
But on Darwin, they actually don't map page 0.
They just claim it to have no protections at all,
which that gives you your nice behavior,
where if you try to do reference a null pointer,
it crashes instead of just doing whatever.
And so there might be a change to this.
I remember reading somewhere that they were looking at
actually mapping it.
Not like allocating memory for it, just mapping it,
because apparently there's a null pointer exploit,
because of the fact that it's not actually mapped.
And for 64-bit applications, this
doesn't just map the first page of the process space.
It also maps all of 32-bit application space.
So on 64-bit apps on Darwin, you cannot dereference
a 32-bit pointer.
And it also means you can't load a 32-bit library
into 64-bit space.
So that's a neat little thing they did there.
The text segment is usually the first segment.
It is the first segment, and it always contains the mock
header.
The reason it does that is so that when you're code signing
your stuff, the code signing only looks at the text segment.
So whenever you have signed executables from the iOS app
store or whatever, it's just looking at that first text
segment.
That's the only thing executable that gets code signed.
That also means that if you have data
that you want to be code signed and have integrity protection
on it from Apple systems, you can put it in the text segment.
You can either be a C string, or you can actually just
explicitly tell the compiler, hey,
put this in the text segment.
Watch out, though.
It means it will also be executable.
So I don't know.
Take your risk there.
Data, that's where all your common data, immutable state
will be.
Data const is a bit interesting.
So yeah, the way these protections work is left
is the initial protection.
So you can see on text, it's read execute,
and then right is max protections.
So that's what you can theoretically
upgrade the segment to.
So on Mac, you can make your text segment writable.
Data const is a bit weird.
So data const was a section that was added,
because originally, there was no const data on Darwin
or any Mac system like that.
They just had a section in data that
was assumed const for the global offset table.
That isn't great, because you can just go in and--
for example, an attacker can go in and change the global offset
table, and then suddenly all your calls to any system
functions can be easily detoured.
They kind of fix this by adding data const,
which is also not const.
But instead, what happens is it starts out as read/write.
Then dyld, after it's done dynamic linking,
will make it read-only.
But you can always just go back and make it writable again,
because it has max protections, and dyld is in user space.
So it's not const.
But it's by default, though, const.
However, there is a change happening here.
In iOS 17, they are releasing--
which I should be releasing in like a month or two--
they added this SG read-only flag.
So this flag, when added to a mock header,
it will have the kernel make it actually read-only
after dyld is done.
If you don't want that behavior for your executable,
you can remove the flag.
But that's something you notice.
Or maybe if you're trying to hook another executable,
look for that flag, because it might break all your hooks.
But it will actually be const, or closer to const, in iOS 17.
And then there you have things like your global offset table,
your Objective-C class list, and const data, that kind of stuff.
And then link edit is at the end of most executables.
And it's just linker data.
It's where dyld will get its operation codes,
because dyld has its own special opcodes that
are used to define how to update the various dynamic linking
in the file.
So yeah, so let's look at what an actual mock header looks
like.
First, you have the magic number to make sure it is correct.
Similarly, it has an Indianness check in it.
So you can check if it's MHMagic or MH.GAM.
And it again contains the architecture info, the file
type-- we'll look at those in just a second.
It also contains the number of load commands
and the size of the entire list of commands.
And that's because load commands are variable size, which
we'll get into in a bit.
That has some annoying implications, but yeah.
And then it also has some flags on it.
That's where that SG read-only thing--
this is where this would be.
There's a lot of flags, and they're mostly used by the linker.
But here are some ones that you'll probably
see if you look poke around.
No-on-defs means that it's fully linked
and doesn't need to be dynamically linked.
Py-- every executable should have py.
That means position independent.
Don't write position-dependent executables.
That's really not great.
Has objective C just means it has objective C.
dYLD means use dYLD.
And toLevel is a feature where dYLD will create namespaces
for the libraries that you link against so that there's not
naming collisions between symbols.
I think that's on by default, but don't quote me.
Here are the various file types that are known.
Assumably, Apple might have some more that they just
don't publicly define.
Object-- that's like when you compile a C file.
It's an intermediate object representation of machine code.
Execute is just an executable.
FVM lib is an obsolete VM file for in the old days.
Core is for when you get core dumps.
So if you ever want to investigate core dumps,
those are also mock-o files.
That's how Apple gets those crash reports on them.
Preload is also an obsolete one.
Dylib is how you determine a dynamic library
that wants to be loaded.
These have to be loaded.
So think libObjectiveC.dylib or whatever.
Those are all Dylib files, Dylib executables.
They can only be loaded via load commands
inside another executable or through an explicit call
to DL open inside a running process.
Dylinker is a special executable file that is a dynamic linker.
You can make your own and have executable programs
point to a unique dynamic linker if you don't want to use
DYLD for whatever reason.
Image bundle-- that one is for resource bundles.
Those actually can only be opened via bundle APIs in code.
You can't open them via the loader directly.
dsim is for dsim files.
And Dylib stub-- that one's an old one.
It's not really used anymore.
Essentially, Apple doesn't want to give out
its source code and Xcode.
So the way they do that-- they used to do that--
was to create empty executable files that
just had symbol tables.
So you could say, oh, yes, link me to this Dylib.
And then on Mac, on your devolving machine,
it would have no code, but it would link correctly.
And then on the phone, it would work.
They've replaced that with this new text-based definition file,
which you won't get into.
So you probably won't see that around.
If you wanted to try and poke around Xcode
to see what APIs are available, you'll
see a lot of these text-based definition files
and some of these Dylib stubs, which are just essentially
just empty files and lists of names.
And yeah, and then kex bundles for macOS kernel extensions.
So yes, this is kind of what we're looking at now.
Each individual object has these load commands.
And we know that there's segments somewhere spread
about, but we don't know where they are yet,
and we don't know how big they are,
and we don't know what kind of memory protections they have.
But we can figure that out by looking at-- but we do
know where these load commands are,
and that's where we're going to find out
all this extra information.
So a load command defines the various segments and sections,
and also other various things, such as OS version and name
and all types of stuff.
There's a lot of load commands.
There are a variable size.
As you can see, the base load command
is just a type and a size.
That also means that you can't index into load commands.
You have to go one by one and look
at each individual load command as you go through.
So whenever you're parsing one of these things,
try and make it to where you only have to do it once,
so you don't have to do an n squared algorithm of going
over and over again.
And we're going to look at a few load commands,
different. There are about 47, I think, load commands currently supported, or at least
publicly supported by Apple on Darwin systems. However, you can make your own, and you could
also presumably make, if you make your own dynamic link, you could also use them somehow.
So we'll look at a few common ones, but if you want an exhaustive list, I think loader.h
has most of them, and you can also look at, Jonathan Levin has a good list that's pretty
exhaustive of which ones are actually used. So first we'll look at probably one of the
more important ones, which is the segment command. There's 64-bit versions of a lot
of these commands, which is _64. You'll see this pattern a lot when looking at load commands,
where it's basically just C-style inheritance, where you have, it's the same as the load
command in the first two fields, and then just adds a bunch of stuff. So first it starts
with the segment name, which is a 16-byte string. That's where you get things like text,
data, data const. So that's where you'll be looking for those things if you're looking
for a specific segment. Then you get the address and size and memory. Note that if you're reading
from file, these are zero, or at least I wouldn't trust the values if you find them here. These
are provided by the loader as it loads it into process space. So if you're reading it
off a file, this will probably be zero, or maybe some other garbage value. And if you're
reading it off a file, you're going to want to use file offset and file size. That tells
you where in the file and how big it is. I also think there might be a potential difference
between file size and VM size, as I think VM size will always be page-aligned. Well,
I don't know if file size is guaranteed to be that. Then you have your maximum and initial
memory protections. And then, of course, you have something called sections. So each segment
is allowed to define sections within itself. The sections are more logical than necessarily
physical, but they are there to help define and separate the segment into logical sections.
If the insects flag is greater than zero, then that's how you know there will be that
many sections immediately after this segment command. So it's immediately after the command.
It's not located somewhere else. It's just right after it. And that's where you get these
section commands. A section has a 16-byte name as well. And also, you can see it has the
address that it's currently located at, the size of bytes in the section, as well as the
offset. Similarly, address may not be filled if you're reading it from disk. Use offset.
The alignment, the relocation entries, all that kind of simple stuff. The flags are pretty
interesting, but essentially the 8 LSB determines the type. So, for example, you could have
a section of C string literals. This is very helpful when you're trying to parse because
it tells you how to actually interpret the section. So a C string literal section is
literally a list of null terminated C strings. So when you know where that section is, you
can just build an array of strings pretty easily off of it. There's various types. You
can find them in loader.h. The other bits are used for various attributes such as you
could say it's a debug section, which means it has debug symbols included inside it. And
also, sections are naive in that there is no checking to make sure sections make sense.
You can have the bounds of a section are not guaranteed to overlap or not overlap with
any other section. They're not guaranteed to actually be ordered. There's no checking.
It's just off of good faith. So there might be an exploit there. I don't know. Maybe you
all can think of it. But there is no checking there. You can probably come up with some
things to fuck with sections. Or at least fuck with your disassembler. You can also
create custom sections inside a segment by using encode. You can just say this attribute
like put this section in this segment. This is also how if you wanted to put data in your
text segment, you could create a custom section, put the data in there, and then it will get
code signed. And then you can also create a section via a file by using this linker command
to ld. Yeah, another load command would be lcmain. This literally just tells us where
the main function is. So if you're ever interested in where the entry point--finding entry points
can be kind of hard in code. Luckily for executables, you just look at this guy. It'll tell you
exactly where it is. Yeah, another one is dilinker. This actually specifies the dynamic linker
that the operating system should use when fixing up the executable at runtime. Usually
it's dyld or dyld_simulator if you're working on simulator. But you can always make your
own. I don't know why you would, but you can. And you can always just have your executable
or what executable you're investigating. You can always overwrite this load command with
your dynamic linker, and then you're able to then capture and get the fix-ups. Maybe you
want to see what symbols various objects want to look at or something like that. And then
this is probably the other more common command you'll see is the load command. This is what
loads dynamic libraries into your process space. It uses this dialib struct inside of
it, which is this long thing of various... It's just like a bunch of data structs inside
of each other. It's just nested for you. And it has a command and command size. It just
tells you where it is. There's various versions of this one for all the types you can use
to import a dynamic library, such as weekly or re-exporting and all those things. Yeah,
so this is the basic structure of a MacO file. You have a fat header with various architectures
after it. Each of those architectures has a MacO object that has a header themselves,
followed by a list of load commands inside of the text segment. And those load commands
tell you where all of these segments lie within the object. And then you can use the various
things to go through and look at the sections and look at the various load commands as well.
So to give this a little more concrete example, I wrote up a little program that will extract
the Objective-C class hierarchy from any module loaded into a process space. And we'll just
go run through that so you can see what it looks like. We're assuming 64-bit because
I don't think Apple supports 32-bit since 2017, 2018. And also, I removed some error
checkings to make sure that it's not too verbose. So yeah, we're going to look at all of those--even
all the system modules loaded into just a basic Hello World application. And we're going
to take a look at what their Objective-C class hierarchy is by just looking at the executable
images that were loaded by the system. And for reference, Objective-C only has single
inheritance and the root object is NSObject. So here's our main function. This first bit
here is just some way so that it didn't take forever. It's just parallelizing it, so don't
worry about it. The first important bit is that we do want to see every mock image loaded
into our process and we do that with doild register func for add image. What this will
do is it--when we first call it, it will use this callback we give it and call it for every
single image that has been loaded as of yet into the process. That includes our own image
as well. Then after it is done doing that, it will go to sleep and then any time a new
image is loaded into the process in the future, it will also call into our callback to let
us know this happened. So if you ever want to monitor what images are being loaded in
and out of your process, this is a really good way to do that. Maybe you're worried
about someone injecting themselves in there or something like that. But yeah, this is
a good way to do that is to use this doild API. So in our callback, this is what the
callback signature looks like. It gives us a mock header and the slide. The slide is
referring to address layout space randomization. Essentially, whenever the OS loads you into
memory, it chooses a random place to just plop you so that it's harder to determine
where your code will be so that things like stack overflows and buffer overflows are less
exploitable. Note that the mock header here is technically a 32 bit one. I don't know
why they did the API like this, but it only gives you a 32 bit one. Luckily, you can just
cast it to 64 bit and it works fine. Actually, you kind of have to. Yeah, so next we will
just go into here. We're not going to look at the export hierarchy code because it's
boring. So we'll just go straight into the actual mock O parsing. So at the beginning
we're just doing normal Ejective-C initialization stuff. This is where you can see I'm just
casting it to the 64 bit mock header. You just have to remember to do that. If you're
doing things in production, you would probably have a preprocessor defined to make sure you're
choosing the correct headers here. That's actually pretty common. It's just redefine
all of Apple's structs to be based off of preprocessor definitions so that you always
get the 64 bit or 32 bit version. Here we're checking that file type. We only care about
dialibs. We're not going to look at bundles or executables or anything like that. Note
that we don't need to actually check for Indianness because it's loaded into our process space
and so hopefully it's the same Indianness as our process. Also, we're not looking at
the fat header because it's also loaded into our process space. We don't need to know which
architecture we're looking at. Once again, to reiterate, if you are looking at fat headers,
you do need to check that. If you're looking at architectures that you're unsure of, you
really should check it so that you can swap. Yeah, and then we go to parse the binary.
So this is kind of the basic iteration loop of parsing the binary. First, we grab the
load command which is right after the mock header so we just take the next element after
that and cast it. Then as we iterate, we just double check that our current command as a
byte location is less than our size of commands. Then starting at the beginning of the first
load command and then to iterate at the bottom, we just add the command size from each particular
command. Yeah, and then at the end we return nil if we return true if we parsed it correctly
and all that stuff. So here's the inside of the loop. You'll almost definitely use a switch
structure just because it's convenient because you're going to be checking IDs of load commands
so it makes sense. So in this first case, we're going to see if it's a dialib and if
it is a dialib, we're just going to grab its name or this grabs the name of the dialib
that was loaded. So this lets us keep track of which images are which. Here we're grabbing
the segments of that image and trying to see which one. If it's a data const section, then
we move on because that's where the Objective-C class list is. We then iterate through all
the sections. So this is a very similar structure as to integrate through the load commands
where we just move past each section until we find the Objective-C class list, in which
case we can then parse that, which we can see here. Then when we're inside the Objective-C
class list, this is like a literal array of Objective-C class objects that the runtime
has already populated with the correct data since we're loaded into memory. So because
of that, we can actually just use the Objective-C runtime functions on these byte values that
we found in this list. There's a million ways you could have done this. You could have parsed
the Objective-C class object. You could have done a lot of things. I thought this was the
simplest. So we just use class get name and class get superclass to get all of the names
of the various classes we're trying to look at. Then we just return it. And yeah, that's
basically the entire parsing. Here's a little test framework I made to verify that this
worked. And you can see from this you can actually get a decent amount of structural
information, but this is a very simple case. Here is the shared with you framework. This
is how they do-- this is the framework that is used for screen sharing or the activity
controller where you do the little shared communication stuff. It's not too complicated,
it looks like. It just has a little bit of a radial thing here, not too many clusters
as you would normally see in an Objective-C stuff. Here is CloudKit. It was a bit too
big, but if we zoom in a little bit, we can see right here, that's where all of the database
operations are. So you can check this out there. And I wanted to do core data, but it
got really gnarly. Let's see if I can get this video working. Yeah, it was just-- there
was too many things in there, but you can even see there's a guy freaking out in the
middle. I think that's NSObject. And yeah. But yeah, so that was the little thing there
just as an example of what you could do with it. I'm sure you all are more creative than
me, so you can figure out other cool stuff. Yeah. So for resources on this, there are
the various headers I was referring to throughout Loader, FAT, Arch, and Machine. OTool and
LiPo are really helpful for working these types of files. And as for books, Jonathan
Levin has great books on all of this. In fact, he probably is the person for the definition
of-- I mean, he's the documentation for Apple and he doesn't work there. So star OS internals
volume one has all of this information and more. So I highly recommend it if you're interested
in reverse engineering Apple systems. And yeah. Any questions or-- yeah.
[ Applause ]
>> Other questions?
>> Hi. Thank you. That was very interesting. Earlier in the talk, you mentioned about Apple
code signing some parts of the thing, the text section.
>> Yeah.
>> And I think maybe I misunderstood you, but you said like it only signs the first
part. There was something like that.
>> So when Apple-- when you code sign to be released on the app store or so that your
Mac OS app doesn't require them to have to like click through three menus to be loaded,
it doesn't code sign the entire executable. It only code signs the text segment. So that
includes the mock header and then all your code and then anything you want to put in
there like C strings or whatever. They don't do the entire executable. I'm not exactly
sure why, but that's currently how it works. It's also what is encrypted by the iOS store.
So you can actually-- if you get iOS apps downloaded from the store, there's an old
iTunes IT app for Windows that lets you download IPAs from the app store directly onto a Windows
PC. You can actually look at their executables and you won't see their code because it's
encrypted. But that text segment will be code signed and encrypted but nothing else will.
Yeah.
>> So if I were to rewrite some parts of the Mac OS actions, would I need to re-sign the
binary then because the signer signature wouldn't match, right?
>> Yeah. So if you are using build tooling to do that, you will have to re-sign it. If
you're building locally, that's not a huge issue. You can just use the code sign tool
on command line. It does get a little more troubling if you're trying to, I don't know,
break into something, right? Because you don't have their certificate. I would say the easiest
way to fix that would be to jailbreak the phone you're on and then you don't have to worry
about it. Yeah, that's probably the easiest way to do it. Or there might be a way to do
it also using the debugger. However, I don't know the exact way you would do that. I know
that Frida is able to get away with breaking code signing using the debugger. So maybe
look at the Frida tool, which is also a very great tool for this kind of stuff.
>> Thank you. >> Yeah.
>> Are there further questions? Okay. That seems not to be the case. And you now all
know how these files work and have fun with some. And we thank Gary again.
[ Applause ]
[ Music ]