You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
9.9 KiB
251 lines
9.9 KiB
// Script to gather the shared library from disk and also
|
|
// from memory utilizing Frida. After reading the file from
|
|
// disk, it will then compare some sections of the file in
|
|
// order to hunt and identify potentially modified and hooked
|
|
// functions.
|
|
//
|
|
// Re-written over the ages for usage while
|
|
// unpacking Android applications by
|
|
// Tim 'diff' Strazzere, <tim -at- corellium.com> <diff -at- protonmail.com>
|
|
// Based off older code and concepts from lich4/lichao890427
|
|
//
|
|
// Corresponding blog https://corellium.com/blog/android-frida-finding-hooks
|
|
|
|
// Helper function for creating a native function for usage
|
|
function getNativeFunction(name, ret, args) {
|
|
var mod = Module.findExportByName(null, name);
|
|
if (mod === null) {
|
|
return null;
|
|
}
|
|
|
|
var func = new NativeFunction(mod, ret, args);
|
|
if (typeof func === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
return func;
|
|
}
|
|
|
|
var open_ptr = getNativeFunction('open', 'int', ['pointer', 'int', 'int']);
|
|
var read_ptr = getNativeFunction('read', 'int', ['int', 'pointer', 'int']);
|
|
var close_ptr = getNativeFunction('close', 'int', ['int']);
|
|
var lseek_ptr = getNativeFunction('lseek', 'int', ['int', 'int', 'int']);
|
|
|
|
function getElfData(module) {
|
|
console.log('Processing ', module.path);
|
|
if (module.sections) {
|
|
return true;
|
|
}
|
|
|
|
var fd = open_ptr(Memory.allocUtf8String(module.path), 0 /* O_RDONLY */, 0);
|
|
if (fd == -1) {
|
|
return false;
|
|
}
|
|
|
|
// Get elf header
|
|
var header = Memory.alloc(64);
|
|
lseek_ptr(fd, 0, 0 /* SEEK_SET */);
|
|
read_ptr(fd, header, 64);
|
|
|
|
// Allow for both 32bit and 64bit binaries
|
|
var is32 = Memory.readU8(header.add(4)) === 1;
|
|
module.is32 = is32;
|
|
|
|
// Parse section headers
|
|
var sectionHeaderOffset = is32 ? Memory.readU32(header.add(32)) : Memory.readU64(header.add(40)).toNumber(); // For some reason this is read as a string
|
|
var sectionHeaderSize = is32 ? Memory.readU16(header.add(46)) : Memory.readU16(header.add(58));
|
|
var sectionHeaderCount = is32 ? Memory.readU16(header.add(48)) : Memory.readU16(header.add(60));
|
|
var sectionHeaderStringTableIndex = is32 ? Memory.readU16(header.add(50)) : Memory.readU16(header.add(62));
|
|
|
|
var sectionHeaders = Memory.alloc(sectionHeaderSize * sectionHeaderCount);
|
|
|
|
lseek_ptr(fd, sectionHeaderOffset, 0 /* SEEK_SET */);
|
|
read_ptr(fd, sectionHeaders, sectionHeaderSize * sectionHeaderCount);
|
|
|
|
var stringTableOffset = is32 ? Memory.readU32(sectionHeaders.add(sectionHeaderSize * sectionHeaderStringTableIndex + 16)) : Memory.readU64(sectionHeaders.add(sectionHeaderSize * sectionHeaderStringTableIndex + 24)).toNumber();
|
|
var stringTableSize = is32 ? Memory.readU32(sectionHeaders.add(sectionHeaderSize * sectionHeaderStringTableIndex + 20)) : Memory.readU64(sectionHeaders.add(sectionHeaderSize * sectionHeaderStringTableIndex + 32)).toNumber();
|
|
|
|
var stringTable = Memory.alloc(stringTableSize);
|
|
lseek_ptr(fd, stringTableOffset, 0 /* SEEK_SET */);
|
|
read_ptr(fd, stringTable, stringTableSize);
|
|
var sections = [];
|
|
|
|
var dynsym = undefined;
|
|
var dynstr = undefined;
|
|
var relplt = undefined;
|
|
var reldyn = undefined;
|
|
|
|
for (var i = 0; i < sectionHeaderCount; i++) {
|
|
var sectionName = Memory.readUtf8String(stringTable.add(Memory.readU32(sectionHeaders.add(i * sectionHeaderSize))));
|
|
var sectionAddress = is32 ? Memory.readU32(sectionHeaders.add(i * sectionHeaderSize + 12)) : Memory.readU64(sectionHeaders.add(i * sectionHeaderSize + 16)).toNumber();
|
|
var sectionOffset = is32 ? Memory.readU32(sectionHeaders.add(i * sectionHeaderSize + 16)) : Memory.readU64(sectionHeaders.add(i * sectionHeaderSize + 24)).toNumber();
|
|
var sectionSize = is32 ? Memory.readU32(sectionHeaders.add(i * sectionHeaderSize + 20)) : Memory.readU64(sectionHeaders.add(i * sectionHeaderSize + 32)).toNumber();
|
|
|
|
if (['.text', '.rodata', '.got', '.got.plt'].includes(sectionName)) {
|
|
var section = {};
|
|
section.name = sectionName;
|
|
section.memoryOffset = sectionAddress;
|
|
section.fileOffset = sectionOffset;
|
|
section.size = sectionSize;
|
|
if (sectionSize > 0) {
|
|
section.data = Memory.alloc(sectionSize);
|
|
lseek_ptr(fd, sectionOffset, 0 /* SEEK_SET */);
|
|
read_ptr(fd, section.data, sectionSize);
|
|
} else {
|
|
section.data = undefined;
|
|
}
|
|
sections.push(section);
|
|
} else if (['.dynsym', '.dynstr', '.rel.dyn', '.rel.plt'].includes(sectionName)) {
|
|
var section = {};
|
|
section.name = sectionName;
|
|
section.memoryOffset = sectionAddress;
|
|
section.fileOffset = sectionOffset;
|
|
section.size = sectionSize;
|
|
if (sectionSize > 0) {
|
|
section.data = Memory.alloc(sectionSize);
|
|
lseek_ptr(fd, sectionOffset, 0 /* SEEK_SET */);
|
|
read_ptr(fd, section.data, sectionSize);
|
|
} else {
|
|
console.log('No data section for', section.name);
|
|
section.data = undefined;
|
|
}
|
|
|
|
if (section.name === '.dynsym') {
|
|
dynsym = section;
|
|
}
|
|
if (section.name === '.dynstr') {
|
|
dynstr = section;
|
|
}
|
|
if (section.name === '.rel.dyn') {
|
|
reldyn = section;
|
|
}
|
|
if (section.name === '.rel.plt') {
|
|
relplt = section;
|
|
}
|
|
sections.push(section);
|
|
}
|
|
}
|
|
|
|
if (!!dynsym && !!dynstr) {
|
|
var symbols = [];
|
|
var stringTable = module.base.add(dynstr.memoryOffset);
|
|
var structSize = is32 ? 16 : 24;
|
|
for (var i = 0; i < dynsym.size / structSize; i++) {
|
|
var symbolOffset = Memory.readU32(module.base.add(dynsym.memoryOffset).add(structSize * i));
|
|
symbols.push(Memory.readUtf8String(stringTable.add(symbolOffset)));
|
|
}
|
|
|
|
module.symbols = symbols;
|
|
}
|
|
|
|
var relmap = new Map();
|
|
if (!!reldyn) {
|
|
for (var i = 0; i < reldyn.size / 8; i++) {
|
|
if ((Memory.readU32(module.base.add(reldyn.memoryOffset).add(i * 8)) != 0) &&
|
|
(Memory.readU32(module.base.add(reldyn.memoryOffset).add(i * 8).add(4)) >> 8 != 0)) {
|
|
relmap[Memory.readU32(module.base.add(reldyn.memoryOffset).add(i * 8))] = Memory.readU32(module.base.add(reldyn.memoryOffset).add(i * 8).add(4)) >> 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!!relplt) {
|
|
for (var i = 0; i < relplt.size / 8; i++) {
|
|
if ((Memory.readU32(module.base.add(relplt.memoryOffset).add(i * 8)) != 0) &&
|
|
(Memory.readU32(module.base.add(relplt.memoryOffset).add(i * 8).add(4)) >> 8 != 0)) {
|
|
relmap[Memory.readU32(module.base.add(relplt.memoryOffset).add(i * 8))] = Memory.readU32(module.base.add(relplt.memoryOffset).add(i * 8).add(4)) >> 8;
|
|
}
|
|
}
|
|
}
|
|
module.relmap = relmap;
|
|
|
|
module.sections = sections;
|
|
return true;
|
|
}
|
|
|
|
function findHooks(module) {
|
|
if (module.sections === undefined) {
|
|
if (!getElfData(module)) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
module.sections.forEach((section) => {
|
|
if (section.size === 0) {
|
|
return;
|
|
}
|
|
|
|
// It's important to cast the ArrayBuffer returned by `readByteArray` cannot be referenced incrementally
|
|
var file = new Uint8Array(Memory.readByteArray(section.data, section.size));
|
|
var memory = new Uint8Array(Memory.readByteArray(module.base.add(section.memoryOffset), section.size));
|
|
for (var i = 0; i < section.size;) {
|
|
if (['.rodata', '.text'].includes(section.name)) {
|
|
if (file[i] != memory[i]) {
|
|
console.log('*** Potential variance found at ', DebugSymbol.fromAddress(module.base.add(section.memoryOffset).add(i)));
|
|
i += 4;
|
|
}
|
|
i++
|
|
} else if (['.got'].includes(section.name)) {
|
|
break;
|
|
// It shouldn't be as the got table isn't initialized until execution
|
|
if (file[i] != memory[i]) {
|
|
// todo compare the symbol to string against what it resolves too
|
|
}
|
|
i += module.is32 ? 4 : 8;
|
|
} else {
|
|
// Unscanned sections, to be added as needed
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Quick and simple way to get the package name, assumes that the script
|
|
// was injected into an APK otherwise it won't work.
|
|
function getPackageName() {
|
|
var fd = open_ptr(Memory.allocUtf8String('/proc/self/cmdline'), 0 /* O_RDONLY */, 0);
|
|
if (fd == -1) {
|
|
return 'null';
|
|
}
|
|
|
|
var buffer = Memory.alloc(32);
|
|
read_ptr(fd, buffer, 32);
|
|
close_ptr(fd);
|
|
|
|
return Memory.readUtf8String(buffer);
|
|
}
|
|
|
|
// Adjust this as needed, often I don't need to scan anything outside of the
|
|
// included shared libraries and a few which are almost always in an apex folder.
|
|
// This logic will need to be changed if you're using a pre-apex version of Android
|
|
// to ensure it picked up the proper libraries for hunting
|
|
//
|
|
// While it doesn't hurt to scan everything, it's almost never needed and will just slow
|
|
// down the process at a linear scale.
|
|
//
|
|
// If you already know what you're hunting for, feel free to just return or look for
|
|
// libart, libdvm, etc, etc
|
|
function getRelevantModules() {
|
|
var modules = [];
|
|
var packagename = getPackageName();
|
|
|
|
Process.enumerateModules().forEach((module) => {
|
|
if (module.path.includes(packagename)) {
|
|
modules.push(module);
|
|
console.log('Adding ', module.path);
|
|
} else if (module.path.includes('/apex')) {
|
|
modules.push(module);
|
|
console.log('Adding ', module.path);
|
|
} else {
|
|
console.log('Skipping ', module.path);
|
|
}
|
|
})
|
|
|
|
return modules;
|
|
}
|
|
|
|
var modules = getRelevantModules();
|
|
|
|
modules.forEach((module) => {
|
|
getElfData(module);
|
|
findHooks(module);
|
|
});
|