Skip to content

Commit 9d0e2e4

Browse files
feat(discovery): three-tier IL2CPP library location for containers
1 parent 4927376 commit 9d0e2e4

1 file changed

Lines changed: 224 additions & 36 deletions

File tree

src/jni_entry.c

Lines changed: 224 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@
3737
#include <sys/types.h>
3838
#include <sys/syscall.h>
3939
#include <dlfcn.h>
40+
#include <link.h>
41+
#include <elf.h>
4042
#include <limits.h>
4143

4244
#include "log.h"
4345
#include "il2cpp_api.h"
4446
#include "il2cpp_dump.h"
47+
#include "elf_sym_find.h"
4548

4649
#ifndef PATH_MAX
4750
#define PATH_MAX 4096
@@ -214,7 +217,9 @@ static void read_package_name(char *buf, size_t sz) {
214217
* ActivityThread.currentApplication().getFilesDir().getAbsolutePath()
215218
*
216219
* The call may block until the Application object is created (up to
217-
* 30 seconds).
220+
* 30 seconds). Any pending JNI exceptions are explicitly cleared
221+
* after each call to avoid spurious failures in certain container
222+
* environments.
218223
*
219224
* @param out_buf Buffer to receive the path.
220225
* @param out_sz Size of out_buf.
@@ -261,6 +266,9 @@ static int get_files_dir_jni(char *out_buf, size_t out_sz) {
261266

262267
cls_at = (*env)->FindClass(env, "android/app/ActivityThread");
263268
if (!cls_at) { CLR_EXC(); goto jni_done; }
269+
/* Avoid spurious failures in some
270+
container configurations. */
271+
CLR_EXC();
264272

265273
mid_ca = (*env)->GetStaticMethodID(env, cls_at,
266274
"currentApplication", "()Landroid/app/Application;");
@@ -327,62 +335,242 @@ static int get_files_dir_jni(char *out_buf, size_t out_sz) {
327335
}
328336

329337
/* -------------------------------------------------------------------------
330-
* Locate libil2cpp.so in the process memory map
338+
* IL2CPP library discovery – three‑tier strategy
331339
* ------------------------------------------------------------------------- */
340+
332341
/**
333-
* @brief Parse /proc/self/maps to find the base address and path of
334-
* libil2cpp.so.
335-
*
336-
* The function searches for a mapping where the file offset is 0
337-
* (the first loadable segment) and the path ends with "libil2cpp.so".
342+
* @brief Quick ELF magic check at a given address.
338343
*
339-
* @param out_path Buffer for the absolute path (may be empty).
340-
* @param path_sz Size of out_path.
341-
* @param out_base Receives the mapped base address.
342-
* @return 1 if found, 0 otherwise.
344+
* @param addr potential base address of an ELF image.
345+
* @return 1 if the ELF magic (\x7fELF) is present, 0 otherwise.
343346
*/
344-
static int find_il2cpp_in_maps(char *out_path, size_t path_sz,
345-
uintptr_t *out_base) {
346-
*out_base = 0;
347-
out_path[0] = '\0';
347+
static int is_elf_at(uintptr_t addr) {
348+
if (!addr) return 0;
349+
const unsigned char *m = (const unsigned char *)addr;
350+
return (m[0] == 0x7f && m[1] == 'E' && m[2] == 'L' && m[3] == 'F');
351+
}
348352

353+
/**
354+
* @brief Tier A – scan /proc/self/maps for libil2cpp.so.
355+
*
356+
* Looks for readable entries whose path ends with "libil2cpp.so",
357+
* selects the one with the lowest start address, and verifies the
358+
* ELF signature.
359+
*
360+
* @param out_path buffer for the full path of the library.
361+
* @param psz size of out_path.
362+
* @param out_base receives the detected base address.
363+
* @return 1 on success, 0 on failure.
364+
*/
365+
static int find_il2cpp_by_maps(char *out_path, size_t psz, uintptr_t *out_base) {
349366
FILE *maps = fopen("/proc/self/maps", "r");
350367
if (!maps) return 0;
351368

352-
char line[1024];
353-
int found = 0;
369+
char line[1024];
370+
uintptr_t best = (uintptr_t)-1;
371+
char bpath[PATH_MAX] = {0};
354372

355373
while (fgets(line, sizeof(line), maps)) {
356-
if (!strstr(line, "libil2cpp.so")) continue;
374+
if (!strstr(line, "libil2cpp")) continue;
357375

358-
unsigned long addr_s = 0, addr_e = 0, offset = 0, inode = 0;
359-
char perms[8] = {0};
360-
char dev[16] = {0};
361-
char path[PATH_MAX] = {0};
376+
unsigned long s = 0, e = 0, off = 0, ino = 0;
377+
char perms[8] = {0}, dev[16] = {0}, path[PATH_MAX] = {0};
362378

363379
int r = sscanf(line, "%lx-%lx %7s %lx %15s %lu %4095s",
364-
&addr_s, &addr_e, perms, &offset, dev, &inode, path);
380+
&s, &e, perms, &off, dev, &ino, path);
381+
365382
if (r < 7 || !path[0] || path[0] == '[') continue;
383+
if (!strstr(path, "libil2cpp.so")) continue;
384+
if (perms[0] != 'r') continue;
385+
386+
/* Track the lowest start address = ELF header location */
387+
if ((uintptr_t)s < best) {
388+
best = (uintptr_t)s;
389+
strncpy(bpath, path, sizeof(bpath) - 1);
390+
bpath[sizeof(bpath) - 1] = '\0';
391+
}
392+
}
393+
fclose(maps);
394+
395+
if (best == (uintptr_t)-1 || !bpath[0]) return 0;
396+
if (!is_elf_at(best)) {
397+
LOGW("maps_scan: no ELF magic at 0x%lx (%s)", (unsigned long)best, bpath);
398+
return 0;
399+
}
366400

367-
const char *fn = strrchr(path, '/');
368-
fn = fn ? fn + 1 : path;
369-
if (strcmp(fn, "libil2cpp.so") != 0) continue;
370-
371-
/* The first loadable segment has file offset 0 */
372-
if (offset == 0) {
373-
*out_base = (uintptr_t)addr_s;
374-
strncpy(out_path, path, path_sz - 1);
375-
out_path[path_sz - 1] = '\0';
376-
found = 1;
377-
LOGI("libil2cpp.so: base=0x%lx path=%s",
378-
(unsigned long)addr_s, out_path);
379-
break;
401+
*out_base = best;
402+
strncpy(out_path, bpath, psz - 1);
403+
out_path[psz - 1] = '\0';
404+
LOGI("Tier A found: base=0x%lx path=%s", (unsigned long)best, bpath);
405+
return 1;
406+
}
407+
408+
/* ------------------------------------------------------------------
409+
* Tier B: dl_iterate_phdr
410+
* ------------------------------------------------------------------ */
411+
typedef struct { char path[PATH_MAX]; uintptr_t base; int found; } PhdrCtx;
412+
413+
static int phdr_cb(struct dl_phdr_info *info, size_t size, void *arg) {
414+
(void)size;
415+
PhdrCtx *ctx = (PhdrCtx *)arg;
416+
if (!info || !info->dlpi_name) return 0;
417+
if (!strstr(info->dlpi_name, "libil2cpp.so")) return 0;
418+
419+
uintptr_t base = (uintptr_t)info->dlpi_addr;
420+
421+
/* dlpi_addr is load bias; for p_vaddr==0 SOs it equals the base address.
422+
* For non-zero p_vaddr, add the minimum PT_LOAD vaddr to get the header. */
423+
if (!is_elf_at(base)) {
424+
uintptr_t min_vaddr = (uintptr_t)-1;
425+
ElfW(Half) i;
426+
for (i = 0; i < info->dlpi_phnum; ++i) {
427+
if (info->dlpi_phdr[i].p_type == PT_LOAD) {
428+
uintptr_t v = (uintptr_t)info->dlpi_phdr[i].p_vaddr;
429+
if (v < min_vaddr) min_vaddr = v;
430+
}
380431
}
432+
if (min_vaddr != (uintptr_t)-1)
433+
base = (uintptr_t)info->dlpi_addr + min_vaddr;
434+
if (!is_elf_at(base)) return 0;
381435
}
436+
437+
ctx->base = base;
438+
strncpy(ctx->path, info->dlpi_name, sizeof(ctx->path) - 1);
439+
ctx->path[sizeof(ctx->path) - 1] = '\0';
440+
ctx->found = 1;
441+
return 1; /* stop iteration */
442+
}
443+
444+
/**
445+
* @brief Locate libil2cpp.so via dl_iterate_phdr.
446+
*
447+
* Iterates over all loaded shared objects and finds the one whose
448+
* name contains "libil2cpp.so". It computes the runtime base by
449+
* adding the smallest PT_LOAD vaddr to the dlpi_addr reported by
450+
* the linker (as some containers mangle the reported base).
451+
*
452+
* @param out_path buffer for the library path.
453+
* @param psz size of out_path.
454+
* @param out_base receives the computed base address.
455+
* @return 1 on success, 0 on failure.
456+
*/
457+
static int find_il2cpp_by_phdr(char *out_path, size_t psz, uintptr_t *out_base) {
458+
PhdrCtx ctx;
459+
memset(&ctx, 0, sizeof(ctx));
460+
dl_iterate_phdr(phdr_cb, &ctx);
461+
if (!ctx.found) return 0;
462+
*out_base = ctx.base;
463+
strncpy(out_path, ctx.path, psz - 1);
464+
out_path[psz - 1] = '\0';
465+
LOGI("Tier B found: base=0x%lx path=%s", (unsigned long)ctx.base, ctx.path);
466+
return 1;
467+
}
468+
469+
/**
470+
* @brief Tier C – brute‑force scan of all readable, offset‑0 mappings.
471+
*
472+
* Opens /proc/self/mem, scans every readable region with file offset 0,
473+
* checks for the ELF magic, and then resolves the `il2cpp_domain_get`
474+
* symbol to confirm that the region is indeed libil2cpp.so. This is
475+
* the final fallback and can be slow.
476+
*
477+
* @param out_path buffer for the library path (or a synthetic name).
478+
* @param psz size of out_path.
479+
* @param out_base receives the detected base address.
480+
* @return 1 on success, 0 on failure.
481+
*/
482+
static int find_il2cpp_by_brute(char *out_path, size_t psz, uintptr_t *out_base) {
483+
FILE *maps = fopen("/proc/self/maps", "r");
484+
if (!maps) return 0;
485+
486+
/* Open /proc/self/mem for safe pread()-based magic checks.
487+
* pread() returns -1 on unreadable/XOM pages — never faults. */
488+
int memfd = open("/proc/self/mem", O_RDONLY);
489+
490+
char line[1024];
491+
int found = 0;
492+
493+
while (fgets(line, sizeof(line), maps) && !found) {
494+
unsigned long s = 0, e = 0, off = 0, ino = 0;
495+
char perms[8] = {0}, dev[16] = {0}, path[PATH_MAX] = {0};
496+
497+
int r = sscanf(line, "%lx-%lx %7s %lx %15s %lu %4095s",
498+
&s, &e, perms, &off, dev, &ino, path);
499+
500+
if (r < 6) continue;
501+
if (perms[0] != 'r') continue;
502+
if (off != 0) continue;
503+
if (path[0] == '[') continue;
504+
if (strstr(path, "/dev/") ||
505+
strstr(path, "/proc/") ||
506+
strstr(path, "/sys/")) continue;
507+
508+
uintptr_t base = (uintptr_t)s;
509+
510+
/* Safe ELF magic check via pread() — no fault on XOM pages */
511+
int magic_ok = 0;
512+
if (memfd >= 0) {
513+
unsigned char magic[4] = {0};
514+
ssize_t n = pread(memfd, magic, 4, (off_t)base);
515+
magic_ok = (n == 4 &&
516+
magic[0] == 0x7f && magic[1] == 'E' &&
517+
magic[2] == 'L' && magic[3] == 'F');
518+
} else {
519+
/* Fallback: direct read protected by thread crash handler */
520+
magic_ok = is_elf_at(base);
521+
}
522+
if (!magic_ok) continue;
523+
524+
/* Confirm: probe il2cpp_domain_get via in-memory ELF parse.
525+
* Protected by thread-level crash handler if a bad page is hit. */
526+
uintptr_t sym = elf_sym_find(base, "il2cpp_domain_get");
527+
if (!sym) continue;
528+
529+
*out_base = base;
530+
if (r >= 7 && path[0])
531+
strncpy(out_path, path, psz - 1);
532+
else
533+
snprintf(out_path, psz, "<anon@0x%lx>", (unsigned long)base);
534+
out_path[psz - 1] = '\0';
535+
LOGI("Tier C found: base=0x%lx path=%s", (unsigned long)base, out_path);
536+
found = 1;
537+
}
538+
539+
if (memfd >= 0) close(memfd);
382540
fclose(maps);
383541
return found;
384542
}
385543

544+
/**
545+
* @brief Dispatch: try all three discovery tiers in order.
546+
*
547+
* Tier A – /proc/self/maps with ELF check.
548+
* Tier B – dl_iterate_phdr.
549+
* Tier C – brute‑force scan.
550+
*
551+
* The first successful tier sets `out_path` and `out_base`.
552+
*
553+
* @param out_path buffer for the library path.
554+
* @param path_sz size of out_path.
555+
* @param out_base receives the base address.
556+
* @return 1 if any tier succeeded, 0 otherwise.
557+
*/
558+
static int find_il2cpp_in_maps(char *out_path, size_t path_sz,
559+
uintptr_t *out_base) {
560+
*out_base = 0;
561+
out_path[0] = '\0';
562+
563+
if (find_il2cpp_by_maps(out_path, path_sz, out_base)) return 1;
564+
LOGW("Tier A failed; trying dl_iterate_phdr...");
565+
566+
if (find_il2cpp_by_phdr(out_path, path_sz, out_base)) return 1;
567+
LOGW("Tier B failed; trying brute ELF scan (slow)...");
568+
569+
if (find_il2cpp_by_brute(out_path, path_sz, out_base)) return 1;
570+
571+
return 0;
572+
}
573+
386574
/* -------------------------------------------------------------------------
387575
* Background dump thread
388576
* ------------------------------------------------------------------------- */

0 commit comments

Comments
 (0)