Hot reloading to fix LD_LIBRARY_PATH issues

Where is my libart at?

Since approximately Android 10, libart (Android Runtime) has been moved to the apex (Android Pony EXpress) directory. This was a design decision by Google which allowed them to perform updates to the DVM/ART subsystem without requiring an OEM to push a full update – and leveraging Google Play to push down smaller packages. This is a win for users, the ecosystem, etc - for both speed, security. Though this sometimes presents an issue for non-standard usecases. One of which is some long standing code I use often for debugging and attacking different applications, the native-shim. While it isn’t anything super special, mostly just some boilerplate code and some dlopen calls, it’s an approach I’ve utilized for almost 10 years.

What broke due to APEX?

Nothing really “broke”, just we no longer know where to look for libart. If we dig into the linker code we will see that the default paths generally look like we would expect;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#if defined(__LP64__)
static const char* const kSystemLibDir = "/system/lib64";
static const char* const kOdmLibDir = "/odm/lib64";
static const char* const kVendorLibDir = "/vendor/lib64";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib64";
static const char* const kAsanOdmLibDir = "/data/asan/odm/lib64";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib64";
#else
static const char* const kSystemLibDir = "/system/lib";
static const char* const kOdmLibDir = "/odm/lib";
static const char* const kVendorLibDir = "/vendor/lib";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib";
static const char* const kAsanOdmLibDir = "/data/asan/odm/lib";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib";
#endif

...

static const char* const kDefaultLdPaths[] = {
kSystemLibDir,
kOdmLibDir,
kVendorLibDir,
nullptr
};

This is generally speaking, where we would expect to find all the libraries needed, so utilizing dlopen("libart") seemingly would continue to work. Except now we can see that this is no longer where it is held;

1
2
3
beyond1q:/ # find / -name libart.so
/system/apex/com.android.runtime.release/lib/libart.so
/system/apex/com.android.runtime.release/lib64/libart.so

Fixing our shim

The dead simple fix would be to just ensure the correct path, depending on if the binary is 64 bit or not, is added to our LD_LIBRARY_PATH.

1
beyond1q:/data/local/tmp # LD_LIBRARY_PATH=/system/apex/com.android.runtime.release/lib64/ ./shim ./libHelper.so

While the above would suffice, it’s not exactly great. I’m pretty lazy and will easily forget this the next time around. So let’s make this programmatic which brings us to another minor issue. Prior to exploring this problem, I wasn’t positive you could change the LD_LIBRARY_PATH during execution. Technically, you can in a few different hacky ways, but we will actually opt for not modifying it on the fly, but just setting the variable if needed and re-executing ourselves correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if __aarch64__
#define APEX_LIBRARY_PATH "/apex/com.android.runtime/lib64/"
#else
#define APEX_LIBRARY_PATH "/apex/com.android.runtime/lib/"
#endif

...

char* path = getenv("LD_LIBRARY_PATH");
if(path == NULL || strstr(path, "apex") == NULL) {
if(setenv("LD_LIBRARY_PATH", APEX_LIBRARY_PATH, 1) != 0) {
printf(" [!] Error setting LD_LIBRARY_PATH via setenv!");
return -1;
} else {
printf(" [+] Success setting env - reloading shim\n");
int rc = execv( "/proc/self/exe", argv);
}
}

The above snippet allows us to properly detect which path would be required to find libart, then looks to see if our path contains this already. If not, we set the environment we are currently running in and then re-execute ourselves via execv and retain the original arugments. This will transparently allow all dlopen, dlsym, etc, calls to work as originally intended and have the extra paths we need. This style of code isn’t anything ground shaking, however it took me a while to both track down the issue and figure out a programmatic way of tackling it.

The full commit can be found here for building locally.

Now viola! We can continue running the native-shim as we always have. Have fun doing more unpacking on newer systems.