local function runDeepAnalysis(ns, cls, maxDepth, filterStr) local al = getAddressList() local function getFields(handle) local f = {} local curr = handle while curr and curr ~= 0 do local fl = mono_class_enumFields(curr) if fl then for _, field in ipairs(fl) do table.insert(f, field) end end curr = mono_class_getParent(curr) end return f end local function applyType(rec, tn) if not tn then rec.Type = vtDword return false end local t = tn:lower() if t:find("string") then local strPtr = readPointer(rec.Address) local strLen = 5 if strPtr and strPtr ~= 0 then strLen = readInteger(strPtr + 0x10) or 5 end rec.Type = vtString rec.String.Unicode = true rec.String.Size = strLen rec.String.ZeroTerminate = true rec.OffsetCount = 1 rec.Offset[0] = 0x14 return true end local isRef = t:find("class") or t:find("object") or t:find("%[") or (not t:find("system%.")) if isRef then rec.Type = vtPointer; rec.ShowAsHex = true elseif t:find("int32") or t:find("i4") then rec.Type = vtDword elseif t:find("single") or t:find("r4") or t:find("float") then rec.Type = vtSingle elseif t:find("bool") then rec.Type = vtByte else rec.Type = vtDword end return isRef end local function buildFields(pRec, handle, base, depth) if depth > maxDepth then return end local fields = getFields(handle) for _, f in ipairs(fields) do if not f.isStatic then local r = al.createMemoryRecord() r.Parent = pRec r.Address = (depth == 0) and (base .. "+" .. string.format("%X", f.offset)) or (string.format("[%s]+%X", base, f.offset)) local isRef = applyType(r, f.typename) r.Description = f.name .. " (" .. (f.typename:match("[^%.]+$") or "unknown") .. ")" if isRef and depth < maxDepth and not f.typename:lower():find("string") then local fClass = mono_field_getClass(f.field) if fClass and fClass ~= 0 then buildFields(r, fClass, r.Address, depth + 1) end end end end end local assemblies = mono_enumAssemblies() local classHandle = nil for _, a in ipairs(assemblies) do local img = mono_getImageFromAssembly(a) local classes = mono_image_enumClasses(img) if classes then for _, c in ipairs(classes) do if c.classname == cls and c.namespace == ns then classHandle = c.class; break end end end if classHandle then break end end if not classHandle or classHandle == 0 then showMessage("Class not found!"); return end local allFields = getFields(classHandle) local fieldMap = {} for _, f in ipairs(allFields) do fieldMap[f.name] = f end local filterFunc = nil if filterStr and filterStr:trim() ~= "" and filterStr ~= "instance." then local code = "return function(instance) return (" .. filterStr .. ") end" local chunk, err = load(code) if chunk then filterFunc = chunk() else showMessage("Lua Error: " .. tostring(err)); return end end local instances = mono_class_findInstancesOfClassListOnly(nil, classHandle) or {} local rootDesc = cls .. ":0" local root = al.createMemoryRecord() root.Description = rootDesc; root.IsGroupHeader = true local matchCount = 0 for _, addr in ipairs(instances) do local keep = true if filterFunc then local proxy = {} setmetatable(proxy, { __index = function(_, key) local f = fieldMap[key] if not f then return nil end local t = f.typename:lower() local fieldAddr = addr + f.offset if t:find("string") then local strPtr = readPointer(fieldAddr) if not strPtr or strPtr == 0 then return nil end return mono_string_readString(strPtr) elseif t:find("int32") or t:find("i4") then return readInteger(fieldAddr) elseif t:find("single") or t:find("r4") or t:find("float") then return readFloat(fieldAddr) elseif t:find("bool") then return readBytes(fieldAddr, 1) ~= 0 elseif t:find("double") or t:find("r8") then return readDouble(fieldAddr) elseif t:find("int64") or t:find("i8") then return readQword(fieldAddr) end return readPointer(fieldAddr) end }) local status, result = pcall(filterFunc, proxy) keep = status and (result == true) end if keep then matchCount = matchCount + 1 local r = al.createMemoryRecord() r.Parent = root; r.Address = string.format("%X", addr) r.Description = "Instance: " .. r.Address; r.Type = vtPointer buildFields(r, classHandle, r.Address, 0) end end root.Description = cls .. ":" .. matchCount if matchCount == 0 then root.destroy() showMessage("No instances found.") end end local function createScannerForm() if getOpenedProcessID() == 0 then showMessage("Error: Not attached!"); return end if not LaunchMonoDataCollector() then showMessage("Error: Mono not supported!"); return end local form = createForm(false) form.Caption = 'Mono Instance Scanner' form.Width = 420; form.Height = 420 form.Position = 'poScreenCenter'; form.BorderStyle = 'bsSingle' local L_POS, C_WIDTH = 15, 380 local lblAsm = createLabel(form); lblAsm.Caption = '1. Select Assembly:'; lblAsm.setPosition(L_POS, 10) local cbAsm = createComboBox(form); cbAsm.setPosition(L_POS, 30); cbAsm.Width = C_WIDTH; cbAsm.Style = 'csDropDownList' local lblNS = createLabel(form); lblNS.Caption = '2. Select Namespace:'; lblNS.setPosition(L_POS, 70) local cbNS = createComboBox(form); cbNS.setPosition(L_POS, 90); cbNS.Width = C_WIDTH; cbNS.Style = 'csDropDownList' local lblClass = createLabel(form); lblClass.Caption = '3. Select Class:'; lblClass.setPosition(L_POS, 130) local cbClass = createComboBox(form); cbClass.setPosition(L_POS, 150); cbClass.Width = C_WIDTH; cbClass.Style = 'csDropDownList' local lblFilter = createLabel(form); lblFilter.Caption = '4. Filter (e.g. instance.resourceKey == "boy_1"):'; lblFilter.setPosition(L_POS, 195) local edtFilter = createEdit(form); edtFilter.setPosition(L_POS, 215); edtFilter.Width = C_WIDTH; edtFilter.Text = 'instance.' local lblDepth = createLabel(form); lblDepth.Caption = 'Recursion Depth (0-3):'; lblDepth.setPosition(L_POS, 255) local edtDepth = createEdit(form); edtDepth.setPosition(250, 252); edtDepth.Width = 40; edtDepth.Text = '1' local btnOk = createButton(form); btnOk.Caption = 'Start Scan & Add'; btnOk.setSize(150, 40); btnOk.setPosition(135, 320) local asmMap = {} local function refreshAssemblies() cbAsm.Items.clear(); asmMap = {} local assemblies = mono_enumAssemblies() or {} local names = {} for _, a in ipairs(assemblies) do local name = mono_image_get_name(mono_getImageFromAssembly(a)) table.insert(names, name); asmMap[name] = a end table.sort(names, function(a,b) return a:lower() < b:lower() end) for _, n in ipairs(names) do cbAsm.Items.add(n) end end cbAsm.OnChange = function() cbNS.Items.clear(); cbClass.Items.clear() local hAsm = asmMap[cbAsm.Text] if not hAsm then return end local img = mono_getImageFromAssembly(hAsm) local classes = mono_image_enumClasses(img) local nsList, nsSet = {}, {} if classes then for _, c in ipairs(classes) do local ns = c.namespace or "" if ns ~= "" and not nsSet[ns] then table.insert(nsList, ns); nsSet[ns] = true end end table.sort(nsList, function(a,b) return a:lower() < b:lower() end) cbNS.Items.add("(Default Namespace)") for _, n in ipairs(nsList) do cbNS.Items.add(n) end end end cbNS.OnChange = function() cbClass.Items.clear() local hAsm = asmMap[cbAsm.Text] if not hAsm then return end local img = mono_getImageFromAssembly(hAsm) local classes = mono_image_enumClasses(img) local targetNS = cbNS.Text == "(Default Namespace)" and "" or cbNS.Text local classList = {} if classes then for _, c in ipairs(classes) do if c.namespace == targetNS then table.insert(classList, c.classname) end end table.sort(classList, function(a,b) return a:lower() < b:lower() end) for _, cn in ipairs(classList) do cbClass.Items.add(cn) end end end btnOk.OnClick = function() if cbClass.Text ~= "" then local ns = cbNS.Text == "(Default Namespace)" and "" or cbNS.Text runDeepAnalysis(ns, cbClass.Text, tonumber(edtDepth.Text) or 0, edtFilter.Text) form.close() else showMessage("Please select a class!") end end refreshAssemblies(); form.show() end local function initPlugin() local al = getAddressList() if not al then return end local alControl = al.getComponent(0) if not alControl or not alControl.PopupMenu then return end local menu = alControl.PopupMenu local btnCaption = 'Scan Mono Instances' for i=0, menu.Items.Count-1 do if menu.Items[i].Caption == btnCaption then return end end local newItem = createMenuItem(menu); newItem.Caption = btnCaption; newItem.OnClick = createScannerForm menu.Items.add(newItem) end initPlugin()