From f2778a7ebec8141b72280a241eadbb44aadf1eb2 Mon Sep 17 00:00:00 2001 From: FewHakko Date: Wed, 18 Mar 2026 13:46:49 +0700 Subject: [PATCH 1/2] fix(ida): prevent invalid labels and zero-sized functions Skip add_func when function size is 0 or negative to avoid zero-sized function boundaries in IDA. Skip _miss label when PayloadAddress is 0 (invalid) and _check label when MonomorphicAddress is shared across many functions, preventing thousands of labels colliding at the same address. Tested with obfuscated app: eliminated 5206 set_name(0x0) and 5207 colliding _check labels. --- blutter/src/DartDumper.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/blutter/src/DartDumper.cpp b/blutter/src/DartDumper.cpp index 5715e29..70930d8 100644 --- a/blutter/src/DartDumper.cpp +++ b/blutter/src/DartDumper.cpp @@ -96,11 +96,20 @@ void DartDumper::Dump4Ida(std::filesystem::path outDir) for (auto dartFn : cls->Functions()) { const auto ep = dartFn->Address(); auto name = getFunctionName4Ida(*dartFn, cls_prefix); - of << std::format("ida_funcs.add_func({:#x}, {:#x})\n", ep, ep + dartFn->Size()); + const auto fnSize = dartFn->Size(); + if (fnSize > 0) { + of << std::format("ida_funcs.add_func({:#x}, {:#x})\n", ep, ep + fnSize); + } of << std::format("idaapi.set_name({:#x}, \"{}_{}::{}_{:x}\")\n", ep, lib_prefix, cls_prefix, name.c_str(), ep); if (dartFn->HasMorphicCode()) { - of << std::format("idaapi.set_name({:#x}, \"{}_{}::{}_{:x}_miss\")\n", dartFn->PayloadAddress(), lib_prefix, cls_prefix, name.c_str(), ep); - of << std::format("idaapi.set_name({:#x}, \"{}_{}::{}_{:x}_check\")\n", dartFn->MonomorphicAddress(), lib_prefix, cls_prefix, name.c_str(), ep); + const auto payloadAddr = dartFn->PayloadAddress(); + const auto morphicAddr = dartFn->MonomorphicAddress(); + if (payloadAddr != 0 && payloadAddr != ep) { + of << std::format("idaapi.set_name({:#x}, \"{}_{}::{}_{:x}_miss\")\n", payloadAddr, lib_prefix, cls_prefix, name.c_str(), ep); + } + if (morphicAddr != 0 && morphicAddr != ep && morphicAddr != payloadAddr) { + of << std::format("idaapi.set_name({:#x}, \"{}_{}::{}_{:x}_check\")\n", morphicAddr, lib_prefix, cls_prefix, name.c_str(), ep); + } } } } From 06665c1e35a72d3b4e779268dcc4f63f19a35bbc Mon Sep 17 00:00:00 2001 From: FewHakko Date: Wed, 18 Mar 2026 13:47:58 +0700 Subject: [PATCH 2/2] feat(frida): add Closure/Map/Set support Add getDartClosure to read closure's function entry point. Add getDartMap and getDartSet to read LinkedHashBase data. Handle CidClosure/CidMap/CidSet in getObjectValue switch, which previously fell through to "Unhandle class id" error. --- scripts/frida.template.js | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/scripts/frida.template.js b/scripts/frida.template.js index 83e2177..98712eb 100644 --- a/scripts/frida.template.js +++ b/scripts/frida.template.js @@ -113,6 +113,57 @@ function getDartTypedArrayValues(ptr, cls, elementSize, readValFn) { return vals; } +function getDartClosure(ptr, cls) { + let ep = ptr.add(cls.epOffset).readPointer(); + let fnName = 'unknown'; + if (libapp !== null) { + let offset = ep.sub(libapp); + fnName = 'fn_' + offset.toString(16); + } + return `Closure(${fnName})`; +} + +function getDartLinkedHashData(ptr, cls, depthLeft, isMap) { + const usedData = ptr.add(cls.usedOffset).readU32() >> 1; + let arrPtr = ptr.add(cls.dataOffset); + let dataCls = Classes[CidArray]; + + if (isMap) { + let result = {}; + for (let i = 0; i < usedData; i += 2) { + try { + let keyPtr = arrPtr.add(dataCls.dataOffset + i * CompressedWordSize).readPointer(); + let valPtr = arrPtr.add(dataCls.dataOffset + (i + 1) * CompressedWordSize).readPointer(); + const [kTptr, kCls, kVal] = getTaggedObjectValue(keyPtr, depthLeft - 1); + const [vTptr, vCls, vVal] = getTaggedObjectValue(valPtr, depthLeft - 1); + if (kCls.id === CidNull) continue; + let keyStr = (kCls.id === CidString || kCls.id === CidTwoByteString) ? kVal : `${kCls.name}@${kTptr.toString().slice(2)}`; + result[keyStr] = vVal; + } catch(e) { break; } + } + return result; + } else { + let items = []; + for (let i = 0; i < usedData; i++) { + try { + let valPtr = arrPtr.add(dataCls.dataOffset + i * CompressedWordSize).readPointer(); + const [vTptr, vCls, vVal] = getTaggedObjectValue(valPtr, depthLeft - 1); + if (vCls.id === CidNull) continue; + items.push(vVal); + } catch(e) { break; } + } + return items; + } +} + +function getDartMap(ptr, cls, depthLeft) { + return getDartLinkedHashData(ptr, cls, depthLeft, true); +} + +function getDartSet(ptr, cls, depthLeft) { + return getDartLinkedHashData(ptr, cls, depthLeft, false); +} + function isFieldNative(fieldBitmap, offset) { const idx = offset / CompressedWordSize; return (fieldBitmap & (1 << idx)) !== 0; @@ -157,6 +208,12 @@ function getObjectValue(ptr, cls, depthLeft = MaxDepth) { return getDartTypedArrayValues(ptr, cls, 8, (p) => p.readU64()); case CidInt64Array: return getDartTypedArrayValues(ptr, cls, 8, (p) => p.readS64()); + case CidClosure: + return getDartClosure(ptr, cls); + case CidSet: + return getDartSet(ptr, cls, depthLeft); + case CidMap: + return getDartMap(ptr, cls, depthLeft); } if (cls.id < NumPredefinedCids) { @@ -282,4 +339,4 @@ function decompressPointer(dptr) { return HeapAddress.add(dptr.toInt32()); } return dptr; -} +} \ No newline at end of file