- Hello everyone and welcome
to Sleight of ARM: Demystifying Intel Houdini.
My name is Brian
and I'll be discussing
Intel's Houdini binary translator.
So what it is, where it's used,
how it works,
security concerns and recommendations.
But before that,
a little bit about myself real quick.
That's me, I'm Brian Hong.
I studied electrical engineering
at Cooper Union,
and I'm currently working as
a security consultant with the NCC group.
Lately I've been performing
a lot of Android pentests,
and sometimes even Android malware analysis.
Besides that, I like to build
random bits of hardware
and also like to reverse engineer
low-level stuff that other people built,
both software and hardware.
So with that, let's get started with Android.
So Android is
one of the largest operating systems
in the world.
And you could write applications for Android,
Android applications using Java and Kotlin.
And you could also,
as well as C and C++ using
their native development kit.
Android was originally designed and built
for ARM devices,
but Google later added support for x86.
And just to note,
there has been out-of-tree support
before that as well,
such as the Android x86 project.
So since then there's been
several Android devices running on x86,
but now there's many too,
which are x86 Chromebooks
and x86 hosts running
commercial Android emulators.
However, apps generally lack support
for x86 still.
And that's because ARM is,
ARM is the primary hardware platform for Android.
And in fact, if you have native components
in your app,
the Play Store only requires the ARM builds.
And because of this,
many native applications don't end up containing
any x86 binaries, only ARM.
So how can x86 devices running Android run
these apps that only contain ARM binaries?
And this would be a great time for me
to introduce you to Houdini.
So Houdini, the topic of this talk,
is Intel's proprietary binary translator
that allows x86 devices to run on binaries.
And it was co-created with Google,
as it was designed to be run
with Android NativeBridge.
We'll get to it in a second.
Houdini is this mysterious little black box.
We don't know how it does.
We don't know how it works
and there doesn't seem to be
any documentation on it.
And it's possible some vendors may be trying
to hide that they're using it.
We'll get to that as well.
And there are three variants,
32-bit x86 implementing 32-bit ARM,
64-bit x86 32-bit ARM,
64-bit x86 64-bit ARM.
Right.
So Houdini can be used in physical hardware
as they were on x86 based mobile phones.
And they are still used in x86 Chromebooks,
which is how we actually got the binaries.
So we could take a look at them.
They're also using
some commercial Android emulators,
such as BlueStacks and NOX.
Well, I don't believe they're enabled by default.
I think, I believe, I remember,
I think there is an option to enable it
in the settings.
And also it's using the Android x86 project,
which can be run on real hardware
or on an emulator.
Okay.
So how does it work?
Houdini is basically a interpreted emulator
for ARM instructions.
What that means is
there's a loop reading ARM instruction,
the opcodes,
and it produces a corresponding behavior in x86.
I just want to make clear
that it does not do just in time compilation.
It doesn't translate nor output
any x86 instruction.
It reads it and then does the behavior.
And Houdini has two main components.
The first, just Houdini, runs executables.
And the second, libhoudini, is used
to load and link ARM shared objects.
So like I said,
the first part, Houdini, runs ARM executables,
both statically and dynamically linked.
When running dynamic binaries,
it actually uses
its own set up pre-compiled libraries
for ARM Android
in addition to the x86 library needed
by the rest of the Android and Houdini itself.
And there's a screenshot
from the Chromebook actually.
You could see that we have a x86 machine,
denoted by x86 at the end.
And the program I'm trying to run, hello static,
is a 32-bit ARM statically-linked ELF binary,
and I run it ./hello_static,
and it just prints "Hello world."
So some of you may have noticed
that I just executed the binary directly
without invoking Houdini.
So you might be wondering,
where does Houdini come in from?
And this is actually a kind of cool feature,
Linux kernel feature called binfmt_msc.
And if you are familiar with it,
I'll give a quick explanation.
Miscellaneous binary format is
a Linux kernel feature
that lets you basically register interpreters
for custom binary formats.
Kind of similar how a shebang works
in Bash or Python programs.
So in our specific case,
our custom binary format is an ARM ELF binary.
So the two screenshots below show
the registered entries
for a static and dynamic ARM ELF binaries.
And you can see that the interpreter is set
to /system/bin/houdini.
So essentially the resulting effect is
when I try to,
when I type in ./hello in batch,
or try to exec this binary,
the kernel compares the magic bytes
by looking at the first end bytes.
If it matches, it turns it into that.
So it actually execs /system/bin/houdini
and it passes my program name
as the first argument.
Right.
So,
now that we know
how the Houdini part is being used,
let's look at the more interesting second component,
which is libhoudini.so.
So libhoudini is itself a shared object for x86,
and it's used to load
other shared objects for ARM,
that's built for ARM.
It was designed to be used
with Android NativeBridge.
So let's talk about that.
So Android NativeBridge is the component
in Android that allows
this binary translation to work.
It is part of the Android runtime
and is the main interface
between our Android side
and our Houdini binary.
So NativeBridge provides an interface for,
for our Android to talk with Houdini.
And I want to point out that
while it might have been designed specifically
with ARM and Houdini in mind,
the interface that it provides can be used
to implement other processor architectures.
For example, running MIPS code on ARM device.
So it's part of Android runtime.
So it's initialized on boot.
And when it, during it,
it checks a system property,
ro.dalvik.vm.native.bridge,
and if it's set to zero it's disabled.
If it's not,
the value is used as the name
of the library to load,
which implements the NativeBridge implementation.
So in our case, that would be libhoudini.so.
Actually a few interesting things about this.
According to a DEFCON talk a few years back,
it seems like BlueStacks renamed libhoudini
to something like lib3btrans.so
or something for some unknown reasons.
And also it looks like Android x86 project uses
their own implementation, libnb.so.
But when you take a look at the source tree,
it's actually just a thin wrapper
that loads and uses libhoudini itself.
Another interesting thing was,
there's a script in the Android x86 project
that enables NativeBridge,
and it downloads libhoudini
from a couple of obfuscated .cn URL shorteners.
And yeah.
It also seems like they move that link
and the related code around a couple of times.
Not sure what's going on in there,
but back on topic.
NativeBridge defines two main callback interfaces.
And just to get off topic again,
I have to talk about the JNI first
before I get into those callbacks.
So Java Native Interface is
a foreign function interface
that enables our JVM code to interact
with the native code and vice versa.
Yeah.
So,
this part is actually not specific to Android
and it is part of Java.
So on the right side,
we see a struct,
the JNI Native Interface.
It's also typed up to...
Its pointer is also typed up to JNIEnv.
So this structure has basically
a bag of function pointers
that's provided to the native code
so that our native code could be,
could use these functions
to perform low-level JVM reflection.
And I cut out a lot of it.
There's a lot of functions functions in there,
but some of them are, you know,
calling methods,
getting the method ID,
allocating object,
getting a field,
finding classes and so on.
So this,
the pointer to this struct has actually passed
as the first argument
when a Java,
when your Java code calls the native code.
And we'll see how that's used later.
So the first callback interface
from NativeBridge is
the NativeBridgeRuntimeCallbacks.
It's quite simple,
but it's passed from NativeBridge
to our libhoudini binary
so that our libhoudini can call,
find and call native functions
in the Android side
or the NativeBridge side.
The second more interesting callback interface is
the NativeBridge callbacks.
And this is quite kind of like the opposite.
It provides a way for our NativeBridge
to call function in our libhoudini binary.
We see some of the functions on the right.
The most interesting of these are
initialize(), loadLibrary(), and getTrampoline(),
the latter two of which are quite similar
to how dlopen and dlsym works.
And I'll show that in a later slide.
Yeah, so this,
this struct is actually exposed
via the symbol NativeBridge ITF,
which can be seen here.
By looking at it
in a hex editor or disassembler,
you could see all the function pointers
in that data structure.
So, I have all of the components
of like NativeBridge explained kind of,
so I'm going to try my best
to kind of put them all together.
So here we go.
So normally it would look something like this.
Here you have an ARM device running ARM Android,
and we want to load a ARM native library.
So when your application launches,
it would call system.loadLibrary,
and which would trigger
the Android runtime to call dlopen,
and that will load our libnative.so
into memory, into the processor.
And then when our app wants to call
a native code, native function,
it will first do a dlsym,
which will give it in turn a function pointer
to our code.
And then it jumps to it
with the first argument being
the pointer to our JNIEnv structure.
And if our native code wants to interact
with the Java world,
it could do so by looking
for the appropriate function
in the JNIEnv, you know, function pointers.
Now this gets a little more complicated
when we talk about NativeBridge.
So before anything happens,
NativeBridge gets loaded on boot, right?
So it checks the system property
and sees that it's pointing
to libhoudini.so.
And it dlopens the libhoudini library.
Our Android and our device is x86,
and so is libhoudini,
and our goal is to run co,
which is an ARM in our libnative.so.
So after that, after libhoudini is loaded,
it fetches the NativeBridge callbacks using
dlsym() and closeinitialize(),
which isn't shown in the diagram.
So after that, you know,
the Android continues to boot up.
And then when you launch your app,
it would try to load the library again
with system.load or loadLibrary(),
which triggers NativeBridge plus Android runtime
to call our NativeBridge callbacks load library,
which acts similar to dlopen().
So we'll turn a handle.
And with the handle,
we could pass it to getTrampoline()
to get a function pointer,
similar to a dlsym().
Now we can't,
we can't actually just use
dlopen() and dlsym() directly
because the kernel will complain
it's a different architecture.
And especially for dlsym(),
because dlsym() will give you a function pointer.
So Houdini has their own versions,
loadLibrary() and getTrampoline().
loadLibrary just opens our native file
and maps it into memory.
And getTrampoline() should return
the function pointer, but it doesn't.
It can't return the actual function pointer
to our native library
because our code is written in ARM,
or it will contain ARM instructions in there,
which our x86 processor probably won't know
how to handle.
So instead libhoudini returns a pointer
to a little step inside of our interpreter,
inside our libhoudini,
so that when we call the,
the function returned by getTrampoline(),
the interpreter is going to start running
and the interpreter will in turn,
start reading the native code
and executing it.
So the last part,
I want to bring up is the JNIEnv pointer.
I mentioned the, that the,
when your Java code calls native code,
the pointer to JNIEnv is passed
as the first argument.
We can't pass that straight through
to our native code
because while our native code is running in ARM
and our JNI functions are in x86.
So what libhoudini does is it just remembers
where it's,
like where it is,
and then it creates its own fake version
that's filled with ARM instructions.
Actually it's filled the,
JNIEnv function pointers point
to a bunch of trap instructions.
That way, when the interpreter sees
those ARM trap instructions,
it knows which of like proper JNI function
to call on the real x86 structure.
All right so,
now that I've kind of explained
how libhoudini comes together with NativeBridge
and how it all fits together,
let's start thinking deeper
into how the interpreter part works,
and starting with memory.
So, the emulation is a dual architecture,
so it contains both separate ARM
and x86 binaries.
And it is a shared virtual address space,
as well as they both have
real world view of memory.
So what that means is
the x86 parts of the process
and the ARM parts of the process view
the memory the same way.
And they're in the same,
they're in the same address space.
So there's no magic translation
between an ARM address versus an x86 address.
And the last point is,
there is a separate allocation for ARM stack.
And just to, just to show,
this is a snippet
from one of the app's process memory,
memory map.
We see here our native libraries loaded up there.
And on down there,
we have our libhoudini loaded.
You could also see
a bunch of ARM libraries loaded
that's used by our native code.
The next one is, yeah.
Specifically libc and a couple of others,
we could see are loaded for both ARM and x86.
And you also see
a bunch of anonymously map pages,
which is used internally by libhoudini.
And our ARM stack lives
somewhere in or around there.
So moving on,
I want to talk about the actual execution loop.
So I mentioned earlier,
it's essentially a switch statement
inside while loop.
So this screenshot shows
the portion of the interpreter
where it would like,
it would fetch,
it would read the instruction,
it would partially decode it,
and then jump to the proper instruction handler.
So in this assembly,
I have the comments on the right,
but I do have a equivalency code
on the next slide.
So basically I'll run through this real quick.
The snippet of code gets the program counter
from the processor state,
reads the instruction from memory,
and then checks the condition bit,
condition field.
Condition code?
Yeah, condition code to determine
whether the current instruction should be
executed or not.
Once it determines that it should,
it calculates this offset
by concatenating bits 20-27
and the bits 4-7.
So that offset is used as the entry offset
into this instruction handler table,
which has filled with
a bunch of function pointers
to instruction handlers.
And then it jumps to it.
So for example,
our "mov r0, r1" instruction has
the entry offset of 0x1A1.
And each entry is a 32-bit address.
So multiply by four bytes,
we get 0x684.
And then, so the final offset,
final address that this function pointer is in,
is 4BC044.
And we could see that right here.
If I look at it through a disassembler.
At that address,
we see a pointer to our function handler,
instr_mov_1.
And just note that
this decompilation is not entirely correct,
but already we could see
in around lines 22, 23 and 27,
we see some registers being moved around.
And even some shifting and masking
because mov instruction has the option
to do that.
The important thing to look at is that the,
all of the instruction handlers have
two parameters that's passed in.
The first is the instruction itself,
the instruction bytes,
so that the handler,
the instruction handler could
pull out the operands
and fully decode it.
And the second argument is the processor state.
And it is basically what it is.
It's this data structure
that keeps track of the ARM,
emulator ARM's processor state,
but mostly continues to register values
such as, you know, "r0, r1,"
but also registers such as
the program counter, stack pointer,
link register, and so on.
It also contains a byte that tells you
whether it's in thumb mode or not,
and there's a bunch of other fields,
but I couldn't really figure out
what all of those do.
Note that this is just
the data structure in memory.
And they have shared memory addresses
between the x86 and the ARM side.
So you can technically just,
if you find that you could write values
to these registers,
to this struct to change and register values
inside of ARM.
So the next thing
I took a look at was the syscalls.
Trying to figure out how syscalls work.
Syscalls adjust instructions as well.
They're special instructions,
but they are instructions.
So we could actually find them
in the instruction handler table.
We could see on the right,
it takes the same parameters,
the instruction and the processor state.
This is also not entirely correct,
the decompilation,
but we could see that it,
it actually doesn't issue any x86 syscalls,
but rather it just sets that SVC number field
in the processor state and returns.
So the actual switch for issuing x86 syscalls
is further down that,
that loop in the interpreter,
and depending on which syscall number it is,
it will do different things.
Most of the time,
it's just simple wrappers or pastors
with some conversion
between like moving the ARM register value
to the actual x86 register
and calling int 80 in x86.
Or just simple, you know, conversions.
But some of them are a little more complicated
than the other.
One such example that was
of interest to us was the clones call.
And I've actually combined fork and clone here
because nowadays if you call fork,
it'll go to libc fork,
which would actually call clone.
So, clone was also very interesting
because it has a parameter there
to pass in called child_stack.
And you pass in a memory region,
which will be used as a child_stack.
And on top of that set stack will be
the return address
so that when the child is cloned,
it'll return and that address becomes
the entry point of our child process.
Now, we were wondering
how that gets handled by the Houdini,
and it turns out the child_stack
we pass in is not passed to the kernel,
but instead libhoudini creates
its own empty RWX page
and passes that as the child stack
and handles the parent and child logic.
So now that we have some ideas
on how it works internally,
let's get to the fun stuff.
So,
detection.
Is there ways we could detect
whether we're running as an app,
we're running inside of libhoudini or not?
We came up with a couple of ways
and the first way would be,
we build an ARM native app.
And in that app,
we could check the host architecture,
either via Java's os.arch system property
or by reading the /proc/cpuinfo.
But as it turns out,
you actually can't do that
because Houdini hides these.
So when you do os.arch,
System.getProperty,
libhoudini makes us say armv7l
from the Java side,
when you're running with NativeBridge.
And when you try to cat /proc/cpuinfo,
it would actually return
ARMv8 processor rev 1 (aarch64).
Actually, if you're careful,
you might be able to tell
whether you're running under Houdini
because there seems to be some inconsistency,
because one of them would've turned on v7,
the other one would've turned on v8,
and hardware says "placeholder," funny enough.
So there are some other ways as well.
So checking the memory map,
you could try to read the memory maps
and see if either libhoudini is loaded
or both ARM and our x86 libraries are loaded.
So these are okay methods,
but we think the best ways are
those that are undetectable itself.
So like no syscall issues,
no files being opened,
anything that would trigger an analysis tool,
you know.
So the method I came up with was using
the JNIEnv function pointer.
So I mentioned earlier,
if you're on a real device,
I mentioned earlier that libhoudini creates
its own ARM version
of the JNIEnv structure function pointers.
Now, if you're on a real device,
those functions pointers would point
to real ARM opcodes.
But if you're running under libhoudini,
the function pointers will point to
also real ARM instructions,
but those would be syscall instructions.
I'll have a quick demonstration of that later.
So the next thing is,
once we detect
that we're running inside libhoudini,
can we escape to x86 with it?
So of course we could call mprotect
and write code to memory.
But again, this isn't very subtle.
We would need to call mprotect,
which would probably trigger
most of the analysis tools.
And another way we could try to do this is
by x86 stack manipulations.
We know approximately where the x86 stack is.
So we could try to clobber the stack
with ROP payloads
and have it jumped to somewhere.
This method is much more annoying,
but one of the harder parts is trying
to figure out
where we could actually run our code.
So we need to find a page
that has execute permissions
or try to find a bunch of,
a lot of raw payloads.
That brings us to security concerns.
It turns out libhoudini uses,
creates a bunch of RWX pages they use internally.
And we see,
we saw one of these for that,
which has been used for the clone's call.
And they have read, write,
and execute permissions,
which means we could write x86 code to them
and just jump to it.
And there are again, shared memory.
So we could write code
from either both x86 side
or from the ARM side.
So just to show you
what some of these are used for,
the ARM JNIEnv,
the ones filled with trap instructions,
isn't there.
The ARM stack is in that memory region.
So, back to security concerns.
We have RWX pages in x86.
So what about trying to get
code execution on ARM?
So it turns out Houdini ignores
this bit entirely.
Yeah.
Which just means,
you could load,
you could write code anywhere and jump to it.
And I don't think I need to explain
why that's an issue,
but yeah,
ARM libraries themselves are loaded
without the execute bit on their pages.
So regarding the behavior ignoring
the non-execute bit,
not that this is correct,
but if you think about it,
this kind of makes sense.
Houdini is an interpreter for ARM.
The interpreter gets the data input.
And that means,
if you could read the data,
read the instructions,
it will run it.
So to demonstrate that,
I got this little program here,
nx stack.c.
And in my main,
I allocate some memory
on the stack code[512],
and then I write two ARM instructions on it.
And then I make that cast them
to function pointers
and jump to it.
And normally on a real device,
real ARM device,
this will cause a SEC fault.
But as we see below, it doesn't.
It just works.
Actually, in the first iteration of this code,
I accidentally have the memory
outside of the function.
So it was in the data section
or some other region,
and it still worked perfectly fine.
Well, this runs fine
with devices running with libhoudini.
So the next step is
a couple of quick demos.
So, for the demo,
this is on the Chromebook.
And for the demo,
I wrote this app.
And I've actually built it,
built two separate versions of it.
It's the exact same source.
I just have two versions of it.
One is built with just x86 libraries,
our native code,
and one only contains the ARM binary.
So the top one is the x86 one
and the bottom one is the ARM.
And the Chromebook itself is x86.
So to run the bottom app,
it is running through libhoudini.
So the first tab is CPU info.
Well,
you know,
overturn the values that I mentioned before,
and the top one doesn't have any libhoudini.
It's running x86 on x86.
So all the values are correct.
We see GenuineIntel.
Everything's all nice.
Whereas on the bottom,
we're running it with libhoudini,
and we saw the output we saw before,
ARMv7, ARMv8.
Inconsistency, as well as hardware = placeholder.
The second tab demonstrates the GetVersion,
the detection method I quickly described,
but here we see on the x86,
when we do reference the GetVersion
and CallStaticMethod functions,
I believe, I mean,
those are valid x86 instructions.
I just,
I think those are a bunch of push instructions
as you've often seen
in the beginning of a function.
And on the bottom,
when we do the same thing, fetch all,
so this is running with libhoudini.
So we will see ARM instruction,
but specifically those instructions, 0xef000,
those are our syscall instructions.
So you could use that as a method
to detect whether libhoudini is running or not.
In this case, yeah.
So the third tab actually is not
for demonstration.
Just the utility to show you
the process's memory map.
So we're in x86.
There's no libhoudini.
And there should,
everything should look fine.
Right.
But when we look at the,
the ARM versions process map,
we see a bunch of anonymous map memory,
we see libhoudini right there.
And we should also see
our ARM libraries loaded in right there.
Okay.
So the,
I think the most interesting tab is the last tab,
the exec,
which demonstrates the NX bit
or the lack of NX bit check
on the ARM side with libhoudini.
So top one is running without libhoudini.
And just to kind of explain to you
what this layout is.
So on the left side,
you will write some bites.
That's going to get written to a buffer.
You're going to type in some bites,
and then you got to click run,
and then it will be passed to a native code
where those bites will be actually written
to memory and then jumped to.
However, the top one is our x86 version,
so obviously we can't run ARM instructions
and there's no libhoudini loaded.
So it's going to crash.
That was the intended behavior.
However, on apps that's running with libhoudini
we could actually just type in
valid ARM instructions, click run,
and it would run.
And I have a couple
of different programs written up there
because I don't want to type it up manually.
Run and then multiply,
and multiply is r1 and r2,
and then adds it to r0.
That's correct.
And getSP actually reads the stack pointer
of the ARM processor and returns it.
And just to show you that this is dynamic,
these bytes are actually being copied,
I could change the actual bytes
of the instruction.
Register 15 will be reading the PC.
I could also modify it.
So the left side is completely changeable.
As long as it's executable ARM instructions,
it will run it.
I changed the one to a two or three
and would add three.
Same thing for adding two integers.
I added three times.
So 2 times 3 plus 6 is 13.
So just for completeness,
I have the same app,
but now it's running on a real ARM device.
So this device happened to be on v8.
So we'll stay on v8 and aarch64 processor.
It's going to all look correct.
There's no libhoudini running on this
because it's an ARM device running ARM code.
So we go to the detect tab.
Let's skip to it.
We go to detect tab.
We see just valid ARM instructions
that are not,
they're not syscalls.
And in the maps, you know,
this should also be fine.
Completely fine.
No Houdini.
No, yeah.
And of course,
this is not running with libhoudini.
This is just running on actual ARM hardware.
So when we try to copy these bytes
into malloc memory
or stack with the heap,
and then jumped to it, it would crash.
And it does.
With demos out of the way,
let's now talk about possibilities
of malware that knows about libhoudini.
To start, we know that
applications are often run
in sandbox environments for analysis.
This is mainly done in one of three ways.
Running them on actual devices would give
the most realistic behaviors,
but it is hard to do on a large scale
and also hard to instrument.
The second best option is
fully virtualized environments, like QEMU.
but these have somewhat,
a performance overhead
since they would have to emulate
the entire hardware and the processor.
And that brings us to our third option,
Android emulators.
And those Android emulators on x86 devices
can use technology like Houdini
to run ARM applications.
This has the least overhead
as would only emulate parts of the application
instead of the whole hardware
and the operating system.
And on another point,
most of you would agree
that inconsistent behaviors are
harder to debug.
And similarly, apps that may
or may not have behaved maliciously are
harder to detect
and are also harder to analyze.
So let's combine those points.
And so for example,
a malware can use one
of those detection methods mentioned previously
to figure out whether or not
it is running with libhoudini.
Then it's possible for them now
to act benevolently when it thinks
it is under analysis
by seeing that libhoudini is being used.
And in other cases,
show malicious behaviors
when libhoudini is not present.
Yeah.
So what about the other way around?
We could also perform malicious actions
only when Houdini is present,
abusing the knowledge of its inner workings
to further obfuscate itself.
And for example,
we don't know what the play store uses nowadays,
but it seems like
their automatic app testing doesn't use,
doesn't run ARM APKs on x86 with libhoudini.
In a case like this,
a malware could detect
that it's running on,
well, it's not under analysis,
and when it is running under libhoudini,
for example, inside an commercial emulator,
then it could do some tricks
like running code from the stack,
which you can't do on a real device.
And trying to analyze that would prove
to be difficult
because a static analysis tool would see that
you write some code onto the stack
and it jumps to it.
And that should crash.
Whereas if you're running on libhoudini,
it works.
So we finally come to the recommendations
and how not to write an emulator.
And we could start by talking
about the RWX pages.
So we noticed that libhoudini internally uses,
well, libhoudini maps a bunch of RWX pages
to be used internally,
and those should not be there.
If it's really necessary,
we recommend performing
a finer-grain page permission control.
And one of those methods would be implementing
an efficient NX implementation.
So,
we understand that checking page permissions
every instruction would incur
a very significant overhead, right?
Every instruction you want to run,
it has to check the page permissions,
via software.
So instead what we could do is,
we keep track of it in a data structure,
and we only check
if the instruction we're currently running is
different than the previous instructions.
So in the case of jumps
or instructions across a page boundary.
We could check those.
So this basically becomes
our userland page table implementation.
Given that,
our recommendation is to just use virtualization.
Simple enough.
But regarding actually implementing
the userland page table via software,
we can do it a couple of ways, right?
We could only trust the text section
of the library on load.
And the other option is
to check the memory map
and every time a new page is added.
And then if a new page is added,
we add that to our data structure
that we keep track of.
And third, we could,
we could hook the memory mapping related syscalls,
and then,
add, whenever, for example,
mmap is called
or mprotect is called
with the execute permissions,
we update our data structure accordingly.
So ideal solution combines the last two,
two and three.
So it will,
yeah, every time you do mmap or mprotect,
for example,
it would add an entry into our data structure
that keeps track of the page permissions.
And just as a catchall,
we could check the memory map
for new pages that's not already in there.
And this has some good side effects,
such as,
we can now,
since we have a userland page table,
we could do dynamic library loading
via dlopen().
And we could also do
legitimate just-in-time compilation.
And of course,
the used JIT pages should be cleared,
properly cleaned up after usage
to prevent page reuse attacks.
And another thing is that, of course,
this data structure is a critical data structure
as it acts as our page table
and should be heavily protected.
So some of the things we mentioned is
writeable only when being updated,
surrounded by guard pages,
not accessible to ARM,
et cetera, et cetera.
And another thing we recommend
for researchers or vendors doing analysis
or Android applications is
when you're running dynamic analysis,
you should also run apps through libhoudini.
As we mentioned,
it's possible for malwares
or any other applications
to behave differently
when they see that libhoudini is enabled.
Also when doing static analysis,
we should look for access
to Houdini RWX pages,
or attempt to execute
from non-executable pages,
which would work
if it was running under libhoudini.
And just to add on that,
anything scanning for JNIEnv function points,
as that was one of our detection methods.
So to summarize,
what I'm trying to say
in this presentation is that Houdini introduces
a couple of security weaknesses
into processes using it.
And that would be
ARM native applications running on x86 devices.
Some of these impact the security
of the emulator ARM code,
such as the NX bit,
the lack of NX bit check,
while some also impact the security
of the host x86 code,
such as the rewrite execute pages everywhere.
Yeah.
And actually think the fact
that Houdini is not well-documented publicly,
nor easily accessible,
has something to do with
preventing wider security analysis
and research into this,
which could have caught these issues earlier.
Yeah.
Which brings us to our few last slides.
I'd like to give big, big, big, special thanks
to Jeff for mentoring this project
and helping develop the methodology.
Also Jennifer for all the support
and research and amazing feedback,
and Effi for basically bootstrapping this research.
And with that,
thanks everyone for joining.
And I believe you're at Q and A right now.