From 638d032764d01c213bb35322a3ff5946e3b88d77 Mon Sep 17 00:00:00 2001 From: r0ny123 <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:09:04 +0000 Subject: [PATCH 01/25] Refactor FunctionAnalysisState for Dalvik Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- smda/Disassembler.py | 5 + smda/dalvik/DalvikDisassembler.py | 220 ++++++++++++++++ smda/dalvik/DalvikFunctionAnalysisState.py | 277 +++++++++++++++++++++ smda/dalvik/__init__.py | 2 + smda/utility/DexFileLoader.py | 29 +++ smda/utility/FileLoader.py | 3 +- 6 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 smda/dalvik/DalvikDisassembler.py create mode 100644 smda/dalvik/DalvikFunctionAnalysisState.py create mode 100644 smda/dalvik/__init__.py create mode 100644 smda/utility/DexFileLoader.py diff --git a/smda/Disassembler.py b/smda/Disassembler.py index c2b66415..e890d4d2 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -7,6 +7,7 @@ from smda.common.BinaryInfo import BinaryInfo from smda.common.labelprovider.GoLabelProvider import GoSymbolProvider from smda.common.SmdaReport import SmdaReport +from smda.dalvik.DalvikDisassembler import DalvikDisassembler from smda.ida.IdaExporter import IdaExporter from smda.intel.IntelDisassembler import IntelDisassembler from smda.SmdaConfig import SmdaConfig @@ -27,6 +28,8 @@ def __init__(self, config=None, backend=None): self.disassembler = IntelDisassembler(self.config) elif backend == "cil": self.disassembler = CilDisassembler(self.config) + elif backend == "dalvik": + self.disassembler = DalvikDisassembler(self.config) elif backend == "IDA": self.disassembler = IdaExporter(self.config) self._start_time = None @@ -41,6 +44,8 @@ def initDisassembler(self, architecture="intel"): self.disassembler = IntelDisassembler(self.config) elif architecture == "cil": self.disassembler = CilDisassembler(self.config) + elif architecture == "dalvik": + self.disassembler = DalvikDisassembler(self.config) def _getDurationInSeconds(self, start_ts, end_ts): return (end_ts - start_ts).seconds + ((end_ts - start_ts).microseconds / 1000000.0) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py new file mode 100644 index 00000000..253fa695 --- /dev/null +++ b/smda/dalvik/DalvikDisassembler.py @@ -0,0 +1,220 @@ +import datetime +import logging + +import lief + +from smda.dalvik.DalvikFunctionAnalysisState import DalvikFunctionAnalysisState +from smda.DisassemblyResult import DisassemblyResult + +LOGGER = logging.getLogger(__name__) + +# Basic lookup table for Dalvik opcodes. +# This covers simple CFG branching and method calls. +DALVIK_OPCODES = { + 0x00: ("nop", 1), + 0x01: ("move", 1), + 0x02: ("move/from16", 2), + 0x03: ("move/16", 3), + 0x04: ("move-wide", 1), + 0x05: ("move-wide/from16", 2), + 0x06: ("move-wide/16", 3), + 0x07: ("move-object", 1), + 0x08: ("move-object/from16", 2), + 0x09: ("move-object/16", 3), + 0x0A: ("move-result", 1), + 0x0B: ("move-result-wide", 1), + 0x0C: ("move-result-object", 1), + 0x0D: ("move-exception", 1), + 0x0E: ("return-void", 1), + 0x0F: ("return", 1), + 0x10: ("return-wide", 1), + 0x11: ("return-object", 1), + 0x12: ("const/4", 1), + 0x13: ("const/16", 2), + 0x14: ("const", 3), + 0x15: ("const/high16", 2), + 0x16: ("const-wide/16", 2), + 0x17: ("const-wide/32", 3), + 0x18: ("const-wide", 5), + 0x19: ("const-wide/high16", 2), + 0x1A: ("const-string", 2), + 0x1B: ("const-string/jumbo", 3), + 0x1C: ("const-class", 2), + 0x1D: ("monitor-enter", 1), + 0x1E: ("monitor-exit", 1), + 0x1F: ("check-cast", 2), + 0x20: ("instance-of", 2), + 0x21: ("array-length", 1), + 0x22: ("new-instance", 2), + 0x23: ("new-array", 2), + 0x24: ("filled-new-array", 3), + 0x25: ("filled-new-array/range", 3), + 0x26: ("fill-array-data", 3), + 0x27: ("throw", 1), + 0x28: ("goto", 1), + 0x29: ("goto/16", 2), + 0x2A: ("goto/32", 3), + 0x2B: ("packed-switch", 3), + 0x2C: ("sparse-switch", 3), + 0x2D: ("cmpl-float", 2), + 0x2E: ("cmpg-float", 2), + 0x2F: ("cmpl-double", 2), + 0x30: ("cmpg-double", 2), + 0x31: ("cmp-long", 2), + 0x32: ("if-eq", 2), + 0x33: ("if-ne", 2), + 0x34: ("if-lt", 2), + 0x35: ("if-ge", 2), + 0x36: ("if-gt", 2), + 0x37: ("if-le", 2), + 0x38: ("if-eqz", 2), + 0x39: ("if-nez", 2), + 0x3A: ("if-ltz", 2), + 0x3B: ("if-gez", 2), + 0x3C: ("if-gtz", 2), + 0x3D: ("if-lez", 2), + 0x6E: ("invoke-virtual", 3), + 0x6F: ("invoke-super", 3), + 0x70: ("invoke-direct", 3), + 0x71: ("invoke-static", 3), + 0x72: ("invoke-interface", 3), + 0x74: ("invoke-virtual/range", 3), + 0x75: ("invoke-super/range", 3), + 0x76: ("invoke-direct/range", 3), + 0x77: ("invoke-static/range", 3), + 0x78: ("invoke-interface/range", 3), +} + +# The size is in 16-bit code units. + + +class DalvikDisassembler: + def __init__(self, config): + self.config = config + self.disassembly = DisassemblyResult() + self.disassembly.smda_version = config.VERSION + + def analyzeFunction(self, dex_file, method_info): + start_addr = method_info["offset"] + code_item = method_info["code_item"] + + # In DEX, the code_item header is 16 bytes. The bytecode immediately follows. + insns_size_units = code_item.insns_size + insns_size_bytes = insns_size_units * 2 + bytecode_offset = start_addr + 16 + + if bytecode_offset + insns_size_bytes <= len(dex_file.header): + pass # LIEF parsing might just map it, but we have binary_info + + # We need raw bytecode. method_info has raw_data. + raw_data = method_info["raw_data"] + bytecode = raw_data[bytecode_offset : bytecode_offset + insns_size_bytes] + + state = DalvikFunctionAnalysisState(start_addr, self.disassembly) + + idx = 0 + while idx < len(bytecode): + # A simple loop to read dalvik bytecode. LIEF returns list of bytes. + op = bytecode[idx] + + mnemonic, length_units = DALVIK_OPCODES.get(op, ("unknown_" + hex(op), 1)) + length_bytes = length_units * 2 + + if idx + length_bytes > len(bytecode): + length_bytes = len(bytecode) - idx + + i_bytes = bytes(bytecode[idx : idx + length_bytes]) + i_address = start_addr + idx + i_size = length_bytes + i_mnemonic = mnemonic + i_op_str = "" + + state.setNextInstructionReachable(True) + + # Simple CFG handling + if i_mnemonic in ["return-void", "return", "return-wide", "return-object", "throw"]: + state.setNextInstructionReachable(False) + elif i_mnemonic.startswith("goto"): + target_offset = 0 + if i_mnemonic == "goto": + target_offset = int.from_bytes(i_bytes[1:2], byteorder="little", signed=True) * 2 + elif i_mnemonic == "goto/16": + target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 + elif i_mnemonic == "goto/32": + target_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 + target_address = i_address + target_offset + state.addCodeRef(i_address, target_address, by_jump=True) + i_op_str = hex(target_address) + state.setNextInstructionReachable(False) + elif i_mnemonic.startswith("if-"): + target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 + target_address = i_address + target_offset + state.addCodeRef(i_address, target_address, by_jump=True) + i_op_str = hex(target_address) + elif i_mnemonic.startswith("invoke-"): + method_idx = int.from_bytes(i_bytes[2:4], byteorder="little") + try: + target_method = dex_file.methods[method_idx] + i_op_str = target_method.name + # Also we can update API information if we want. + except Exception: + i_op_str = f"method_idx_{method_idx}" + + state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, i_bytes) + idx += length_bytes + + state.label = method_info["name"] + state.finalizeAnalysis() + return state + + def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): + LOGGER.debug( + "Analyzing buffer with %d bytes @0x%08x", + binary_info.binary_size, + binary_info.base_addr, + ) + self.disassembly = DisassemblyResult() + self.disassembly.smda_version = self.config.VERSION + self.disassembly.setBinaryInfo(binary_info) + self.disassembly.binary_info.architecture = "dalvik" + self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) + self.disassembly.language = "dalvik" + + # LIEF natively accepts bytes or bytearray + if hasattr(lief.DEX, "parse"): + try: # noqa: SIM105 + dex_file = lief.DEX.parse(list(binary_info.raw_data)) # Fallback if list is needed + except Exception: + pass + + # In recent LIEF, we can just pass the raw bytes array + # Let's try raw bytes first, then fallback to list + try: + dex_file = lief.DEX.parse(binary_info.raw_data) + except TypeError: + dex_file = lief.DEX.parse(list(binary_info.raw_data)) + + if dex_file: + for method in dex_file.methods: + if not method.has_class: + continue + code_info = method.code_info + if not code_info: + continue + # method.code_offset gets us an offset but if LIEF returns something we can map: + # since DEX doesn't map directly, we use code_offset as address + + method_info = { + "offset": method.code_offset, + "name": f"{method.cls.fullname}->{method.name}", + "code_item": code_info, + "raw_data": binary_info.raw_data, + } + if cbAnalysisTimeout and cbAnalysisTimeout(): + break + self.analyzeFunction(dex_file, method_info) + + self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc) + if cbAnalysisTimeout and cbAnalysisTimeout(): + self.disassembly.analysis_timeout = True + return self.disassembly diff --git a/smda/dalvik/DalvikFunctionAnalysisState.py b/smda/dalvik/DalvikFunctionAnalysisState.py new file mode 100644 index 00000000..d8d894cc --- /dev/null +++ b/smda/dalvik/DalvikFunctionAnalysisState.py @@ -0,0 +1,277 @@ +import logging + +LOGGER = logging.getLogger(__name__) + +# Dalvik-specific lists for block derivation +# Any invoke-* is a call +CALL_INS = [ + "invoke-virtual", + "invoke-super", + "invoke-direct", + "invoke-static", + "invoke-interface", + "invoke-virtual/range", + "invoke-super/range", + "invoke-direct/range", + "invoke-static/range", + "invoke-interface/range", +] +# Any return-* or throw is an end instruction +END_INS = ["return-void", "return", "return-wide", "return-object", "throw"] + + +class DalvikFunctionAnalysisState: + def __init__(self, start_addr, disassembly): + self.start_addr = start_addr + self.disassembly = disassembly + self.block_queue = [start_addr] + self.current_block = [] + self.blocks = [] + self.num_blocks_analyzed = 0 + self.instructions = [] + self.instruction_start_bytes = set() + self.processed_blocks = set() + self.processed_bytes = set() + self.jump_targets = set() + self.call_register_ins = [] + self.block_start = 0xFFFFFFFF + self.data_bytes = set() + self.data_refs = set() + self.code_refs = set() + self.code_refs_from = {} + self.code_refs_to = {} + self.suspicious_ins_count = 0 + self.is_jmp = False + self.is_next_instruction_reachable = True + self.is_block_ending_instruction = False + self.is_sanely_ending = False + self.has_collision = False + self.colliding_addresses = set() + self.is_tailcall_function = False + self.is_leaf_function = True + self.is_recursive = False + self.is_thunk_call = False + self.label = "" + + def chooseNextBlock(self): + self.is_block_ending_instruction = False + self.block_start = self.block_queue.pop() + self.processed_blocks.update([self.block_start]) + return self.block_start + + def addBlockToQueue(self, block_start): + if block_start not in self.processed_blocks: + self.block_queue.append(block_start) + + def endBlock(self): + if self.current_block: + self.num_blocks_analyzed += 1 + self.current_block = [] + + def addInstruction(self, i_address, i_size, i_mnemonic, i_op_str, i_bytes): + ins = (i_address, i_size, i_mnemonic, i_op_str, i_bytes) + self.instructions.append(ins) + self.instruction_start_bytes.add(ins[0]) + self.current_block.append(ins) + for byte in range(i_size): + self.processed_bytes.add(i_address + byte) + if self.is_next_instruction_reachable: + self.addCodeRef(i_address, i_address + i_size, self.is_jmp) + self.is_jmp = False + + def addCodeRef(self, addr_from, addr_to, by_jump=False): + self.code_refs.update([(addr_from, addr_to)]) + refs_from = self.code_refs_from.get(addr_from, set()) + refs_from.update([addr_to]) + self.code_refs_from[addr_from] = refs_from + refs_to = self.code_refs_to.get(addr_to, set()) + refs_to.update([addr_from]) + self.code_refs_to[addr_to] = refs_to + if by_jump: + self.is_jmp = True + self.jump_targets.update([addr_to]) + + def removeCodeRef(self, addr_from, addr_to): + if (addr_from, addr_to) in self.code_refs: + self.code_refs.remove((addr_from, addr_to)) + if addr_from in self.code_refs_from and addr_to in self.code_refs_from[addr_from]: + self.code_refs_from[addr_from].remove(addr_to) + if addr_to in self.code_refs_to and addr_from in self.code_refs_to[addr_to]: + self.code_refs_to[addr_to].remove(addr_from) + if addr_to in self.jump_targets: + self.jump_targets.remove(addr_to) + + def addDataRef(self, addr_from, addr_to, size=1): + self.data_refs.update([(addr_from, addr_to)]) + for i in range(size): + self.data_bytes.update([addr_to + i]) + + def backtrackInstructions(self, addr_from, num_instructions): + backtracked = [] + for instruction in sorted(self.instructions, key=lambda x: x[0]): + if instruction[0] >= addr_from: + break + backtracked.append(instruction) + return backtracked[-num_instructions:] + + def identifyCallConflicts(self, all_refs): + conflicts = {} + non_instruction_start_bytes = self.processed_bytes.difference(self.instruction_start_bytes) + conflict_addrs = set(all_refs.keys()).intersection(non_instruction_start_bytes) + for candidate_source_ref in conflict_addrs: + candidate = all_refs[candidate_source_ref] + if candidate not in conflicts: + conflicts[candidate] = [] + conflicts[candidate].append(candidate_source_ref) + return conflicts + + def _finalizeRegularAnalysis(self): + fn_min = min([ins[0] for ins in self.instructions]) + fn_max = max([ins[0] + ins[1] for ins in self.instructions]) + + self.disassembly.function_symbols[self.start_addr] = self.label + self.disassembly.function_borders[self.start_addr] = (fn_min, fn_max) + for ins in self.instructions: + self.disassembly.instructions[ins[0]] = (ins[2], ins[1]) + for offset in range(ins[1]): + self.disassembly.code_map[ins[0] + offset] = ins[0] + self.disassembly.ins2fn[ins[0] + offset] = self.start_addr + self.disassembly.data_map.update(self.data_bytes) + self.disassembly.functions[self.start_addr] = self.getBlocks() + for cref in self.code_refs: + self.disassembly.addCodeRefs(cref[0], cref[1]) + for dref in self.data_refs: + self.disassembly.addDataRefs(dref[0], dref[1]) + if self.is_recursive: + self.disassembly.recursive_functions.add(self.start_addr) + if self.is_leaf_function: + self.disassembly.leaf_functions.add(self.start_addr) + if self.is_thunk_call: + self.disassembly.thunk_functions.add(self.start_addr) + + def finalizeAnalysis(self, as_gap=False): + if as_gap: + LOGGER.debug( + "0x%08x had sanity state: %s (%d ins, blocks: %d)", + self.start_addr, + self.is_sanely_ending, + len(self.instructions), + self.num_blocks_analyzed, + ) + if as_gap and not self.is_sanely_ending: + if len(self.instructions) == 1 and self.instructions[0][2].startswith("goto") or self.num_blocks_analyzed == 1 and ( + self.instructions[-1][2].startswith("goto") or self.instructions[-1][2].startswith("invoke-") + ): + pass + else: + return False + if self.instructions: + self.num_blocks_analyzed = len(self.getBlocks()) + if self.num_blocks_analyzed: + self._finalizeRegularAnalysis() + return True + + def revertAnalysis(self): + self.disassembly.function_borders.pop(self.start_addr, None) + for ins in self.instructions: + self.disassembly.instructions.pop(ins[0], None) + for byte in range(ins[1]): + self.disassembly.code_map.pop(ins[0] + byte, None) + self.disassembly.ins2fn.pop(ins[0] + byte, None) + for cref in self.code_refs: + self.disassembly.removeCodeRefs(cref[0], cref[1]) + for dref in self.data_refs: + self.disassembly.removeDataRefs(dref[0], dref[1]) + self.disassembly.functions.pop(self.start_addr, None) + + def getBlocks(self): + if self.blocks: + return self.blocks + self.instructions.sort() + ins = {i[0]: ind for ind, i in enumerate(self.instructions)} + potential_starts = {self.start_addr} + potential_starts.update(list(self.jump_targets)) + blocks = [] + for start in sorted(potential_starts): + if start not in ins: + continue + block = [] + for i in range(ins[start], len(self.instructions)): + current = self.instructions[i] + block.append(current) + if ( + current[0] in self.code_refs_from + and current[2] not in CALL_INS + and i != len(self.instructions) - 1 + and any(r != self.instructions[i + 1][0] for r in self.code_refs_from[current[0]]) + ): + break + if ( + i != len(self.instructions) - 1 + and self.instructions[i + 1][0] in self.code_refs_to + and ( + len(self.code_refs_to[self.instructions[i + 1][0]]) > 1 + or self.instructions[i + 1][0] in potential_starts + ) + ): + break + if current[2] in END_INS: + break + if block: + blocks.append(block) + self.blocks = blocks + return self.blocks + + def hasUnprocessedBlocks(self): + return len(set(self.block_queue).difference(self.processed_blocks)) > 0 + + def isProcessed(self, addr): + return addr in self.processed_bytes + + def isProcessedFunction(self): + return self.start_addr in self.disassembly.code_map + + def isNextInstructionReachable(self): + return self.is_next_instruction_reachable + + def setNextInstructionReachable(self, is_reachable): + self.is_next_instruction_reachable = is_reachable + + def isBlockEndingInstruction(self): + return self.is_block_ending_instruction + + def isFirstInstruction(self): + return len(self.instructions) == 0 + + def setBlockEndingInstruction(self, is_ending): + self.is_block_ending_instruction = is_ending + + def setSanelyEnding(self, is_sanely_ending): + self.is_sanely_ending = is_sanely_ending + + def addCollision(self, colliding_address): + self.has_collision = True + self.colliding_addresses.add(colliding_address) + + def setRecursion(self, is_recursive): + self.is_recursive = is_recursive + + def setThunkCall(self, is_thunk_call): + self.is_thunk_call = is_thunk_call + + def setLeaf(self, is_leaf): + self.is_leaf_function = is_leaf + + def __str__(self): + result = "0x{:x} | current: 0x{:x} | blocks: {} | queue: {} | processed: {} | crefs: {} | drefs: {} | suspicious: {} | ending: {}".format( + self.start_addr, + self.block_start, + len(self.getBlocks()), + ",".join([f"0x{b:x}" for b in sorted(self.block_queue)]), + ",".join([f"0x{b:x}" for b in sorted(self.processed_blocks)]), + len(self.code_refs), + len(self.data_refs), + self.suspicious_ins_count, + self.is_sanely_ending, + ) + return result diff --git a/smda/dalvik/__init__.py b/smda/dalvik/__init__.py new file mode 100644 index 00000000..33b8395b --- /dev/null +++ b/smda/dalvik/__init__.py @@ -0,0 +1,2 @@ +from .DalvikDisassembler import DalvikDisassembler as DalvikDisassembler +from .DalvikFunctionAnalysisState import DalvikFunctionAnalysisState as DalvikFunctionAnalysisState diff --git a/smda/utility/DexFileLoader.py b/smda/utility/DexFileLoader.py new file mode 100644 index 00000000..99034bbd --- /dev/null +++ b/smda/utility/DexFileLoader.py @@ -0,0 +1,29 @@ +class DexFileLoader: + @staticmethod + def isCompatible(data): + return bool(len(data) >= 3 and data[0:3] == b"dex") + + @staticmethod + def mapBinary(data): + return data + + @staticmethod + def getBaseAddress(data): + return 0 + + @staticmethod + def getBitness(data): + return 32 + + @staticmethod + def getArchitecture(data): + return "dalvik" + + @staticmethod + def getAbi(data): + return "" + + @staticmethod + def getCodeAreas(data): + # We return the whole data as code area + return [(0, 0, len(data))] diff --git a/smda/utility/FileLoader.py b/smda/utility/FileLoader.py index 47e08b92..695b7a5f 100644 --- a/smda/utility/FileLoader.py +++ b/smda/utility/FileLoader.py @@ -1,6 +1,7 @@ import os from smda.utility.DelphiKbFileLoader import DelphiKbFileLoader +from smda.utility.DexFileLoader import DexFileLoader from smda.utility.ElfFileLoader import ElfFileLoader from smda.utility.MachoFileLoader import MachoFileLoader from smda.utility.PeFileLoader import PeFileLoader @@ -16,7 +17,7 @@ class FileLoader: _abi = "" _architecture = "" _code_areas = [] - file_loaders = [PeFileLoader, ElfFileLoader, MachoFileLoader, DelphiKbFileLoader] + file_loaders = [PeFileLoader, ElfFileLoader, MachoFileLoader, DelphiKbFileLoader, DexFileLoader] def __init__(self, file_path, load_file=True, map_file=False): self._file_path = file_path From ebfe007d1aa4b68abb8f418cd34cb56e045d946c Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:53:07 +0530 Subject: [PATCH 02/25] Update smda/dalvik/DalvikDisassembler.py Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- smda/dalvik/DalvikDisassembler.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 253fa695..806428f4 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -180,15 +180,7 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) self.disassembly.language = "dalvik" - # LIEF natively accepts bytes or bytearray - if hasattr(lief.DEX, "parse"): - try: # noqa: SIM105 - dex_file = lief.DEX.parse(list(binary_info.raw_data)) # Fallback if list is needed - except Exception: - pass - - # In recent LIEF, we can just pass the raw bytes array - # Let's try raw bytes first, then fallback to list + # Try raw bytes first, then fallback to list try: dex_file = lief.DEX.parse(binary_info.raw_data) except TypeError: From 39e92dd99231a82a176760c0b5b1e740ae9b768a Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:53:28 +0530 Subject: [PATCH 03/25] Update smda/utility/DexFileLoader.py Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- smda/utility/DexFileLoader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smda/utility/DexFileLoader.py b/smda/utility/DexFileLoader.py index 99034bbd..9edcafd7 100644 --- a/smda/utility/DexFileLoader.py +++ b/smda/utility/DexFileLoader.py @@ -26,4 +26,4 @@ def getAbi(data): @staticmethod def getCodeAreas(data): # We return the whole data as code area - return [(0, 0, len(data))] + return [(0, len(data))] From c4e2b9f4b9cfb9254907ae83e8617e0124d9185e Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:05:26 +0530 Subject: [PATCH 04/25] fix(dalvik): address code review - remove invalid header bounds check and handle unknown opcodes - Remove incorrect len(dex_file.header) bounds check (lief.DEX.Header is not a buffer); replace with proper validation against raw_data size - Stop assuming length=1 for unknown Dalvik opcodes which would desync the instruction stream; instead log a warning and abort method disassembly - Apply ruff formatting to DalvikFunctionAnalysisState.py --- smda/dalvik/DalvikDisassembler.py | 23 +++++++++++++++++----- smda/dalvik/DalvikFunctionAnalysisState.py | 7 +++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 806428f4..21d03c25 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -103,11 +103,17 @@ def analyzeFunction(self, dex_file, method_info): insns_size_bytes = insns_size_units * 2 bytecode_offset = start_addr + 16 - if bytecode_offset + insns_size_bytes <= len(dex_file.header): - pass # LIEF parsing might just map it, but we have binary_info - - # We need raw bytecode. method_info has raw_data. raw_data = method_info["raw_data"] + + if bytecode_offset + insns_size_bytes > len(raw_data): + LOGGER.warning( + "Bytecode range [0x%x:0x%x] exceeds raw data size (0x%x), skipping method.", + bytecode_offset, + bytecode_offset + insns_size_bytes, + len(raw_data), + ) + return None + bytecode = raw_data[bytecode_offset : bytecode_offset + insns_size_bytes] state = DalvikFunctionAnalysisState(start_addr, self.disassembly) @@ -117,7 +123,14 @@ def analyzeFunction(self, dex_file, method_info): # A simple loop to read dalvik bytecode. LIEF returns list of bytes. op = bytecode[idx] - mnemonic, length_units = DALVIK_OPCODES.get(op, ("unknown_" + hex(op), 1)) + if op not in DALVIK_OPCODES: + LOGGER.warning( + "Unknown Dalvik opcode 0x%02x at offset 0x%x, aborting method disassembly.", + op, + start_addr + idx, + ) + break + mnemonic, length_units = DALVIK_OPCODES[op] length_bytes = length_units * 2 if idx + length_bytes > len(bytecode): diff --git a/smda/dalvik/DalvikFunctionAnalysisState.py b/smda/dalvik/DalvikFunctionAnalysisState.py index d8d894cc..ea8b3782 100644 --- a/smda/dalvik/DalvikFunctionAnalysisState.py +++ b/smda/dalvik/DalvikFunctionAnalysisState.py @@ -159,8 +159,11 @@ def finalizeAnalysis(self, as_gap=False): self.num_blocks_analyzed, ) if as_gap and not self.is_sanely_ending: - if len(self.instructions) == 1 and self.instructions[0][2].startswith("goto") or self.num_blocks_analyzed == 1 and ( - self.instructions[-1][2].startswith("goto") or self.instructions[-1][2].startswith("invoke-") + if ( + len(self.instructions) == 1 + and self.instructions[0][2].startswith("goto") + or self.num_blocks_analyzed == 1 + and (self.instructions[-1][2].startswith("goto") or self.instructions[-1][2].startswith("invoke-")) ): pass else: From ef65e930a49adc06d94852d1142634bab180ad7c Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:42:52 +0530 Subject: [PATCH 05/25] feat(dalvik): implement complete Dalvik opcode table (0x00-0xFF) The opcode table previously only covered 0x00-0x3D and 0x6E-0x78, causing the disassembler to abort on any method using common opcodes like array ops (aget/aput), field ops (iget/iput/sget/sput), arithmetic, type conversions, or literal operations. Now covers all 209 defined opcodes per the Android Dalvik bytecode spec: - Array operations (0x44-0x51) - Instance field operations (0x52-0x5F) - Static field operations (0x60-0x6D) - Unary operations and type conversions (0x7B-0x8F) - Binary 3-register operations (0x90-0xAF) - Binary 2addr operations (0xB0-0xCF) - Binary lit16/lit8 operations (0xD0-0xE2) - invoke-polymorphic, invoke-custom, const-method-handle/type (0xFA-0xFF) Ref: https://source.android.com/docs/core/runtime/dalvik-bytecode --- smda/dalvik/DalvikDisassembler.py | 185 +++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 2 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 21d03c25..2822e62d 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -8,9 +8,11 @@ LOGGER = logging.getLogger(__name__) -# Basic lookup table for Dalvik opcodes. -# This covers simple CFG branching and method calls. +# Complete lookup table for Dalvik opcodes per the Android Dalvik bytecode specification. +# Each entry maps opcode -> (mnemonic, size_in_16bit_code_units). +# Reference: https://source.android.com/docs/core/runtime/dalvik-bytecode DALVIK_OPCODES = { + # Moves and misc (0x00-0x0D) 0x00: ("nop", 1), 0x01: ("move", 1), 0x02: ("move/from16", 2), @@ -25,10 +27,12 @@ 0x0B: ("move-result-wide", 1), 0x0C: ("move-result-object", 1), 0x0D: ("move-exception", 1), + # Returns (0x0E-0x11) 0x0E: ("return-void", 1), 0x0F: ("return", 1), 0x10: ("return-wide", 1), 0x11: ("return-object", 1), + # Constants (0x12-0x1C) 0x12: ("const/4", 1), 0x13: ("const/16", 2), 0x14: ("const", 3), @@ -40,6 +44,7 @@ 0x1A: ("const-string", 2), 0x1B: ("const-string/jumbo", 3), 0x1C: ("const-class", 2), + # Monitor and type checks (0x1D-0x23) 0x1D: ("monitor-enter", 1), 0x1E: ("monitor-exit", 1), 0x1F: ("check-cast", 2), @@ -47,42 +52,218 @@ 0x21: ("array-length", 1), 0x22: ("new-instance", 2), 0x23: ("new-array", 2), + # Filled arrays (0x24-0x26) 0x24: ("filled-new-array", 3), 0x25: ("filled-new-array/range", 3), 0x26: ("fill-array-data", 3), + # Throw (0x27) 0x27: ("throw", 1), + # Gotos (0x28-0x2A) 0x28: ("goto", 1), 0x29: ("goto/16", 2), 0x2A: ("goto/32", 3), + # Switches (0x2B-0x2C) 0x2B: ("packed-switch", 3), 0x2C: ("sparse-switch", 3), + # Comparisons (0x2D-0x31) 0x2D: ("cmpl-float", 2), 0x2E: ("cmpg-float", 2), 0x2F: ("cmpl-double", 2), 0x30: ("cmpg-double", 2), 0x31: ("cmp-long", 2), + # If-test two registers (0x32-0x37) 0x32: ("if-eq", 2), 0x33: ("if-ne", 2), 0x34: ("if-lt", 2), 0x35: ("if-ge", 2), 0x36: ("if-gt", 2), 0x37: ("if-le", 2), + # If-testz single register (0x38-0x3D) 0x38: ("if-eqz", 2), 0x39: ("if-nez", 2), 0x3A: ("if-ltz", 2), 0x3B: ("if-gez", 2), 0x3C: ("if-gtz", 2), 0x3D: ("if-lez", 2), + # (0x3E-0x43 are unused) + # Array operations (0x44-0x51) - format 23x, 2 code units + 0x44: ("aget", 2), + 0x45: ("aget-wide", 2), + 0x46: ("aget-object", 2), + 0x47: ("aget-boolean", 2), + 0x48: ("aget-byte", 2), + 0x49: ("aget-char", 2), + 0x4A: ("aget-short", 2), + 0x4B: ("aput", 2), + 0x4C: ("aput-wide", 2), + 0x4D: ("aput-object", 2), + 0x4E: ("aput-boolean", 2), + 0x4F: ("aput-byte", 2), + 0x50: ("aput-char", 2), + 0x51: ("aput-short", 2), + # Instance field operations (0x52-0x5F) - format 22c, 2 code units + 0x52: ("iget", 2), + 0x53: ("iget-wide", 2), + 0x54: ("iget-object", 2), + 0x55: ("iget-boolean", 2), + 0x56: ("iget-byte", 2), + 0x57: ("iget-char", 2), + 0x58: ("iget-short", 2), + 0x59: ("iput", 2), + 0x5A: ("iput-wide", 2), + 0x5B: ("iput-object", 2), + 0x5C: ("iput-boolean", 2), + 0x5D: ("iput-byte", 2), + 0x5E: ("iput-char", 2), + 0x5F: ("iput-short", 2), + # Static field operations (0x60-0x6D) - format 21c, 2 code units + 0x60: ("sget", 2), + 0x61: ("sget-wide", 2), + 0x62: ("sget-object", 2), + 0x63: ("sget-boolean", 2), + 0x64: ("sget-byte", 2), + 0x65: ("sget-char", 2), + 0x66: ("sget-short", 2), + 0x67: ("sput", 2), + 0x68: ("sput-wide", 2), + 0x69: ("sput-object", 2), + 0x6A: ("sput-boolean", 2), + 0x6B: ("sput-byte", 2), + 0x6C: ("sput-char", 2), + 0x6D: ("sput-short", 2), + # Invoke operations (0x6E-0x72) - format 35c, 3 code units 0x6E: ("invoke-virtual", 3), 0x6F: ("invoke-super", 3), 0x70: ("invoke-direct", 3), 0x71: ("invoke-static", 3), 0x72: ("invoke-interface", 3), + # (0x73 is unused) + # Invoke range operations (0x74-0x78) - format 3rc, 3 code units 0x74: ("invoke-virtual/range", 3), 0x75: ("invoke-super/range", 3), 0x76: ("invoke-direct/range", 3), 0x77: ("invoke-static/range", 3), 0x78: ("invoke-interface/range", 3), + # (0x79-0x7A are unused) + # Unary operations (0x7B-0x8F) - format 12x, 1 code unit + 0x7B: ("neg-int", 1), + 0x7C: ("not-int", 1), + 0x7D: ("neg-long", 1), + 0x7E: ("not-long", 1), + 0x7F: ("neg-float", 1), + 0x80: ("neg-double", 1), + 0x81: ("int-to-long", 1), + 0x82: ("int-to-float", 1), + 0x83: ("int-to-double", 1), + 0x84: ("long-to-int", 1), + 0x85: ("long-to-float", 1), + 0x86: ("long-to-double", 1), + 0x87: ("float-to-int", 1), + 0x88: ("float-to-long", 1), + 0x89: ("float-to-double", 1), + 0x8A: ("double-to-int", 1), + 0x8B: ("double-to-long", 1), + 0x8C: ("double-to-float", 1), + 0x8D: ("int-to-byte", 1), + 0x8E: ("int-to-char", 1), + 0x8F: ("int-to-short", 1), + # Binary operations (0x90-0xAF) - format 23x, 2 code units + 0x90: ("add-int", 2), + 0x91: ("sub-int", 2), + 0x92: ("mul-int", 2), + 0x93: ("div-int", 2), + 0x94: ("rem-int", 2), + 0x95: ("and-int", 2), + 0x96: ("or-int", 2), + 0x97: ("xor-int", 2), + 0x98: ("shl-int", 2), + 0x99: ("shr-int", 2), + 0x9A: ("ushr-int", 2), + 0x9B: ("add-long", 2), + 0x9C: ("sub-long", 2), + 0x9D: ("mul-long", 2), + 0x9E: ("div-long", 2), + 0x9F: ("rem-long", 2), + 0xA0: ("and-long", 2), + 0xA1: ("or-long", 2), + 0xA2: ("xor-long", 2), + 0xA3: ("shl-long", 2), + 0xA4: ("shr-long", 2), + 0xA5: ("ushr-long", 2), + 0xA6: ("add-float", 2), + 0xA7: ("sub-float", 2), + 0xA8: ("mul-float", 2), + 0xA9: ("div-float", 2), + 0xAA: ("rem-float", 2), + 0xAB: ("add-double", 2), + 0xAC: ("sub-double", 2), + 0xAD: ("mul-double", 2), + 0xAE: ("div-double", 2), + 0xAF: ("rem-double", 2), + # Binary operations 2addr (0xB0-0xCF) - format 12x, 1 code unit + 0xB0: ("add-int/2addr", 1), + 0xB1: ("sub-int/2addr", 1), + 0xB2: ("mul-int/2addr", 1), + 0xB3: ("div-int/2addr", 1), + 0xB4: ("rem-int/2addr", 1), + 0xB5: ("and-int/2addr", 1), + 0xB6: ("or-int/2addr", 1), + 0xB7: ("xor-int/2addr", 1), + 0xB8: ("shl-int/2addr", 1), + 0xB9: ("shr-int/2addr", 1), + 0xBA: ("ushr-int/2addr", 1), + 0xBB: ("add-long/2addr", 1), + 0xBC: ("sub-long/2addr", 1), + 0xBD: ("mul-long/2addr", 1), + 0xBE: ("div-long/2addr", 1), + 0xBF: ("rem-long/2addr", 1), + 0xC0: ("and-long/2addr", 1), + 0xC1: ("or-long/2addr", 1), + 0xC2: ("xor-long/2addr", 1), + 0xC3: ("shl-long/2addr", 1), + 0xC4: ("shr-long/2addr", 1), + 0xC5: ("ushr-long/2addr", 1), + 0xC6: ("add-float/2addr", 1), + 0xC7: ("sub-float/2addr", 1), + 0xC8: ("mul-float/2addr", 1), + 0xC9: ("div-float/2addr", 1), + 0xCA: ("rem-float/2addr", 1), + 0xCB: ("add-double/2addr", 1), + 0xCC: ("sub-double/2addr", 1), + 0xCD: ("mul-double/2addr", 1), + 0xCE: ("div-double/2addr", 1), + 0xCF: ("rem-double/2addr", 1), + # Binary operations lit16 (0xD0-0xD7) - format 22s, 2 code units + 0xD0: ("add-int/lit16", 2), + 0xD1: ("rsub-int", 2), + 0xD2: ("mul-int/lit16", 2), + 0xD3: ("div-int/lit16", 2), + 0xD4: ("rem-int/lit16", 2), + 0xD5: ("and-int/lit16", 2), + 0xD6: ("or-int/lit16", 2), + 0xD7: ("xor-int/lit16", 2), + # Binary operations lit8 (0xD8-0xE2) - format 22b, 2 code units + 0xD8: ("add-int/lit8", 2), + 0xD9: ("rsub-int/lit8", 2), + 0xDA: ("mul-int/lit8", 2), + 0xDB: ("div-int/lit8", 2), + 0xDC: ("rem-int/lit8", 2), + 0xDD: ("and-int/lit8", 2), + 0xDE: ("or-int/lit8", 2), + 0xDF: ("xor-int/lit8", 2), + 0xE0: ("shl-int/lit8", 2), + 0xE1: ("shr-int/lit8", 2), + 0xE2: ("ushr-int/lit8", 2), + # (0xE3-0xF9 are unused) + # Invoke-polymorphic (0xFA-0xFB) - format 45cc/4rcc, 4 code units + 0xFA: ("invoke-polymorphic", 4), + 0xFB: ("invoke-polymorphic/range", 4), + # Invoke-custom (0xFC-0xFD) - format 35c/3rc, 3 code units + 0xFC: ("invoke-custom", 3), + 0xFD: ("invoke-custom/range", 3), + # Const method handle/type (0xFE-0xFF) - format 21c, 2 code units + 0xFE: ("const-method-handle", 2), + 0xFF: ("const-method-type", 2), } # The size is in 16-bit code units. From d09fc1dc9789fbe1460d3ad9e88f6343c9312264 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:05:26 +0530 Subject: [PATCH 06/25] refactor(dalvik): address 6 code review comments for robust disassembly 1. Switch analyzeFunction from linear sweep to recursive traversal using the block_queue in DalvikFunctionAnalysisState. This prevents data payloads (packed-switch, sparse-switch, fill-array-data) from being misinterpreted as instructions and produces a correct CFG. 2. Include class name in invoke-* operand strings for disambiguation (e.g. 'Ljava/lang/Object;->' instead of just ''). 3. Replace list() fallback with bytes() for LIEF DEX parsing, which is more efficient and idiomatic. 4. block_queue and related methods (chooseNextBlock, addBlockToQueue, hasUnprocessedBlocks, endBlock) are now actively used for the recursive traversal. 5. Remove dead identifyCallConflicts method from DalvikFunctionAnalysisState to reduce cognitive load. 6. Fix instruction addresses to be relative to bytecode_offset (code_item + 16) rather than the code_item start, which was causing all addresses to be 16 bytes too low. --- smda/dalvik/DalvikDisassembler.py | 237 +++++++++++++++------ smda/dalvik/DalvikFunctionAnalysisState.py | 11 - 2 files changed, 175 insertions(+), 73 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 2822e62d..9fe55b96 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -1,5 +1,6 @@ import datetime import logging +import struct import lief @@ -275,6 +276,82 @@ def __init__(self, config): self.disassembly = DisassemblyResult() self.disassembly.smda_version = config.VERSION + def _getPayloadSize(self, bytecode, idx): + """Calculate the size (in bytes) of a data payload at the given index. + + Dalvik embeds data payloads (packed-switch, sparse-switch, fill-array-data) + inline in the instruction stream. The first 16-bit code unit is a pseudo-opcode + identifying the payload type, and the remaining data follows a defined layout. + Returns the total size in bytes, or 0 if not a recognized payload. + """ + if idx + 2 > len(bytecode): + return 0 + ident = struct.unpack_from(" len(bytecode): + return 0 + size = struct.unpack_from(" len(bytecode): + return 0 + size = struct.unpack_from(" len(bytecode): + return 0 + element_width = struct.unpack_from(" len(bytecode): + return [] + ident = struct.unpack_from(" len(bytecode): + return [] + size = struct.unpack_from(" len(bytecode): + break + rel_offset = struct.unpack_from(" len(bytecode): + break + rel_offset = struct.unpack_from(" len(bytecode): - length_bytes = len(bytecode) - idx - - i_bytes = bytes(bytecode[idx : idx + length_bytes]) - i_address = start_addr + idx - i_size = length_bytes - i_mnemonic = mnemonic - i_op_str = "" - - state.setNextInstructionReachable(True) - - # Simple CFG handling - if i_mnemonic in ["return-void", "return", "return-wide", "return-object", "throw"]: - state.setNextInstructionReachable(False) - elif i_mnemonic.startswith("goto"): - target_offset = 0 - if i_mnemonic == "goto": - target_offset = int.from_bytes(i_bytes[1:2], byteorder="little", signed=True) * 2 - elif i_mnemonic == "goto/16": + # Addresses are relative to bytecode_offset (where instructions actually start), + # not start_addr (which points to the code_item header). + state = DalvikFunctionAnalysisState(bytecode_offset, self.disassembly) + + # Use block_queue for recursive traversal instead of linear sweep. + # This avoids misinterpreting data payloads (packed-switch-payload, + # sparse-switch-payload, fill-array-data-payload) as instructions. + visited_offsets = set() + + while state.hasUnprocessedBlocks(): + block_start_addr = state.chooseNextBlock() + idx = block_start_addr - bytecode_offset + + while 0 <= idx < len(bytecode): + if idx in visited_offsets: + break + visited_offsets.add(idx) + + op = bytecode[idx] + + if op not in DALVIK_OPCODES: + LOGGER.warning( + "Unknown Dalvik opcode 0x%02x at offset 0x%x, aborting block disassembly.", + op, + bytecode_offset + idx, + ) + break + mnemonic, length_units = DALVIK_OPCODES[op] + length_bytes = length_units * 2 + + if idx + length_bytes > len(bytecode): + break + + i_bytes = bytes(bytecode[idx : idx + length_bytes]) + i_address = bytecode_offset + idx + i_size = length_bytes + i_mnemonic = mnemonic + i_op_str = "" + + state.setNextInstructionReachable(True) + + # CFG handling + if i_mnemonic in ["return-void", "return", "return-wide", "return-object", "throw"]: + state.setNextInstructionReachable(False) + elif i_mnemonic.startswith("goto"): + target_offset = 0 + if i_mnemonic == "goto": + target_offset = int.from_bytes(i_bytes[1:2], byteorder="little", signed=True) * 2 + elif i_mnemonic == "goto/16": + target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 + elif i_mnemonic == "goto/32": + target_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 + target_address = i_address + target_offset + state.addCodeRef(i_address, target_address, by_jump=True) + state.addBlockToQueue(target_address) + i_op_str = hex(target_address) + state.setNextInstructionReachable(False) + elif i_mnemonic.startswith("if-"): target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 - elif i_mnemonic == "goto/32": - target_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 - target_address = i_address + target_offset - state.addCodeRef(i_address, target_address, by_jump=True) - i_op_str = hex(target_address) - state.setNextInstructionReachable(False) - elif i_mnemonic.startswith("if-"): - target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 - target_address = i_address + target_offset - state.addCodeRef(i_address, target_address, by_jump=True) - i_op_str = hex(target_address) - elif i_mnemonic.startswith("invoke-"): - method_idx = int.from_bytes(i_bytes[2:4], byteorder="little") - try: - target_method = dex_file.methods[method_idx] - i_op_str = target_method.name - # Also we can update API information if we want. - except Exception: - i_op_str = f"method_idx_{method_idx}" - - state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, i_bytes) - idx += length_bytes + target_address = i_address + target_offset + state.addCodeRef(i_address, target_address, by_jump=True) + state.addBlockToQueue(target_address) + # Fall-through is also a valid path + fall_through = i_address + i_size + state.addBlockToQueue(fall_through) + i_op_str = hex(target_address) + elif i_mnemonic in ("packed-switch", "sparse-switch"): + # The operand is a relative offset (in code units) to the payload + payload_rel_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 + payload_byte_idx = idx + payload_rel_offset + switch_targets = self._resolveSwitchTargets(bytecode, idx, payload_byte_idx) + for target_byte_idx in switch_targets: + target_addr = bytecode_offset + target_byte_idx + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockToQueue(target_addr) + # Fall-through after switch is also possible (default case) + fall_through = i_address + i_size + state.addBlockToQueue(fall_through) + i_op_str = f"payload@{hex(bytecode_offset + payload_byte_idx)}" + elif i_mnemonic.startswith("invoke-"): + method_idx = int.from_bytes(i_bytes[2:4], byteorder="little") + try: + target_method = dex_file.methods[method_idx] + # Include class name for context (e.g. "Ljava/lang/Object;->") + i_op_str = f"{target_method.cls.fullname}->{target_method.name}" + except Exception: + i_op_str = f"method_idx_{method_idx}" + + state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, i_bytes) + idx += length_bytes + + # If this instruction ends the block (return, throw, goto), stop + if not state.is_next_instruction_reachable: + break + + state.endBlock() state.label = method_info["name"] state.finalizeAnalysis() @@ -374,11 +489,11 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) self.disassembly.language = "dalvik" - # Try raw bytes first, then fallback to list + # Try raw bytes first, then fallback to bytes() conversion try: dex_file = lief.DEX.parse(binary_info.raw_data) except TypeError: - dex_file = lief.DEX.parse(list(binary_info.raw_data)) + dex_file = lief.DEX.parse(bytes(binary_info.raw_data)) if dex_file: for method in dex_file.methods: @@ -387,8 +502,6 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): code_info = method.code_info if not code_info: continue - # method.code_offset gets us an offset but if LIEF returns something we can map: - # since DEX doesn't map directly, we use code_offset as address method_info = { "offset": method.code_offset, diff --git a/smda/dalvik/DalvikFunctionAnalysisState.py b/smda/dalvik/DalvikFunctionAnalysisState.py index ea8b3782..a44c0f40 100644 --- a/smda/dalvik/DalvikFunctionAnalysisState.py +++ b/smda/dalvik/DalvikFunctionAnalysisState.py @@ -114,17 +114,6 @@ def backtrackInstructions(self, addr_from, num_instructions): backtracked.append(instruction) return backtracked[-num_instructions:] - def identifyCallConflicts(self, all_refs): - conflicts = {} - non_instruction_start_bytes = self.processed_bytes.difference(self.instruction_start_bytes) - conflict_addrs = set(all_refs.keys()).intersection(non_instruction_start_bytes) - for candidate_source_ref in conflict_addrs: - candidate = all_refs[candidate_source_ref] - if candidate not in conflicts: - conflicts[candidate] = [] - conflicts[candidate].append(candidate_source_ref) - return conflicts - def _finalizeRegularAnalysis(self): fn_min = min([ins[0] for ins in self.instructions]) fn_max = max([ins[0] + ins[1] for ins in self.instructions]) From ab0595989a487c2d79b5719ac60427a0138571e2 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:41:09 +0530 Subject: [PATCH 07/25] fix(dalvik): revert LIEF DEX parse buffer type from bytes back to list LIEF's Python bindings for parse() interpret a bytes() object as a string filename, which was causing the fallback parsing intended for raw buffers to silently fail and return a null or empty object without resolving methods. Using list() correctly triggers the C++ binding for raw buffer parsing, allowing in-memory DEX analysis to work as intended. --- smda/dalvik/DalvikDisassembler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 9fe55b96..b425b81a 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -489,11 +489,13 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) self.disassembly.language = "dalvik" - # Try raw bytes first, then fallback to bytes() conversion + # Try raw bytes first, then fallback to list() conversion + # LIEF's Python bindings for parse() interpret bytes() as a filename string! + # Do not use bytes() here, use list() as it correctly triggers the raw buffer overload. try: dex_file = lief.DEX.parse(binary_info.raw_data) except TypeError: - dex_file = lief.DEX.parse(bytes(binary_info.raw_data)) + dex_file = lief.DEX.parse(list(binary_info.raw_data)) if dex_file: for method in dex_file.methods: From 6220b40bf9be8675dc2213c475c6dc6252a558f8 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:49:52 +0530 Subject: [PATCH 08/25] fix(dalvik): fix insns_size extraction and core compat - Fixed LIEF method.code_offset bug where code_offset points directly to the bytecode. We now safely backtrack 16 bytes to the code_item header to extract insns_size natively via struct unpacking, avoiding AttributeError since LIEF python bindings omit insns_size on CodeInfo. - Added addPdbFile to DalvikDisassembler to fulfill the expected interface and prevent core Disassembler from crashing. - Enabled native C++ fstream file loads when file_path is available to avoid massively slow parsing of 50,000+ Method Android APKs using the memory list fallback. --- smda/dalvik/DalvikDisassembler.py | 42 ++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index b425b81a..03ab1dff 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -1,3 +1,4 @@ +import contextlib import datetime import logging import struct @@ -276,6 +277,9 @@ def __init__(self, config): self.disassembly = DisassemblyResult() self.disassembly.smda_version = config.VERSION + def addPdbFile(self, binary_info, pdb_path): + pass + def _getPayloadSize(self, bytecode, idx): """Calculate the size (in bytes) of a data payload at the given index. @@ -354,16 +358,22 @@ def _resolveSwitchTargets(self, bytecode, switch_insn_idx, payload_idx): def analyzeFunction(self, dex_file, method_info): start_addr = method_info["offset"] - code_item = method_info["code_item"] - - # In DEX, the code_item header is 16 bytes. The bytecode immediately follows. - insns_size_units = code_item.insns_size - insns_size_bytes = insns_size_units * 2 - # bytecode_offset is the absolute offset of the first instruction in raw_data - bytecode_offset = start_addr + 16 raw_data = method_info["raw_data"] + # In LIEF, method.code_offset (which is passed as start_addr) points directly to the + # Dalvik bytecode instructions, NOT the start of the 16-byte code_item header. + bytecode_offset = start_addr + header_offset = start_addr - 16 + + if header_offset < 0 or header_offset + 16 > len(raw_data): + return None + + # The LIEF Python API for CodeInfo does not expose insns_size. We manually parse + # it from the code_item header. It's a 4-byte integer at offset 12. + insns_size_units = struct.unpack_from(" len(raw_data): LOGGER.warning( "Bytecode range [0x%x:0x%x] exceeds raw data size (0x%x), skipping method.", @@ -491,11 +501,19 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): # Try raw bytes first, then fallback to list() conversion # LIEF's Python bindings for parse() interpret bytes() as a filename string! - # Do not use bytes() here, use list() as it correctly triggers the raw buffer overload. - try: - dex_file = lief.DEX.parse(binary_info.raw_data) - except TypeError: - dex_file = lief.DEX.parse(list(binary_info.raw_data)) + # Parsing a massive binary blob via list() is extremely slow in Python (taking minutes). + # We must prioritize loading the file natively using C++ fstream if a file path is provided. + dex_file = None + if getattr(binary_info, "file_path", "") and not getattr(binary_info, "is_buffer", False): + with contextlib.suppress(Exception): + dex_file = lief.DEX.parse(binary_info.file_path) + + if not dex_file: + # Fallback to in-memory list() conversion for raw buffers + try: + dex_file = lief.DEX.parse(binary_info.raw_data) + except TypeError: + dex_file = lief.DEX.parse(list(binary_info.raw_data)) if dex_file: for method in dex_file.methods: From dab84e9af521a349f5d13ef1f1430c5a4c4cb7a9 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:30:18 +0530 Subject: [PATCH 09/25] test(dalvik): add integration tests and raw buffer fixes - Added DalvikDisassembler integration testing using a minimal XORed DEX binary. - Fixed a silent parse failure in LIEF Python bindings where \ytes\ objects passed directly were incorrectly parsed as filenames by the C++ engine. It now natively falls back on list(buffer) to forcefully utilize the memory buffer parsing path. --- tests/blockblast_classes_xored | Bin 0 -> 247668 bytes tests/testDalvikDisassembler.py | 185 ++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 tests/blockblast_classes_xored create mode 100644 tests/testDalvikDisassembler.py diff --git a/tests/blockblast_classes_xored b/tests/blockblast_classes_xored new file mode 100644 index 0000000000000000000000000000000000000000..21fb78eb084e9ea18278cfe46e5b155dc51feaed GIT binary patch literal 247668 zcmYhDk3ZA@*N183chm@tMiiQwq9_U>G&Mp9Q3xS~5E>zb&5 zLgQv%q-D%@?vgVmY(J#9XD?Q1-+tx9xdWD{-yf*oH9$%3DYOA&v{D*S&} zR$Kk5snh;h-`JVd)J#cfY5SDkPGMwqc8`ShP^TjM2JXku25-d=4ab*{jNPvqpLDIA zqWd*WGmhoX&Z*_kFQST;n16~_)=q8!TQU}@dU|Z+z5(T4+kaXW6mn7Rc>nP>7E*ze~?-_iK4pt&rk+9{q070-wnp5 zt{crP_<1d@4{Wlr8{Kd3@cW>n^T)$3ZuLjqJ=1r1`6TS}^>^PB5M-hdOla;84X5=+ zM6G^|j^%tGm!Q&`NZj!P>fe=?ajYXV`^-WPseCrC;KoWJnXyz{`gOdlVsNCgdf#+S zozhf&qjhpqvwdPqTTo^@Wm8&bH!7lsDiGQ?z!^syJQ_VTjI10Po2VF{9BZ7SQ|hJ} z&kJVf?vmygV@j8pA>@^{cndZ}x+w?OLpyH1ZfkzQH~K=zaUEgN3A`AJ4%{53_a%|#{UstYQrg_qUTNtZM%4+6VP2J>V zedFVdre^KzmNrvjJ0&EwvwK@?4^<_hZ(vtAZSdXAp<(8;k+I?DKpi4OB)<(7#e;WHZqnNIX-zMW{QrDpJs?9&Cbmy z&o6#RTVg)XTv>CrXG6p}a&SF%;pS^`=NBCD5<-gm3yba#5<{IONTBgLQZhyQvT~s~ zdCUx65&PX#Nx9cTMYYLVO?~e<4NaFFS~@|yboGUH8{pLU8sS^^8=HPPXl5~b*wQ-v zsEu9a278C7jgHQ#ye@7|Jno*(B3?c=n|=NDBm;t!#DfX+iO_KN(TJ#R-=kxNzs4mT zT1X^fedXZOP*HuU^YA-G=?klT^=&7v!I#g5lj#l4zDzB;e zd`?T-mEv{^TVZFnYGn^~cUj*+M!f?GaI7QfrD$(^;r$g{#{x+gFAHfNB0`we(g5GhaNOGeX`%oqV%YxwbcO| zJ6|4q2YmrYXB&1Gx80oXo-*QIKBAJo{(_q!|3rcbJHLj8AN?K?Wjh)jdwwD=!EHH_ zn6#3dN}Ns0cs`$*eZM`2wAh(f(AiW-rnVH9?xK}dpoc1}2|YD+)_wJjt>mWW@zR#I z-=ucR&4SME*t#BSO=I7Hc?E6oa`n*g?%0vB!wKV)!r@c&FE^(dU+>M%4c(t#Y`n9? z%)Gm@cI6Tq;`U_@u16QR`D!lm3-(_VLN;9&7M;B!h7$9ZKp#0NB_n7kD<`5Sk0EF) zVr^}el%H6ss1}&3sjrx5Xy)$G(s`z!t8XD|fD4i`!gFdEn;ud#v!JP1TDK_K*!A(- zJ3QicbdKO~al3$U_q0HJ`KX|L{pCdif`pO5gf&KJ_;z|kl`e8VQ8@hk*a*g^;68!lIvD#ZW@-5@mrpN;uYW&xKu|Y-FyZiQX!!AY=>MhYSgVz|1mBTF;`Q<5RR5{84A<$* z>^y1?>Cr%5LDgU(`NnW@X+m>Z#dup~HH`xN-Sv%H6-~`%)h%sj>e?y6jh)?&q#i1w zpl@I|nKt<9@z8M2osqHRyW^8r?@iH7?oTr|-k6=Ehs`h6#4j;ZV^-Fp{MZnWy*apO zA>4dRf&7B}jzUO9dtuRI?qVo=7YTHOnUu_DV_CTuHu9L=){0n+zLGLpM@97tUQOK< zr=h8&s-?qF*3~C!8sKaX8{x%v8JkM&Ftb?MYiWIJw~d{PxV?jcq@%O!W*0Yv2;@JH zm(M2wU;kI^0YMd*)CINGH;UCYHScX~Y3q2>PWke*vpe)r54GTN-+=K0 z+ThiPL&LK7M#d2L$0w0@rs%6l(~RNx*|}#i^NaT)mzbepD{F}%YzR^y2iFHbsJ}PA zps2eLa=(kP=s8C*l%GBD+epb|TguA)G?T{+7%O7s@Jh-YIN;Y;Q@7U9(A3n_(s{40 zt3Rn~fQwW%!dGC8O&?&)EY9t+v<}>1V`sPB-hpGAqcd`=i`$Pa?w%E!ynIS|ef?uL z1_b%81`{&bL&K{%BchslqGOo?kbj#KiDn|nsaoP`8CNAUvq@4pr024E1!?kyWV&K; zDMP8OqD7^$`mkC}9j``xql8vdbBlgU8xz+~d4%umeq`E1J!jE3pkqxN+++vu--VH} z`itX}CofIWH(Z`(botHB5xnOYEkc%<>VYe3?<3g|Q(+ujJaOE7lF|Hv`;(#miNd1V znPRA{GzoM>o|H^Rj;!2Vu{;JtR>UH!m6WeksHj>ss;OJlX=qZ~v~+0Ay86rA2Do^N z5k7Xn*tC>tW}*AZ(t6tm8#|#d_71b39i5A2T-;*5xqDt%_VO`U_4VKWBOvI@LNGy> zCp6q&AR_7xTXgJKj<^I~u|%S+L~^Q=P+G=Q(ah{0m>kj&Hm`tFrjUG9uDG<~a9Kst z(aP$H{WW!m57swcKGD=HeyXLdO|PBuOS`kX+O~&kWYsqyX-*qdG#MJ6a~~P&a2cO` z=r~2su%BkQ2G7nN2!Q#|cZoUcv$D23fem33%faOx#m$!<&M!#L7DB$s5EdOx6+b9S8Nmd7WN5gL zXhhVbZPBr>x5p)X+mJ{+yfHb|h&L_6ep6<4;(;8}pMCKDA1NeD9x5(1*;!VRD^*#& zrdU(gD-ZLZSyMBIaZB4_n|8{Kb!Yc2{T}LRoxTAVJZ*3zZfJPyD)3){`RDo+-SOHq zajCDZedMQN=z`Q@{LBL$mFJmQKVcUHzwT4RGGQMtHd{W7A(9W){R& zOY4FcHg<+9_6|x*j?UBbE^Y&}?w*;`UOp~#Uw@U!fS{GJV1g<-G~68(5tX$yI`+wy zxP-M$i9|l$BtRhmlviiMPO`W1ded7lE zrse~VEp5|o?UWmyo!!PhJya+EzJVh_kbl>QhVzb%jHMhOpENu%ML%?Enz3j;JNMpX zevxFm#PqdVS$mq#hIpUL!S$P3wNS|LhGW=puNoOS~6 z$0}koQk9gML>1LT_tn&S?`dete9_YB_^hk{bH)Hy_{|8fyKHQFY1Pa^^M|E%&w`B| zt;60Sw9C;su*t>Eti|0^lIG>}Zpha^wWl4Dd#Ve~FMQZA5gz6g^?oG{=E-h_N zj_nkreP{PDp@&Kg>Kh>W(+0hKhK4ucN5+^Zp#I0F=vR(SGi1-r&i&DwU#v7-ViHcS ztTmOhAr`7QxI}BX`Lyc!1@GhtAxrY${VxRnOC`|0L@Al7R9U&140%j{wjx&IhLW@wt4xyZua$0?G6aCejQAhI2am^+#eC8eKb1u>fyMA28Bc-WlwS{uXI|5 z<<87(Gw~eK4T-!0rcfanBV1g{&0SWZ%U@aj5K&V%%~ju68`RW%iO|x9^=YT{U4{I= z)I&AC+&AEVfi`&L;?VFf+mW#ftMSPY^C|jKlWE4P(d=BO!TjPK-6f`(*2>z4dN#zS zIu5QA)!cmE75swFONEdWvaqPYLot-`0}1rm`%*IT_hjW3?#g41epkc_{!mi3Tu@Pc zu&ky&zp9~GAEu?_8KJ9>j5ffv#2MkgBpRFEN{0N;u(bBbwy`@*vUeCMaC9CdySP0q zb@vQ-;^lMVsjt7tvw)ya&w~kO$3nwnwIiY`PDaO$8^$GUG*2X&n3nVCJ~ zm_zz#pI0F0UPumcDK0heh5g5;vidurrtW1>eWN2+Q!_WBrEQ-e?7zI7-8Z)OP{~{R z2K=_u2HCd_4Kox*#y;#BpNy5Bq7!ybGn5X^&WRtHUmRCjVwS6{tX*qoLu7Yya8a7L z`Bq!_1rH1gA+3jnMV+Z)s3!vw=vKOvjM|K>T+nBEOu-jLZ1`^_<#&HnR1Ykusq_5M z(3FeU(&>%S)xQ;KfO{Thgg=#OY>G@Xv)G+%X}y+cV^>~m?~qmK=xkWv;)Wr)d!ib> ze7@BA`gc|b1m#r(6E5OI!y`{bL}eV0jx9VEmq0p~NTi-kPF*^amO($AnJsiVha`R} zufXYIA^GG5$p7OZbEbu@2i=4S7q?&s_qkmjKc ze&Qb*o>m$ed!;fy`AKbx-lH+iXpownBg)P%hRQE7uPCmpJ?LRWJnQ4&dPn2t`#r=j z$k{A}lx`Ci#ZkmiA>9(_{5dI^`bAl}N~Sy}W)1RhOi6iiQbqL(T}_?J(9qPnqoreh zS6AQoo&nD3z7gK>hOz18n`Rc}Ov6O@`?WMG$=1DKUH(*ZU_FqMNVd6=8;$soUAQ=1TS zwOLqn>l@hrUrV4j{F0Ip`zb3I{6`*h{I??Z$rmN%;?F9oDKl#7f4*sGCf?Q3DZZnt zZDiHVzHHK0Gp( zb98*NZ2uHJ;ovmm=I+_Kz`bz)+Ofpc+qJUx<^vmI@Dt2G{oH(Fy)gfD2_biM2#eaa zilI)tkU)p5O39cn%gQ}okjFgvp@_};4*CC8MU^t4rmjAsp=pz;rQ@BZt1p;rfD=zN z!XJq8E9|>11YaBIJ;pW;#r&$0?5x610q zi#2tm3-yi5+nbtiY-?#FZf&R3Z0YPS-qb_Y=7s#*NE>wCFf=TGbYx89@c1PA!6~}P z{%OYK=GnPn5y*e>C1#T3%39h>rp*Y&BW-lM6h6NfZ3mvgjqZsh6eM->|2@`{b{jb+BBXR6IC z%4p%dIaM~GfNQ>niGy%_;PZ?c04nhv4ihRzXDW^U23 z&Yp1zNmmky*;kWO6RxFYG+u}I|5y&`%kjK|FDDAgZ%!4LcAYM(*mtI~TIFm_o%}hN ze|9uAv+rtYliA%)=~n3MesiFQ`e$F?K>88dVB?{o;hUTzW4GDIC!++W=t(@&3`ddK zIrq);i%ybDOiS^VwW0|&1bLK$tLi&9-JQ0>bS=-;hUGA%D; z<+{7%F)uq5vCaKT%3ZxGs;@q(sjJa6G*9F~|K~ve7aQQ*3yttWmBywHWo8!J>n*J} z*Vxz{jIei53w3nni*s?~h<5ixCwuv@C;IxcX9fgqNDC%#IEIGv*+)bvxJSpTxx^)$ z@l7P^dL^e`3r@@M2*}K~)6F4O>*N*O#21ohaK)u#7G)LROe?Do+11pkS=Toj$Tc+^ z$h5RsV%sU!n9lB#sy)=J%6$V~nzTW0^`T)Cu8}bl#Q3C%;1r#gZ<@g?Iy=YQGQT*q zeTiAOZDq~#3+(@&q5d=6e68R31)0BukSeRfqNjd{p&S=r|LKsDx!ENvcl(7r=4PuR zHtfBU@~@97s#Cpc>J$AMnuSGLI^`w0`cHBVaE5tCcr0h3c6*}j z9oXX>oi~QMxQRr#doHAT`AlT``i~?A1Pvz#6O>&-!`0j)qKxce|8<1@Hz1Md8JwIN z?~|61=bxEP$K{ZI;qwX>bqdL^^@~eiS(jC4*i}|zO>64V7WIu+Fip)t*p{|UGVK&G zxz27S^&aX`&Ax%X%5eWXI5fP#3-iyW@yXT=Q}o9hA^*3{&J}N;Uo69*33e%%F_Bsxs4rfyuE`+4D5fA zE^gn#+&#y#ynM#eefii&PO+)^jC@O*iEKONqEu%$QUmV4 zYJCHTRA__hN<+g!{3Byx+~boYTvPOR#5Ch0dUlS2nqPb>y2MOI0zZQdafQyoU6qq@*nTK}D7Ootiq~t%fG@ zv6hbbBVB#9hX%OL2S)gh&y7uAJ~OkJdunMt`ozYr^ma)MjGn>~Wha_U2SFpvZ zkbK#;xYS*{tOBoBS)F50Q+Lm(zHvnf{{ErT(pIb1PSMxs?Dm%Gp$g0P4Q!C74enDM z8lFPJ`!6~^`4%-rC!?ns4-vC-*0m6FMSCo5O>K^}AWlOlHXrIPZ{D;3q|*J|pD9U7WWRa!cPa$SA# z1_KgqM#_sIR|jTtJXW zbTDDjH#D5hDhSj;xgKi04E+5E3-3Q>XgEN0WK0V=KBk+|(_<+#xq+r;8 zy`p0qed7`yyCf26?#Zc5_GuY^95b`qE#dD!HhBfT#)V{Qv*J=AgR%-9qsr>*S~Yc7 zbm9J?2K%2zOPh*PJ7rk4v%4Du`H$@zn3kapPRI=nKR}I)6{5!{>ycCRXwhj#Cg1E_ zl;HegBw~qqooi(+fyss-u5obPo#W zzLm#Zey4~Hd99>;;f;#wxtD6{Ctkt*WzqmPVDXC9kbBtEdT zCO)*WtGWk&|GE$U-*ItEx$Exf6Yu47A;#C=GBP0ObXYLqWk_iFyTFL3w|;Q{@QzDZ z@kk_ocTG-}bV|$MI-i-%Wt&6dv&t*DWDfJcNpY#i$+8M7!^&zMy_&jWt@_3Zji%-~ zwU)Lpm3B(8QfD_wv4?6R-#1_&OB*~cH8hNq7#Xt>8=u4oPtkV@!T!fTJEy=szbMMF z#9T$JtZiIlLm-(PTzeL|`PAn41uYmtNNu{XsQ;uGDt$}>T{$cz6FMj>cYi=0GeK3v zPIfCPf1;?UcDJditG?9GJo{Wr=khaMeXpkmIQAz-c;3gxru!b5S*Sj=w5C0ikyu9K>RaJFyO>JFfeM941(~IUCEv;=S z?H!bc&aQ6Lo?fbcU;n@r+WSH0p^wA6M@Gkvj!#T(nEFcJHvOHkIQL`j>*B&~tKGslMS6kE2IMDK^?p^7BDg~X*B(nZZ-9tp*H_xPI`7>QEX+IxoK&2 zjh%}fA&B7QLJRWn?dB5@G!fm5)J2Mj2B5`JS5T7ZhjP*~RWdu}DzOTf2+SUAwCX

v4533*1EY{c7snyZb$DcC9*`7FwH#%ct8gkm)!ttDyweVS6yCWCRI|yEI zau&Vp>h|Z7hiC6qZy(wfKmXS2fk98Ng%C_{hK0M_h>VH}i;2Av8J`fFoor*Vc(wH8cixylCd@YHi!~qJyH; z+SUDu)=Pak)IYG)^L}u#@8j^$)aY3C^u%QR$X9yG_;-fi(vLZ(m4!v?*=6Rb`PH@O z-0TPnKPMNRjfd|Shk)Ql)Mn&TNXy``JLU3Z6fo6td$1|$`;>2J z9#CCTKBUf2J)(IZr>#?j*VC`kF~r#%KZ*A_YhrrgoVkU>X)EhZXMq3Gd55i+ot&pH zxVrtm2>jQ)eFm@l`Mwe4d_?T9T5U@i8eY zn^BTWYAVVv7|bss_vV(A9%v}9*jrmwy}7EkPM{q4yIwRSI$GN#T01DaUvzcLe(a@w zr1cNH>3cso-}7Kn){;&xMa-cUORZ$|M%A(>ZDWoLY11l|a4YN~DPfh`2BC`jZr@2o#P5prCAJs$Z zbIM0FbMV?aRX9EU3VlPI!LgHggL5XP{%6fC{LWZeyPdYRlf8W2Vb3KeXSs{6Zt532 zJU3tW_Tjwd=g)aHFzDBn5CZacSh&Qk$S9GUF|pVi@d-!nClTfDrKIk-o1US2Co9|X zaW08mm0xhXwupSEp`>&wx4fb&zp8p~5%8BZG;Zj9(Y(9Awe1tFgEBwV)g9B+OFh%l zKXAPL{h(Xt$6?Iu=-8q8iAk}guk$@Lz^ z!}l32ASk(EGg57%h^Qg2ILc#_B>M4wX_-3*cgp=dtbo~jXb)Cl=RW04(g#$1_8d|_ zt8he9#avsb(?n07YHNu5Vs#SF&@(ZO*EY9!bkfS&*ud6K$M?L0rI(YlTCl4dPk@If zm!r4Os=c58XZOIMcP=4>BgtXm=82I}E}1d07t`VsLL!ogPN6BO4sq!jiLqJPPpflD zQ5E?Gk&Q*O4D- zXcDZnb&$4t`tl}*I1TfYc#NTm>F`N&i#OU<*1z>^?cM~QcNh$Da(?LT>Q?UO;ThxV z?L%<)^S@vp7<9rhgwUQA7Cw*}8AVTwiCs>PPY{hxA_~N%q#g=Q&%j1xWjoa5lCIb1 z7wD7~kNV{UY2Ve!M~WoFNp)wP4$*b(a6Ik}E+;o&ph zDj?|1yBSH?BqDlogE%UABh>$}v`qccopLw!D`2t@?!n&Py-#^!?*Y}B9f#CE?>eHH zcUD`c`kbEr)6<4H9g~yzQzuPKeGJVl-1Mxh9kp%kc7~jHPzZE#R`zpsQG8d?BS&Nggx;|M7x-j)Kih^8OOr1vJDz?Nw&55 z1!t;?$n|9~Osda+rEi@5&S0PZF*i?NSe%|*W_}!7UE7UjM_^E#TuP!md?%0s zg0_O2k=}eFqF1=YQJx4%w4J833|W1rT!pFvCQ5k^_9hncA9Fx;O74*Qgv=4mQaf!O zt+RUidS?uAW~Wc$y-t~!2AnXra6JzE$87E7uAg_ToU-7QdRvuy|(UMRzu^l(HG5UCR*FheC?oke(&lI{LxDdSm+;cS$;q0vI_h! zM#rpMCnooFe5FfweP_t^{+N^RUs#lQzsy8_TwR-!W=E{;ACs zMMQMB8swjbBs!Q4_&IjUO>-+?`uX=@g^>G{&x;;VeTF)uej9y6^NxeIPJ^?azTqW9 z+=*K zKL`s~d>9#}^e84)?{R#B?vo^<|I?JztIyIioS$c9mqzB2YQypi(&LNBGck~VDdiQP zlB%lPvTExT(;FHsCtfsP7;SC4_PvAR_qD4VxzJ0M{Lw$ayZU~RZTaIcqIGmkktB4L0C9~ z6dAR#G$vMv9G|eWI*E9uA|=(NF+JmST~@YlR4(aScz(gjgd(zbY)Pq6YI();J5|-W z_iO8j_Zk`_zr1Kp``p@AH`75e_}0~}^{bbv{j+}{@Xz}}|KA^n&%A>Aznqwqe)E;i z|N1*)+q)lg8{aN0Zuqp!TXw*G|Mnr+YXI&P`bZ#b-()EOa1kye_-zC`@yMSABP8DkB;TMnV8Ib`IX-I z>N_Lp!;iUppB5Gq-Yzpk->t3@cC#a*_HuG1?BL;L-JpQ!+_(oz+qO^n!}bHJEn5z$AKiRJ)5=3z$Jtd+|B4gvpFfG`2r)6` z3pBUb?Pq2E-rLr0`Q&+r4nrsB9z9pL8f_2HB3o~tL@VGo4-5)72_eK5hlQsVMn*N} z#l&{x#3#J0Pa-bVq@+$)re}zjWo4_!<&p%V^9zI{ipU|MC8cLF%PVx!s;cW!YwIeA z4UL107tJ&D*0$)$4ocD(%>QeU|IGdY|Hb!%sJV~B%H5-58z>W#B5hykt1sdG`vmX* zhlR!ZcgxIzx2tPzd)X0$-JD$MyLk9Yb_fU-9o>xlbXY{R^`JQF$9_q)>}F}1eIh&M zc*GSjY?6DhUhMmnoj4DuI`SM+w-Pv_dBj^=r^`=IzbDWTHy&~lpLE{DG|b7|qTJQW z`jLmN-Ff5l4z6ZS&ettn-L!2yJn>rIK3H8pe_?~bAaKCSGxEE(-TPNm{ic|9oo@Nx0GqX!d6T-_YW}>R9-^SL~wIwt(8qU3F zKDP+(KeL0vwbs?WajchmV6uN;KmGlnEaT(w6~%=|a2Yo7br5w{LI4ew2Z*!opMM? z1&qD;9;`FxKIJp)2UKwahtwN*j%dE~)7F{x*3*v(F~mg$p2XjBGBIs9Z*F1YZe^|O zVr!>scHY6(*vVPJ2IfCY4^I(YZ=X$Ce*SAlfkEF5LI{$TVc`m8kx{DkF|l?v@d-|O zNyKzgO6v2{bRf>k9!t$7%@gwr7PE`Utr;by?NQ|w8sRYiCDhhQ#WplvSbWiZb*{C| zZ>@tO$n5GyP4-fG#`*`2GTslW(mxJw9T*)Gr%p^VKYgYD`tY66|LVuwtCtIlFWxLO z>t3&}nH+@qXFtq8M|t={4hsl+?AnY>+#w>Gv{xKO+%1W&+A1w`bIVS-wCxHQ;4gwvYQw_i8zQ6jm&e57tKt)Ei=h4`DXBWS z=@|j}S=kopxulZJ{DPXqB63}FNojI)dBx4Rs_Mwl+PXgx4UN%DFPgJfTH8`*J1C_2 zu5R*FFV$kYf533${h-$P$6+(t=$Pyfynj7k>AU*AGZfl?%x&#lSQKblW+GZv*Lc<0 z5ki`rTzi#y_>QOw2x2gskycp9e;IL zTPkJX-N4e|hr!k1Ps13~n{;cgrY(QYS zHgd`G@$yLvZW26-+=5gW-72bx+J;gQn5k*ujL`3GKu?#5bvTQ=2pHWi(~q z&t4@xApI_QSg=TbMD8qoT-salq~cxm)9OLsrvZOH@RNW)7x;65|2gp20e>OzmjM4A z;74fxZ5|32`OfIk=bbAkUM@Yex< z4e(b1e+BUO0zVb_dx8Hu@c#tRs2{gD**p4;Fke@d*F8h{!75`3H-jm?+yIdfj_p{%qin0RAxGzYYAc zz#jnoA;5nH`2B(39r#^=|03`^0DsPd2c-Om4+|bWdPFXL{J6C8$&-rmr%$V&KYLbJ z_xyQd`^%TjU9VoXy?p(e()Q*}_t4w7)c5b+4ZQpCVUYUi)9~!)&tnT;zD)l9_Km(a zGs77F`Ezdc*RREy-@lpkKY!N5Hf%shZrsSVg_oC4XwxRaOCy{A<8J5BxuXe+KyJz%K^;lEA+M_~n3K5%?8={~+)y z1HTgR9{~Onz>f!hUEtRS{&T=@4g3bce-ikw0KY%*2Lpc~@LvRe2jI5{ekb6+2mIN< zpA7s-z<(R~V}ZX0_#1%#2JlA#e;n|~0RLU!Cjx&W@TUO(H{fRg|2XiE0{3nm0e>s-9|nF+;8y|ueZapL_%Xn53H-Lee*yTNf!`hY zU4j29@cRJ27x4Q5KQHhL0zU`vvjhKj;70*JH}DGp|9;?C1%3_SKLq@{fL{jqrGS4Y z@P7dQVc_or{$AjJ1N;==UjqJB;C~7HZNT3N{2jpm4*03S-vj*p!2byNOMyQR_;Z2( zIq=s3eZ)p=H@J2||9a5> z+R*xc!|NBAzdqe++K>r6q|3^aqhe7|x zL;uG>|EECzCqe%M|3~2e4E$?3@ctJ+BC8fYE>*02QlU`xw0c+lvpSiY=Z*F)FPoj3 zUbS87d`SJ8SopQL zy!@M)+`O`Ok9`AzlXD|ifGYg{gMX9Ymd#s`?igXwez9#RwB&ZQl=Kc6y6i4FjKXeA zF;WrxS!utr>H+9~E;V(A#!*cj?PEHfLAv^x1}AX%lc(@ap2ntOW@jv{t2~Gm;v9v6hi*P{KKD6R^b5iPhIV^x`6QdM$x91&26pV|6)4@ z)%m8I+WVH8^A-O7N&7H3{PEMU%J2yM{o~6d{p&Zn5_Os(H~VvL5%^bHmYA58KWhl! zA5n+zAGmq>1O+w;uE2$(OL(g&N_-n?N=5>`D78aIZs#sJ`K|I8rs7_#^1l7b8oVm1 zAJq=4tHJkgTEPE6_qe{1;R&406(f8#eE()_e#S!UqNR16%{e>A^A{X+&pJAXx?FN| z@wn`1dCJR&=zG;aIPh9f)A3+}Yv_&e(8!xnHBX~s-QsR1Bq!Y=CKe{A64LHvq-EXD zPP?B&I++Ll!~8dWr?^xP=D+%?r`2gUYwEP>;r)B@vN`T|OPg%_Ys#yxH{CtJuh#c& z;Pv|tgG0cN8u>i-YvRjf&ATZ&+w=@$;>XXqdEggV`px{h`e)7ZC>vq}y#Ht(UOxBz z{DLCz{v$-Tin{L-L(NKTM<0Uv%L0Fk{BF#_J%7Ldd!cerb;qH@>NOiQG>^giZ>M)$ zzvZd{?i9TLIFr++otMlkN-WP>YuUp0f9K%uzm6B3ajuu#+<`yS>xz$+-&Owr;ExHu zPACbx5kB-hBFZ=VRxBz0c7n#^L}Ebl-Bfb=y^KuYH_v%MN`?7X;%*_?xcG5tRQZz% z?pu}BW;M_1!W*79HUa;>mRD_y9j__eKRde>d)`v%{qF{RCuxHUL!X9+M?a6Xe1Pvi zroPds-)9&aZ)WFy!Ti$({>#5ySwq15qYnO09pvU)2LG|(|A@S>=s5T<1O9hzmy+29 z^N%R_Purr1Z3X}N!2e+IzX$wBK>c-rzZU!#1pn2*|A*i|3jFT_|B2wg9Qf~|;Nq4J z{wst3Whdb8zu>1)fVud0RC&RYw6U0|9s$oyMPfsAN&^u|NF!(t&6~a8SvlU($Sd!^B)%KKfce) zCk*^ohx#Y#1QPx45B#Ts|J?n_sgB_PFw~!WG>7yU=3hGaFY&#&^cl>* zi{L-Ua!s8v_#X-NZvp;w|8M<2|A{RBCBXk8@L#HEiarDL?{n~9wrYM+82m2=|Ji{5 z2ggP(OYmP2{GSlog46~7C1k}=XqbPmfd5NQ@cR$&pTP2;2L5}1{~EyGpmj`VGt@ta z#{gGhbPCT2{uhgwSyY1myx_k*@ZSUfkAnX$ite6q;QtQr-(4*rC=~pE2mYtE{Pq6> z_+QzRNX&%!_YL@8Hk6rd1OER6|I@&KE%5&v_+JV9>;8Xc`7aOt7lQwtz^?)RKL!73 zz`ySQjsNHWZScRn?63d#!2kOCm9=TM4G3rOpBwl`z<+=6Ul{n;{SN~FRqUl?dLjSu z;J>}QBDNF!HwFLQ1Ju+zz<)XLU*fv1ekJ&i0sr}hj7@96e^5%ND2 z{1*rReStp${1*ZL%fNqc@P8EipX!WD2nPR`!2dMh_k#R?1ODd$zb)kdOYmPeuB^fm z{Ow#Yp{4az0w*$Wv`2P_6?*o1n@ShC+j{yH>@ZTByw|qOz;Dr1S0{>~RmY71| ze-hN61Nhhd&jSC4_6i}l!u}Hj{(IR-pgSS|8`kq5_?y6gGWf3o{9WLG0o33A`d|O4 z;J*sL5#AE^pKkD_1}QzeIa< ztQYvt5B{t4{q>)1-T#psk^$uZLGZsA_}BA)H!J_w{XhTz{J+fdUmEiN8r0uCb7*+o z|Ks4lapB+mH)rKP5BP5d{<~{}KF;1^>kjUJ_G)vz<-tio#UMUwTNTQKMUZ0HPl}o@_!cmCxQPt zF#oLkUkd(*g8ww|KLhF?0silY{Wl8guMzY&{});L{|x+Rg8wwg|3dJe0shNM*!<6b zD){ei2EYFT|J%U-Ar)W$WbnTg>K_XHH(~$X2mWVv|IPnB;Qt5>zJCG##le66@w@_E z@P8}RzYO@-^M3>Q&%aXNr~&?82mi~W+bIXZ|4ZP1d+Oi(zX<*t z4*4Gs{zv;5;p-v)?}7i8z+VRbmxKR~z#jqrKL`K4fPdZpVwV5zkpDJN|7hTkh5g?U z{4X3xPW6TS*9ZS|fPdZp(=7ju!2eU={|K|D&Jg_90RQu%;P+3E|NFrI8e-4?<{vp$ z{tJWuqtO3p{!@T|zH@d?4E*nf{$GLqUjhGBfIk}d*MDi? zFE$evod^G~1ONK`zdrxqp#Sqc6|qB*|EGap4f=mQ|5borD?nGj9{fK9{8C6`(_HX> z5Adg5u(7j;`_E>0|5h+AZXR&|;ROCZRp0;RKM(Z3Qgdi{0ObEF@T-9T>-ql~_}BA) z-T(LS{&i0N^}h%D|3Cj*p#QyK{_lYMR}H*>+_CNd%m2r~zn=f=^Unj|mnV&kaYOwL zfPWhLf8Bp`;9vLu58S`1fWMFBe=YQXC-^@F^?wZf^LBszCqnCv7_^YVC zg!)GUe;Gncrx5BN0sKbL|LgwW2L5&bA3^<}!TTp+?dY5h^-&!{B|0_|{67KwWibDEK>d#czvI2X`F|Dqe?9+gp#Eo||7)QC*YkfD^#A(& za|G&-f&QPJ`Ro4<;9sBrMWFuMfuDcium8NjKl^lkaTe}>8^M46V{8b1$p4?+-}I}9sHwPHv2nn^ zRGLU!NQC;s{ZE*bnf)>k-hY_?Xx|FS-LU_b!Ti_$tFn3s_TPHAf9l?7YR-ZEw;k@E zHn%&wD`5ZagZroMJ=$O+?7t&$|I~UoJ{br5?=;MRHcw{f?#x2{f!`ALUuO<@|AAi& z^52nrlb{gnKaLi{qTUkQP!cfzrCpGcIVA=4hyI_wtccZ7f%hNyXRoQLo2xc>m%48QK~e z{yh}(5AL7xPvR0rlkX6d;r$E!n3l1S2Kfi?U&Lr$!F$;M;lH1hrY)3Je1!eK2JXLc z%k_=Lu>ZHf{dYFLokD{BzX$HWiAj9}MX>)5!Tq;5e`G8k_Wvok|JD@2?>}MxpNIV? zuVIN9zXbUQ`;WUe2Uie3YfAId5 zd8??JXu$jn_umMZe-3Ct{=xmXiN^>pZ3g)V_uo!Q`2Qc4F#kgTukXJb+@b!^|Lgvv zy*ejV8V0+wFNA&H4M7aNI^d_hN%7pz7_8;Mi98y2rKMG<0asOIe z+6woNO1OV}t=80a!TqBN`d=ulrL6((AMo`U7dW?}!4D4$=vy#o0M{XehAhVW;D`4`?lZUcTnCt;X>VgBQ`5kp-PgZzX2M+xSC zTgBZNMYw+}`YI{wD?$Fj{acZsp{cA3`ybrDhqw)JM+{EkjbZ)^7BaI?uz~p(?%&3; z@cVa`|7P;;o*P*Ht7!xT39$TcZ;FUoX8GUJmq?so`QJp#%>KmkKkjQG`6J8!p4rOk zUY7sG5lzh{EdOhwJG*mP{^w=V2CG>9+vki=rnCGHuAH6AV)^e8A1!8M$HVf!QqjeYljVP+ zlCS?}mj9J6{`ubo^Z$ST54}&zV6gl*7|APm!Sdf={-6K#%=*T1mj7&!|M@KcZD#rg zp0NC%D){IB3VE85&hp=;b%~k6^1l%F-}5a0I~9eHPAva9tR>LKEdRMZ|M~B}Nkvtc z<-a?Zmd+lQ|2bFx`EQT@=f4K{FV6B`N!81TljXmPdNAQP%YRyTbnGI_|8{C}>Nv~) z&dD6oFw6fwdU5F+mj6m?HFX*+|INZ%+HzU`yMKfKf5h@%?90$Fk>$T{=@dPS<-bkE z{9-K2|1y0xga^xiiGBQn*I54N*}?b!EdP7NWaX?_{pMT?vix6q_Rs&m{oH&`EdOT} z{^ftCBz*tQ@}IU@5o^WrUy;%hyskh0gUs?W3 ze90kovHTZHEG~V|@;~@bO|0~D; z`Ol%pFL?gH{5KIpIk5cK@PO~%S^f(LDk$%l_he`Jud**7NRZ`!PZ#|EE0+JW?-PlmEdOWwGqb5I|9eLN`A>_ktbY67{1emE zOlJA76$#%zu>3blq79a?@?W=Pd@_UOzh&*e{9kHXS-Zr_|9Wk1J_nZng|`3lf7}(m z|77_;;`A^7r@htGPqOkqO+Z)w5NrOa6#wUcnaDr?m0*XP$`qS8U?@Us3-C9skq6YfNeRRZ;(o z_C0+OCi?&Ig#YRPb_u_KCi?$vtN-c0V)Fm=f6Z1^!(G&W-tzzHe-`ua|LebW_5bvL zNy^IRIsR`){*lbTx6uDQ@{bhxzX|<&lK(q9EMNH={jVYaZ#D1_Y()RTkQlr$6kKZE?gkNy8h^naQBUmB5D=z#u<$^WH0 zZr`&){}0Lk%h~_`iT-QI|F_TZ`(Nn)J^BAZMpLUB{@+giFS_{s=YI6xh5Ubw{1c1* zmB{~^fA{XUi~KWy{4cAnsgr^J^~nFL`wky<6#pMX{&($aY<2|wTaf?#drh8NivD^1 zJk!V|5T&@jpYB~_`eeR4&|4bnNoFf0v<^DAs{b!T^-+jF#>_2ai|0VlR6Xw7B2J-*8 zT5dYWxqln$dFr3z{;g@~Yc!7jr)v^m)*1cVT1Z>oA^$pB%h^1m{w=^SP5oOtsR{QF zU+}L1e}Jc+*Cz10fqyLi=M~@@=(c*(=IG#vkhq=OcN_-)HSXVGQK8Y^hYuVKiH(gv zi2ifIpF;o7OifKor2f;upM(CBvw8oD|5fGZ7e1i=%ZrO|JtP0z1^*-VUpK&C0sbH0 zZvg)n@b3bDDfn}^|5vpCY+v^2$4@P33z>QBfA!>KWBfeu85!TeZ9o-|BV9w?8it&NeDE8apo4B+YE)?-T#HZ((VX`qz}HPpoaMCrzF)v%pTxVanXub4r{h zx=fihe?hC8(!x^9S-x>VD z_+NMQA5Q)Mo_HhK5B|re-c9Rq^z@m*8SR<5mo8i!o_#6%=G@}j z#g*6Y-kSpdW#FGtSy@$v|E;a5scoSCH`UiS*5dz5nwwi+p?`1m|DO5(fV8wsU;O`H zIXQ(v_}_NwUyAvEH}!u&W8k0*TATU)Z^+Oq;J>Ora?~;S*B(E1+-Vatldt0@{_zO> zp);pUeGdNMsWWEYurqPkx?uL4yXgPaocRl`!@mdo-$DQSi@jGq1pls$Yu7!a{wvY{ zZ{R<@XZwz!=zkLW|060Y+J*NY4yF{@&08H`d`ZXmwxEqgZD2h z(Z3S!UzVbOb>6>hE-NdS;r+`x@OS3@%S`Z3;Qh-~@Q>yF%L4F^LjQBXuSxx%0e>a+ ze;oYxssF3szeW9D1pjyV--G{Mr~Y@~e|M?>!}#B0-oG5g|0;O@@)7?_&<{g1-`Jn=sXes>Xnpoo9D zi2rX9|9lbuR1v?Gh~H4eZz|%~5%CWe@edI3_Z9K?67frk_}jssh5k39{{*EZm8IxE zMeT%!2mT+awO7Xu|33_VqroFb?ErsI_}_#7ZAJh8QU7M(|BV0L0snjI|0(_dHT*v! z|0TL5FZ_%9M=JbJVgH!{{sr8>veCaS`nQ1pRP=9&{!P*U4D??Cet+~|3Vw^kn&dk0 zPs9JK!S9gKoY@Bcx%ht*_=n*Cq2Sk|{$uff6ZC%_{Tt$cTfi?*{pWN4>V*HTV*jm9 z{d;l$QKtT9u>WtM{yS0sFTt+>{%_R(Ch)gV|LeeCLj5m<{{r-X9{!)8e>?pD0r(f; ze<#5|2>&|*ehu(n0{;*AKL`G=@V^`V&jSB;@XrJPzv%xO_z!^p4*0#nKMwp$z;6ux zP2ks||E&f8K=981|9Z<8~@c*(dwJ`<$zcppFUV;CXw(O2#@_%8%L z6zx<7fxn-slg4K7kJ5D08Abjv(cPoJ7W`TUTBBE@|KY|aW{KdRWo}{l8vJvutZhEv z|C4R)9CGRZ(;a#L4*#oN-4-q&|8DT`^cqF}TjI0P*BAWW{>#_zrT-rY+P9?{{T~jE z3kw7Pj>v6$Yv6xR%&xej^uOSQn&dy||6wWhX$JKFsPyK{qa$mr zAWQz82>#yS9}9jp^1q^{j@C&0PgOTVUm5?CGDtOAjsGhc8=G`CoA}3S@Rx&s1NaY- zf0o+%JDhQtJ;&9t&1K;1`3qcJUESv5|9T$xy^`^NBOk+6XUIR}{Y?TJssC$1`5^=1|LXrA^5Lp)O1^#<6=5cet|24rs>0(RD_9d%!S{G2D%uaicvuHC$3J>-9q|NIoCmE{NZ?YBwwjfNTiKSlF5ZF%s| z&@~x0dMNK-4SEEFP+fLlsrO20AE{NlH?LhM4}L@Nw*-X*PbL3<3tbzL7O{KJ%g6)K zN4S7D#Kgz`x&Pn8_Y;mK+n+ji{7K4_6T87*kbd&);j0%emSo+^Nx5+4>bbnr1v9{Z zspwX5AMhV3y;MHo*`voND=Mo(!N0FMveq2@hw73W$AUk!>EG6mExdnkYwsu`|M*M& zkntw}`p7O+7~ZXCFAv2{DkBE=?YB_XRsDMJfrDH$y>xyIA2QTIccs4akddRN8_Y7& z9X)p3Wc06u|BZ)#CGh`l#6$S%2a*`1_<= zojnKt1G3iST&Djo&I>HqcJIc`RYhG(c7uOesqFpP)c=x-vdV?zpZV1_wJXR!v(f*l z*B?I4Z2GI^Gx=vK`u|S-Uq%1s=)Xv|M7{_0|3LAPav${nO!bBO0P6p>=6;<+;Qyfe zmwq()=Zk@%(Pi}i!#Kcf{jUc91^jOV^&f-( zEvEhpss9z!|268r5A~mh{|})4&r<(gsQ;VH|2?Sx{oo%%{hwj~F#-I`sQ;l&x-DzJ zfB8D3?VpY=@c&$Dv5Xx0ucQ9ggTIOTUjhC$>VFCN^YFg~^uHoqPkmeFpHc%~qiM`P zmEc##{}RomE$>tRso;Nr|7C#xEBQAY{B7jlU{|e$`S^dBho09p{67l(&%hrG{!;wk zEhsp|5dZfCzaReR3;u)T-vIE(z`q6fPs6`8_#3(Z+JXN&_?@z{b8mxR3;ccXKRxg( zG5;EtmX!~t|CxY49siRC|6cr04*c)Iuhi7s`h)pj4gT-Ke+~SPhyQx;|B3%KgWr+; zPdoT$;D7nxUq}8e0slq(zuaJ+(LC^1f!~V!a|QnOnEx)|f7-(RQ^{cz{qH#VHR*qc z!LLUCISBp{`2Qa8>ofms4j8=N75qDbuZ5i7vVF&%u-g$m!*}mF81*1}&fWtD50n3F zcXoTOvN&!m1j(RlpynTr|k&K6VuSF>N`XkWZ?^=AI(0vGguuXta{!kc&R zJucfi6xPx6?5>uMgoY)1dp_0Jm|c>&kfe5%Ea7m!WuIUU`(fXhl-%6#Gl zkQDv@I~xF1rP(U4cmbuTc1q(N{->)Ir}F^+)6_el{}lf>Hr!)$4gDLK^fWER|E*Yn zbf3ihZ++TEdfJScj&?I0I_zi9v2~j2e8_qJ0uQ%Y3s<9mSI>fFw)nrl?;JlP^1n~O z+(0w(Z)otL5E64 zWv2Go3l}rUzk_qGTrJLDRJg3*#?AcV#9Jdu?%t~;|Bk)?=y4hOSNqBH7xguzH6z}? zd0SIoQ9qXYZ*SJ-_bGc}YkiU9v;lL2PuO(*^_&0_7OHPrtkYkwR6DKloe*!8ow zLjMj<{asw>f6Lt(-2Ku2BF}N2smr`qZuFh9$^-qcA^-lpe$(b%@P7mTw}nMTl*9i& zQH4<|@V`GcI9?h4PbL0LG=l%5sikRSz<-(i-wXWbvtMQVga1zc;e2WEUoYNyOCJ2s zz^?-Shm{Fcy}|#!=1A?>mv7#_t`BJJ-SFY#f6cdB8~*$9^>h2N_S(-se)fiH2QAe+2(nlWwM! z@IS+1orM?pr&#xx*6UBn{=>^Yo&Gn+$;jFL@A(US+-3{-{XK_!S%Kfx_jf<5RcqII zfPXmnBf(z={uuQC1pGl!VbO))4~=~qe+~TU=)V;F+^*A}fIlJQ)7cX6r(}Q69S;7| z{H=uxz+VA=8Sv+!e<|=6RUWF60e@3XZ0!K>x79Z{R>OaF^SRdh;IC`H)KLX~Md?1$ zyXk+baWRd9%=097D z{*w%t{~WD*Pb;DSncH=@uK>T5Q)lO&%zr*^5 z6!;Cn9}9jn@CQ@>v%wz*{>k9a0RKktXEXmT0DodeP{#So7cQoP|LFNESIhF7^1bQ* zmBnqvo9Tb~Wz}UG^#9_@x=QJX&tEjxTzVPs@y*+I>VL-D4u> zQ-*&f>R(-MqWl@^zmw7sWfkSV{p8hrH2zQ@IA}EUpNsa8p{9DB^p6Z1IZB)PCy4$( zOfvtWe|rlr%UAI4Y;9+=iTQt$oteXa=KpC<7B0KN?<3-j+n`1jky``3UO zg8%IczPlxm{BtPmOjsxK&(^48duNh=cE%o$3!whbB_2$SApcxSeShL3^Z${ITW7QA z|0lC|=S-siKgjPW(8T|q75^wHW&XKd)>8Hg{CD8Liv0hjrl__X{Qs!WZ|sHs-!xxr zz6bvw+RuLbN&f36ZPiJY{m&pd3wcHMKi!o6QYpdzdr|+d@&9pJD|Nc!e}CvL*AD{! zQ1I7-f0T*3Nq74H919N~^t3i)p(_-)p1+8h&{za@tL7a4X}@V`*-Yk@xq|CNT$tlfJ@qvG7wYeH6;NJxQ8#ZFd(9wER1^*jn zIB&Er`!8*iOjAwzzq7@1%X|30z4aoSJp6x}U2lgO^JdSPW{C|~a zKQCYSU*Z$0?k1FQp`*KOJyhyFu$Z{HCemM-9rh)UfX%KR4+dn7K1{bz3CKS^24 zKbfijruHWPq-Fe*83BID{%-~KUq$^tM*ro-KZ@JY|1GiqU9U_K`rm&wk8AhP|30(- z+7JKlo9kPbkbmE_|10Q!p!8A!e_y%f0{=ag)+o!e|Ldyeqb|k%Z=#m9wlVv^v3djb z%i({dp`KCkgt6m>n0TAYQvdTU`cCqt{^wYCo@Pt_nQ8aC;Qv#dR=SKM|7`~U0QV(J z*LnK#`%m60ZG9bmCI0UiU>6|i{|UhtLmpB8DfoXC^`9Ok8-1Jl&x*YjJCgbjN{meO zLI0ttF{wW2KQiOZ+3#1F|Iq(&_P;gxUkWPN|JLJw9r#}}`1j*~?dX3i{+C~qU%L+d z7uVly+zkFQ{BJ7wE8ElBCw!CaKPSt`;{SU1e+B+;s3fOSH$bxgl-8)}BiVnh)$WV` z$?0txHVFSyGF&oR3jb3Bza048z`qLop4J1W1>%3ccAf2)<9`88J)9Sze+##MEd-QK7*1vN=KcW7U;Qu=KPvCzKz<(D18w&m${4bLEXG`Muq|axL9S$cnZ!Ie>p922xm6-zn*J^gZY-9g%8~<0r{~t6T zYso|ZPuq`tJB9v-)c-{MPs0B=>R-ZtKdp@d|AX`t_4_mb$kP8L`tL^m zk?3EV{_jZsmBarf{Hxpb68P`uU8uJ>i4@c+F^^OQ2-KVHP2tTj;Q68@K_w?Xe5{Xf&Nr_mew zf3C@LlMnR&LW|xOF4X@mYh`N(>c8Bsvt1heS2;;J%_aZTx^-H(*n7#+Mo(i;?ySK#j){2*9@zkAsIu)g?zZxMh0*xRu)@V~){ZHd0%AD((TE#=thGh;He z1pE`S_v8ey|M@fjQ-L)5@4v{ua_oQpF3Y{I#QZa_^5YYA=AXs%KW*lp74_4^9f3L8MVUxG*-qSCtGCJwdfrEo$PsORQ|JNn|Ud8`3$-n9Be~dHs zXGr)r$i9%I>(-lu`hUj#qX_&Dow_?Ug8#kS?{3e)|Jrk?SFV@$%Kt?CU%q`C4uH0SOnfmu8{|Dp$OXzi@0h*ky~U|DV2kt7ek_z6K0gKb8EWDB@QY z@k>Qj3;ka<_Ko0w28q{`(vS2a3PLzeQPN z`9b{Oy7Fb!Hu|4$&Fb1i;18(Z)VLV@Zp}+uH`4z++r2y7sQ)PGgEFzye=PNXh58RB z|I0A{gsJV)aAf|;(8|$qW&e|{=c@0={y!1_f5iSj)x_2GIs30N{Qo%nuS)#?DETkn z&eUER|0@ywzs2p#!uRz5Z=OHBUZek)z9W4fp#O$|kAd@;f0cu)w#ae+Q3t>Dj@^4? z$bWmtKMJD%8zr_U8KHl()GsH@;9n=B{;VDN^|RmPIDy|GKek{Z_3u)ApkyrdZ&_wp zz6Sg@mBCMJ$p3ydS6=!t{{}Mu&ZGY=Y|d+*PX6_3FKVB{{1Yv0Abp+w9}oU_;15yK zRBFcmBh)O^`}7?+C{t^b_6X+RTs_}m{pf$mhTfxlO&mM!q}cx+SZuKLLjO;#`&w_n z|E}42IrL-xzwPAi(uMi|lZgMji2oJ(Zvg+h0Oi11=AYid>A_=nN$!7HVKeZ*&QS?b zbN-Rs|59R|>Hoje|Ic&(9H06}S|j_v!Suh`!u{`6&fmiQ??k~C^8Y-s|NU9E^L`Qc z@4qDY(f=9=e)`|W`iurS`rqQ_(=AHqe?@!Zx31`azx4c0KhXa_a(43Z;NPY+LpcQe zyVP{mPLY4kYbk0SLjRZbRP_#^|D)*tGx|S;{-2`%hv+{M{Xer-n)WC2|8={b_Vd_( z-f@z4UP}M_>^9MT0s8-e|F5I}zY*uZ4*^>18}a|X!Oyp-(EkR8m4$oae_f;Q?)9bq zd&cI+EusF$CjLm85C0RH|BT81L*QSB`EMlr>y!UyGykn2|ITOrTTT9*g8zBre>0f> z2EhN?nnyKS@V^QEyTkty_*a5|@Aeztn%RFGkRH=%C;N|o!QZ8;w$0-S9sY z{x87)Ao#zcXE)3P{*R0K=Zr}w69xS5u|=U}DE|M#`kKut{QoBYzYhNIQUCSS|5vwn z3lEV0e|k209VY+3^_}T=p8e0qfQI!~(0@Pbe-ZniLHNJd*4=x$asQmc{l6FaFaE&4 zhsPy8Pu_p}*l`2$pQQikWqi(X0l#+k-kh!Ee`oHWufXq6oKl=b{o9a#m!f}5@^1n7 zH{gGBsQ=aV!y0X=|K-g_t=824BJQ7)sQ*LKOQmzE|9#9q%J9Dn{cFSjHnkiLTk8J` z{42x%1-*2AH|qZk_>GwVj+^u}X@LJ17VjoSQvZ*wzuUx8|M%?f+OMGgZ#uO&FCqWt zk^g-0|02)ZUIo;DDfho{^j{HR6>xsdrp=n@e?I!x4Z9p}O#K^B|IXkyi9Hf;NdA{j z%ue>l|Kw6L(yZWLDI+y=0rQVqc4Dp#_}$3=`RIQc`u|D&uPS?6z8(DQE6uCI@xMtm z?X{WUpH}bFcnOYD4ub}=<$jz6Z4F8$ne}n($s$JGt&iogmm8JbR z`G2Rl|Juj?Ly~_FnOF(=_ci+uN&bCr-ACwupY4o<{QIBNH6j0AcXL|k1O7Xn_MV~i zzlYTSSMW>j-|5uf&U@+|H|Uzk~QSNb!C(9uOR(S2gBR|2wqKY2U^F_mF>X!v8@- zm(eqse-4|RG@Z))^VVX8g)#HbN9$?UrOZEVcCikZ(0?=CUyb`uv0Ldvt>ygwk7wSp z=j=bLd>{IC27h@#OW+&u>j(c85(NJ`VT$4Z!M|D5fW3p@-w6LVCI2ZT?oU!-{*g%? zaH2Q!k4A?4+3w6gD%sLGotb}@=ld6KVg6rK?0YMm`F~^C#`{Z{|JRcLs^EV{&8C-Q zx&KV5cW(TH`_KI5xh?mYe`dGO`j*H1lg|7zi1{Z=ZnQA}Bq%*p8N~dbD(3$v_Ft0u zH%`odTMW$w{v%8_nC)Tz(@Oqv#{a&H^UrJcACmp&d-h+F`R9>1|2${^C7FM2_(}=; zuX_RN!u&TbI6EW~|NA3sTf|BHZ)ntsC?EW9RP4O?gXq5>?;oVl{~-LYKm2#g_>{Q> z{(EJ&WbecO*1`XO)c@wf1x>i;qPpWyvNKlo3^|A)bUJo}H;@V`&g|4vc=+r<9&!{VuBEbpJb zSWDY9(f>ZM|JVipZ`l8)(EpzE19-*w-y=^)&qDm~uJ5Z=H}Sul0ZGFC+g!{)R$+4^ zt}_4HMkz+SG55 ze<_u%5c;25<;zNA^sj~gEdjqC?>{ubZ^-;V0{kZ6?}h&JdH*qi`Y)DKSJ3DFS;qcH z1^<5{^8Zrnt4=@me+_y~`fBVyS`6!r{>J~lfj<`i+d}_Kfd6pouhTlC|Gjqa?N#x= zIH!1LQ~WQ9{l`N3Uz#}oWQzSSH$Yb4f3leWr-z*l^ML&(G1pGaVvjqJ8%H9b02UTtr=KrBJk;43=DdvCU=EE&@ z+`kRl3))rb|3%Dy9`Mh%OA2@4zXJTy@L#Itsv!^mb?`5R|5x+=qYwSR&2Y7m5&5Ut z#6j@CFpKpT3G}~U>+Uw1@E;5RZ{a@*{{N)^rSkq|9{eXV|9Fvqb42`S15^b3(}Ht? zUD5v}_8&9g-x>VDiBT*FGK(Tu>Wxfe;WL6$T%y^f2-MlR8s#v#Qjf~;(fPH z(*FjOm6c^M|Mji3t2#vekE}Ub^B4VpNd1JyXYBv}VE-qz3OEROqU4gJrQ`KR8*-^7UiA7znesZRcjWB!>$|KDP_ z+hGj&!=2&;{xjXQ7w*IVb3OIF4&ndFy#LtB{F@dqF>ojO$02x3$ZGJrgpH2a0RNUz zBcm5l|2DBhkb<{%=zM1I7H;Rm^|A z#QWDc=6}ij`$v6@F#qW`N3?pNf6eyG?IWrGO6eTo{&io@N z~=P1X$Xl|5NC{8vUQQ+vl(Y{x3U4xts(4 zHs1evq5oa@UkU!V-}myW;p~6^2}oQ2nEmhK;4WKC!0#>MUn}C@B;ubU;-3kA8TP+( z@PAeOe?fL}ZYKUel>F-s|5_q`v$FeTlKgMP{2$BwuTZ1+@+|vr8R|b5{cEuQ*-!sd zY4`cIpZ?b_4B1 z_#cY?f6Ai2YyU|GtR( z&mVG|6m~NIzfoGMq5}R8YRfe=!2d|=Z=Lbrf39b*|2z0^iumuDc$yi||1Vl}wP?lv zuUZ$|6p??A+dXh7<^FNT>49?v`6tFLL%9D%dd~CQgZ@K(|MZQ4|Db@s0;0el5UeKb zKYZbTJntVo$-l>`f4A6Ev74!Xr^JKFU(mlD`)_~t-`3!_W&X3k|DC{ZlK(khm;7s3 z{9mya_iw$j?e`CH|JJ7dZ_)qMYZ6|5XZ};JS8PW4LQ=>J{a8o~cQH95Af#sB_m>)oM3|L-CdDf5N? z*F)A&p(p;=M^RI$9RC}js;hR3{`Z^a2(5DZ-w54-!{o`oV+`s?+q3_eVC-*t8UOp! zTy0Vd`S&lYchfAHfB&}iwVTZRJI~SEc^dQY67Wmz-`*Y?Ugq$>)~AzS5BzVF|D=Ez z{C|6pXGj3~e|PAw{C^|6pBRy@Tj~f`N|FKkz@+ z_}Bh_hWQ#{{+Yu24}JKbZ)@rBAO1JnafS0Z>fhTn*Zni~zr^F)vLDpHtIsN-{yqF( ztye_8#^r#PpT(*F|EUkL9XQnRjR zm(u^s@=Eg0qW{XGyTwz`e?jT5{8Lge?HVsQ;HuHLWwye`DJs zq5rE$B@6zqA)6xPe;LILq5mtWUJ?A?NK;M6fc>YLuCiVc`%fL-Kb-@=zHyY z`6~rB(EnqDbVKaXe^jVuLl1nSZv&xW})g{;woxCflL^3n>n1zVyG->Dp%v$bZMO z{>n{*{}*|gg>~#d9~b4^I?w*&Ug_R48RoyXieK-a@~SIpoXEdLb*1(8?7vEzikfZM ze^j*PwT~zNX-OGN8?yh?leJLLq5elJ&Qh^w{xwydtKq`_qm$+$9V_t5>(0@4Vg6Az zSZ%Ze{i_>qF#C!A-Obf3!qERRtL`@T^#4`7|6N1>U+<{u^cww7aA@zTw`b%vG{qJ6#cjHU+ z|Csl055fPU&9&n>_=ieaOY89dWt8k5g;4t6IK@_FW9B~t)z=y>^glVxU-utr-N*Xt zz^`U-+h`^Dm5j5@#xwtUnvadCYs=Dz^j?)H}W|0?`nGXGC=9WVI*B;LPD z=Kr}q!&hm-|15v!044nIWKdf0VEpfB=!5VV_}}Ho+k3x}f6m7!#nqF4b|rjIGG+eV zmJ*)YpZ&)_>F>_2h5!9oyK*+;e;@M-@+J50w?*b9NASO&r7dNP@&7uJ|I+G5FCT-y zqRyu=2mElpZVvvsu#g?~>+dFO8Uzn!a& zyBYrPpXNU^a3uAAHYhfvochlR%?cYz{U=2xM440nCt^}!&6)r9 zCVWYH$NU?gvO86>{|reF%S>SZACa{u_XGU5=2aF(lm9!4+KN@#f7F&%m&v04#){of z_EG;2stamog8y0Fm4+j4K772<)K8fI?o$8v$bVy{#>vda|0c>RDY&5jA@u(|{BI=w zmxurL)m*E+ocuFLcd>xKn}M8>Cir`a_}7W}H;ed}iuhML_H^z||95n46Zp3!|16;Y zTlq}&8-o9t`&S1J1Amr?KRr}Yxc{es{}1pdfZq!IG5DW9{ui0@~#*Vg3ou z+MTn9`$v1;*TMtfZ!Rjk70>)z4}K}|*HqM1O0oZ~tiJH_B>s25?yQi1N}6uA9)tga zwwjJK{NGf{K)RIu$7oq&xl;UpnBo-Wx#(X9{w4E|hNg`$|ETCH=^NAk6b!l z##&}x@E>UIV-ZRI^#gw-{CnAUweOGq7m50xOa8e){+s2oX_*1|r~52jrAPjm?0+q= z5BcX(P~4WU%)jSC{|Rp+|DB}&kEH&O(EsD{ze5R~ljZTheJOKKjKlwTrf<)1$N#tD z|4Z=y?|I7$6v=;|iZd71O-!t?tnSUSFJ#U!E{rgVSo0b&( z|9acUZ;9|fLF$6cLi}%xY=8Mp`2Pq+d1W{J|2Nf<>c{Z^0o1<^{?|u$r~Ve|zlXuK z(P!}gF2=GZZ>ax`=FcY`p#IlbeX}`C{$FAH-F_wepT&-!oL$lXd@=vc_PFAu1pXO5 zV|?T3|5M4olJ_rHgSu^5LH@lc_Wx4wOWyxf#OTLYvj3`1P)x4j{!^dw>;B!Gt|0K= zo@JD)2>-!(zxJPDMZfl+dqw|?t61=44EO(}>Laza@Sg_$uiSq#n||#-a@#UHhQR-1 zsV1TSO_%*$Arbx0Qj}9E!vE*0N^7*?|BEzDbnbzFxvrsp75G;wM`RDCtV!M{|K|9=AiN?i;6JK$eO{(FS}HyckgyNUk0@%}jn|LcYRx1s-j zwu|kn;eQbN-wXe`B7RN!Uo!pA*k_q%^kPA`{Z{~uj( zsA@O;FAe?`$$!ap74;VM|J|BEZKy7dG9+fq8e+@Ae^ctww@_wb)ttt-@jZrzlI zIq;v{w4h}s{HL{9eKUuD2dRusS-gLD0sjE_w^SUW+!g+9R0}m6sDD4r8#=4W|AD$i z`Wx}Tg$7TI-0*)d<7Bf%?Eh5ECrui_{zt=VpG{x#uZ-<3hhF4g1;@?K6Uo0uuAAMZ z+5ee&%vdH*{pH9|n_Qn6ogBFA|p#Q2+uZUXoUl@6A?<@3wD`sl^Tk=n9 zf<^Lg%)cEeVJD=yf7hnBWKO34HD-0>PN)8(dH?@A`7gd`_pR~dzmU?!<%5}jBP!gh zhQohm_59l2^uOG?IgS0f|0Xx-x8~vhX>H0K>D0f2)EgOV`k#wzw8A9n-%?Ro*^2tN zQN5;N0snrQ|LIJD|3KZI!&1@zLWBE8XVJfx@lez6=wHSBuw@kb*RV>n*-QSFv0ZJS z0sjh)A6;_b--!Gh0{>&{8O9md$yGP)0nj>r;7X&o!6~kGV@=2k$Qq+Kj*b$^Oeh>UyVZ3sCBX1-vO7W!AR@}4#f{ma>&br_HS zr5)2;#-e`{*Q*QV(Z8WbgqIBZ*YlaRsx$i6@;@Hf4gFUJxrKB=|M$iGUlMs==zj&w zf2*kfZ}^`t{@0RnDD^V@H>Ur3|5ux3nEM_4@p;?wSCIeriT<~(FIKylM`X6ZC#o|5rryu-F-aq!T?dM=g{xx>I zA>@B4=AQ%Lm-V>g)gS#U;(rP7uj*gDz6bfgM)be>&>rC?_9}oVViqxud@Nc32 zt!4fT$N#6{|9kO&P5eKO{QC|4yGzaO6pj9u$zGB_MgCu=D&#*)egN|( z^>3qlO}~)(pJiYn;Gb)})9exWe=`pk^gjap-|_!3^#84V|2x5Pw{s}pe|Dk&-35OS z^zQ}!K0a!GFRA|l{sDonsQ=eNOG32B|L;SGM`+{!pCj9&*WmyE!T%=szn)-_JRAMr zNl8t!NB!$MgAFL9y>{!{5R4{Y1%jP&sgxs)Bh(res^{t|95q@a9d6N_wq;< z>c1cPCzt#)i2Ub?{@=p?Nc8_PbWy}Y@PCa=jXpsB{~5DC){FdqGa(?Ufc|$crTK(9 z`hSf6*W-ULvij#_GXEdXtI2=B{x3n)e-`;KAOBCUxbq|h{|~Khdl`-Y2h}w<>_h)C zOGkqK^#Ah}1y%jn|L>~)RI3aB+v-j>YQq1& zP2C0mI{^P3^gnMYjZP=%e@kR1$XoILW0T@oC0pj7wW{OQ9C`mULvw@nLinGeJ3-Hj z_dg2^-V6GlV?4`j4E{ID{DWl!_=j2@wW-Db{;=)j@QD65&T))O1pTjs>c0s7JEQ*`{J*>ZzV&X@ z|EHjcEuX3X@1geL$LasCBH!)JLjUh#_Qkp4e-GGyE~EcHOIa%HKd)2&Kk>giS+jE2 zfd72ny@I#!f2C-skbjSrjxRUE|ISnl6YhTp!9N52AFk`!XwCj(N7L7qKhS?r+wa2u z*I%k%qUKqmA<^_*Zq!ai7WlOVMNfvfsFW$ojagnn?ba z^54IHB>Ha)+PTG__g_u)ze((W>f!%4_^;s)VCLX|PZFLch0_1ZQyvQVZ^8co>c5cu zzl!{mo42*FkorGYq$1!yS^DIDZ}R_GFvi@#Uk z2>Aam$UkHi{C^I04ENyu+xy5h(XQmb*XZA#{nzsZBccC2N~ufzi~e^vT|4s!{NKnr zpR*SJujCymyaWFi@IMdy|1|#hf&KS!G5;Kfe;eMvAFR`8&?Eo(i1V+1TSj{o{vRY& z+ex=)*KT2A{)c7Ebnx{1Pj}{*n-v{jfI{bgVe~=0K z_5Md8G(9Yv{I3#O8J$c2(_sF20{=P*uaax&fBNh{^XPv@>Gv`V!EZ+Y?*)EK`0qyj z+Z06=OVj@y*ngMM|6KU~{XhEO!s>*YdGvoT`oA{)-;es&q5lUm|K-sCL+Jl==>HMa zzdQXuS}|7Hjs6$U{$~sQFIlsb);|0{O?UOM5BPtkL6^~Ax&P;i`S+Tbe{Wl*PLtd} zAByw;Ge;$-K<=NfU4z}LdH?m^BVw63`@b(f^M(7*4{`t7P4xd>p`PKg@UJTFKNMri zVsoi~-2{syP5fUo#pXm1`!5qQ{~2aAXUB8@vd+^gFlYXqB=$e&((l6k+rDCc)qUQ7 zEaU!h9sKU>KktHneN%RGIsJcC+vW~`_zxHP4-xr~S9~wbzx&w#bYuT@lKPjt|2m@E zRZp`2yku~}$dvqlj_>~m(Eo3nS6D6~|6Q}XC)|IZiTEEn9(B7m z!)KIVfAs&w-!Q<1`tKRkB}9t)?;7eAp1}QQkchuu%&+fX48;47u{i%~rq^cX!oO}- zU2Z1)+vZ&=v;@Cn(fM08^gr`b`*H{T-&)kaZ*^)-A^k6)PN5->{ii$n|H%87W%NHu z{@*K=C?om)GhW0WqBv1W^8O`4^i?v8|9fD5 z*-{4oebz3z%Kl=Y2$86#L{n<6d-AeNQm-?5?KW}{I3jBZYUlia={r3&(8WK+Z z4-AbCiy{AXjf{#ukN@|Kk&3@U{u!GvG+Dy`M3MiYBLAbZ{><%we@Aitw=L2uu>!wU zsnPv5>fb`dA0XoQt&?l;p#OU|skBu5`0~|V#2+W_|M$w)%S-nE;o|;31pX!a&s@zx zS~d88rmmZw0`;F}P&QhEKiRm-RFV1bsi^-4R?^m9=>N8DhP~wef6X!1*@yh|pUD4b zkI9}A{O^61`v$=OYx3_r=Dz_!sv*aifBS^yhozza9+CZ`74g3=F@bUEy#JjD|C09) zV^jVX-hYo0@edXCzd+RgoFe^_YVIFCrDKKnU;Y(-Rcr8n^XlrCbMb%cx}HM+w`;oI zI+p(L)Yk3WCF(zk{g>qXuM@Jv6pqvXGTDDf_|H`})woFi+p1Y5=zpi~R{a$4?=$$v z=rZ^Z8M~OI57_{TX#F(A&w$y)D{Lcgadx`q*2mVa-KZyQ! z4*f69t16Iu|Gu*5aLF9}e;xb3Nci7e@uM<^{r}YJYc&b%KWEmRYe+-?bDCZ^XTra2 zo1CEk6mkETAp5u6I{IIhqLRuB@^89o4~@s<-%w3cosZ<7VBJz7|Hm2>3;rL4{`K(x zdddD1|F1#+Hu!%#^*@jP-|V=-X*T`8*!6FBP4u7dF~d_A|Eu(wDnzbfEQW&h)a{tLms zu;{;%Q1}ll{Zb~u?^p3%z(2jZkFfumT=!?=V)Q?+sfUn%|8AT2E$7RRpQok13H%?E zJ*uz`{a;mlFYtd+^|HD%`8QJZzZmwveVPA)4E#nPW&Q~TzX|Ujn#^S;DX{--x5^gg z-x}_J=a~QM;oqA6Uk3kY@c&AWRbI!*KLtLaLjElg^Y3W4ZR#*g#T$rUWh8? z{qwMxOR*K`UnN0P(7#5?zp4G%f68$G^=AI-o@FfT|JLL!FO>NIrlJ)B{-veGlc$IEH#KWyus*U!cOt+@aFf&ZJgz3&)@|7S_5%iP2N(q;QAs8Ji{BM0;TESKJU#p7dmRw~2 zwXF2NvK00|?!5mn2fs^oguuT;U0Gur`Ol^)vN?|YXW4eCV=WNV>=f_6TaCY&1(AQhn}=AoqyJCbzgy7%JKJ$~jp+Xs z``=0ApQrFIxqm|LguQh5N?@-ankh|Hg!7MBJqQM?{Whb$#IAZ{$D8 z{$pUm-b9K2^@V?X=AWMF7ctJ0bLcU*Ot zdUyCgthr8SEc_qTl^!+`{qHgGG8#(%-(lR>bQJi1nh&+K1OHd6z-dn4|7h!Gx0C$$ z*3r}H3iuQ7|INJrNfG;Bx=*=ZANbGm&k9sz{x=V*3GR;nS%rQG>kNL|NQ3A>;CGBM ziSGw~*95m@J?h^h#XC)l`PV0XU8V{A`)57P9u5Cec}Mf7fnTmDS;+rNrAfcuKUcU4 z^N&__X-!}JU$5@3#zV|MhD{zV+t`1alK&+Azd&l0Q2!;e>B9S$`-%@#Zi4@bYMI6- z@Yia_3I5-x8zuN(tHEx;|2mBSF`Ewl5c3kt=ira9+G%qb{L!|39CpKhyyJcsNAM?e z|LjBlP4jrY>@E2>ll%95@^7yHU+ZtN|FR4kvt>N#iT zgM|I({Fu|R?#%y75{@SyCI5SK|B0ji*K+^<3ICh2G;^EbzbpQ)1OL55{`-~Im*=tn z8C3D>{rgbvKa%f%MzQ~B!T-kLe|O0Le^CD&_+7@sXg3HK_-_{HpD^>SmIvWKnD-wC;XfAtpM(CRsDH`tznpOWEbM=h zJcf8FGXLf9{$&mM=dAzcz)kqyw4fa!JMh0rp?e}i@jqwo-$~>@`NP4eHegs8;z z_@8@9Y^opl*QW<(*yI1JvVwEJf?uBbXCL@Gk^l9ne~r@XLjG5&sHrL<|L9kjynG6N zox0HaE%0yF^y~Y7Bk;du|8a}?cQF1}DEi-1G5`RkqXridD4*|8HjgZ|3)J+Ij!9 z2mh<#{`n36t7rZh!Tb}&{A)@7i?!M~Z8rUHi`f6d9Tz(-1OHi9*M)N6&+%~cdPDw8 z@>${c2>ws_n+IGV|Jes+1zY3)&Y>yc1?YcL@YZE*vc|5qh!NgBrfCs53P z3(0>hJ}-K~e+u=l!u?|x^{>JE#~swaOzFFF59(i`qPnsl^RE%_pLCi3%y|DKMgGxg zDsS<||Mc4)w=X6CK9Z7^mgL{(vi;=$ME^Gwdns>5|94dfYb2omFPf@aMdZI9x(dTC zF#o(lE|0VhTi$wpGfkx=xDd=E`68g6bJshzX{aZ)wi1vVgijhx2)Xkp6q|r@(T0U(f^f-ZWIU5|K&=bmwD6wJ5}6#vLF4MRzIt` z5C5a<>KcRLe^^s{>n8k9yX|$yeEjc;)OeXJ{O`W3f!q!JuS8K-sgnI)fvToj6Z!X> zrj+(F=KmJm--lU(zrkR{=nKsMFOAhr&yxS*%ok324*qDXY1WeaM+EPmWWgW8`(JJF z=YszU_%l6zUsewOG@qNRF5>@3$bXXe4|9W_hLnPTR_KEWS@2Jfyd51x{ZEc@h`$W} z4GD>fZScQ3B{lUv_?M?&$&d&CqO9y}8SwYdyO!S#{Jo2I6u&3`_7LAcbg6hCynmfg z{j~NL_{Y>eXzT+2Bbwq`&cgo?asGQQb-L3P?!S*@kIP@+_s{MsCMcig{&zz)Rs9(C z|3h=P_F?q@MR&)rgS>zKU=TDql;8h+V;p80#QXn$$^Wz1{~jR!&tw0!8~smV|FzxG z&BcoRd&PB%`v~Tr3m!{7hv0vweQx^gMgPZ`|Kj0)e$a#v5Ae_C{jVGNXG9wB9YX(~ z5;G=lB>jI=LPwG{{eNvr>xoJ9fA92(XUBnmY1WBs$^Cy&-mmY!`xSjGInMs47y6$- z{^?e6_DLN1Pg=}Ba&?vU^7y}U)9uzO_*ZYc*3pOhe<`)B(Ml z|F;?aU(<}x?#KT7u6X}{WU$)sHu#?#hnRiG|Bjh=SZ)LVX{%P7OW?m?`_leD`rj4D zX6LW;za6f(-2Wo~1&R45#^>)|6D`W!@p4f{=9z+A^-cv zT#nlT{%Hw4lLqnof0I(Wo#;pZcc%W2Qvdc@y>gwI|Az7YLx=fCyC}Lu3;#1MoqvB0 z`ZucZc;d?Y7lmrOmyYcJWY~ZHApdDJ{ne@peiingO6b2)%B52S{#PrzU49SyukVV| z!v5ovYNq-N=HJ_ze`voV|6S9)IIJuCkEaIPN2@XaJTN|E>WThOnmbsGL;pvt7Fds= z{x8{@+v%f!{{Pe|5B=|SEq5$ljQpc=3j62-)`XFOa5`<{;P!l*^z(x(f_Tve_GN1 zEqMQEM*W-QMd#mO|2>-dSMvU4Sm|5g_fNDdf}g0tzk2nhms;?zT=%_RkM|$)%>RbG z|LD~AvfYIDAK#<~OXpMnEwb8j#ngX;qK#4;_5V^;P2&yqFZutsA@u+I;MXSqm9YP8 zp#K*b|6z85{+DC^r{#U}-&x-OwDbPw1n>Vol7Eu;{o4fcZyfk*;eW4(wpR%L7w%)~ zw-x`};;$31nfZTx(68Tr@)P;@ip-0;ivKT+`4*o}|943!Ob(_0JEXLy?x+9Tq?cx9 zfZsB!GB=y~XS}%o`n~ATt;5v+@X{S+kC^`lSL_si|EquXdExz6ACdnaO?z5A>Hl3t z{(nfBNmn!fe35mQtE2yYQ0%QT0{?%bs;u!F`~T;f`Z@#1e~)!13;F+^f%NF!^#7ae zfBnh-SLy$s$v+p#e_zNyXL$d7j{I|+{5J>uhh1&m-N?TOJ)}H6$-jGieiwfKY+ay5 zpa=6$WXP}m_nt8R|APD1nW$g?f6(txT>ORz{{Qs1MB)E$15^3`2j;&${Qsxq{~yQj z|A&*)E?wRFXTI?Ne|g0vx1PcOM}GhP6aRnp5C4Dpf1I6(KbAqe$CFAWZ7LO#kc3o} z3Q0(cgd`yeNqb2Wk|b%9kc3o7B~eHVNh)pHC>4c}q)lj}oa?@4rt_TVo#%df&!6!9 z{H}ejnTfm$|KGS(v;UvYY=HkS!2cgMq5r>^{rJ^p_P)KNK3IhP|MqN;p5hX~&-{O_ zJ7$2?<-tRx@As6E8^Q6 zXCtF?;QxpF!Ep&Piikgn;fMX#Jj@>r{5O(w^S_uN{#(Fb4D}DgUkdlX0-vM*AC9_L z*UWIMtFPzd4*>p^b?qHnLlFO9;FkseXZV@_j}w4Dx2ufY$rO2o`eY^L$vphb|G!z2 z(Erb)ZW$OF-Y|yu?`F+*1pY3Bf5AfgTHrtX7V-N6|0=e>}93THM-6=YLin{u;w~_uO?0*sHpQNm-FMk*Dzi0R%{}cPa2KvXDI5^e5 zNBj$xf&SS3)#hyQo(}dO>FyVx-V^D666=3-Kl1+vz#q3GE@95-V~I72$tim<{{d|O z2jTylxD&bg4dw-fVMaHLGWqQPI@o{ht%}O?tC0TTJoX>?patH4g8$Fows+X?Mf_eo z{?FL|eBj^EO-9Zr4e4*i@I(A({NDli&x8Nxj?~pxFGl=YSpQ|%{tbbD&nyS0K_3zS zAgupPZ2#I||7Vu_1$34`{NH)>Zvp#15Am-H@Q+bP`j2Axar~7Wn_E7sS75n14Bs{qF((B;eNq{!5sDH;?^Cg8lCw0{_1Pe+lM4!=t|$ z@MnPjTJpO3>+d7}Vm|v{0sJxs4o;mvApTE0_TLWr&jx-qXTJcIUWmUBkN?X6|2@#Z zB`_{wmKx$8&SU?&z~2-6U-wvU{!JQ|90TF0DhTHGIG1pk^YI8KM(Xz0{+u_Q*<^A*VRvbi1=^t z@ZVoO&-Q`iLi_vE9GvEVLi}%e_&)&uRPg_}#eM-Iy%E0{kN(X6KcgZ3neB>8*fIw3 zPs037*#5=g|D%~JW*+@p;QVh++po3_HSHZ` z;fOz$NB<1q|BCaUN(SQ3#r(&BpNao_b*AVz# z^Z!YZ{~7y_T#x+UlgIxV{t(E2?QhyU{0|`h13dnJ5%>cz|Jn11Ka0oz8U9OP{~sX# zm%|4zMzH_zl;OwmuL}5IV*h{s74d&#_@Vz};{P?^Z^!;`+#l&L#iM^N*#98N|J%34 zC9KE%J$U?|see=;|1X;-ALDKNI}l2Kd#W{;`Qd{HZ+rkAOcA z^KZ#U{Clwe-Ld{jm|x>5;{OQzO#UbF{|E4QneO1!(CX~cR_o?oz{9@*<{!6#ztH|;vb6be*m`s4-o$t z{kPg8eqTQRE0F&g{WHB0zXSIF3Y`DLnfM3#FOEk1dD#CgasH2o`iH51&2kX`5v+eC z);}BKA7lUU02XWiUwG`FsefCr|Ns1s_#gA>f72iN{|Mk`{(m3Or+)_ae@V)#FQ-+<$v%6z1MFW@KrPbbKKjyV3+_#pk&`Ru<7=2whC z`ai(>mtg(dvH$nGg!KQ6_1}T@_s05*zd-z3dF+2D*nd~7f7hRg{}>PdQLz7^SbzB; zNdM_P{4*i`UB~*boQ(Kgc=TuDUkB!&YFALW&E#g0-0lBc|5{`JuiJ$96|wy*e?k3U z2Ie1d2=U(pe%}7a67*;6zuQH`|CUex<)FWjtggP_bHty3`QPIDuNCvF|3LgHm_G&A ze??gT`oV~Q0iXX{Vg2PNA$}h|egoiFjL*$qvHQ7~UzbmR3mpG%WBylI z|7%$PM67=k=KsT|zctoB5%cfo^Z#$a{{ig(Am%^E=l=zuf0mVI3o{}q2%RQ~Zr`fFkR?_vGZ+kds)uW0YE zjz#)E<@5j1pud@}jNFVXNdE()p#F#dFPQp&C)9tU*#EW5k^bpC{QJTF4?zA?^K)?8 z+J^Ms%kV@0b9e1}&;4Nk&mjJ{IQRv4VgC78e=i*W`#}C<;(x@HV~L-XlT*AIe)#{& z2K;{k#J>@c|9Zvc<{w;$_($RRHvz}L8QA`#H&@r3cCV}Nf%ymG`0tMOKYUn3bUwtt z_gMc{tp8QezZCf6FUu>ePf=37hxPxA{eM5sf5|Tm42KmNo1DS+ABOF}1=l~zemlEN zsdsbVhwVQK+kZaJf2p#8L2n`cIqdwu{qGWN|DW`ZC0+-9|C9gQ|1kb zA8)L`avZY%w|w?rf%(_v$}2nr{vxbDWBDw2k>LLo`EdyYbdmqt zV}54-krbMhvkB^-+JxNvZVrfF7u!EG|G3{!TGkippEDg5m6L$K7WO}6`S>+K{|%Mx z9q%#!A8h|*{^ckG^DliFxpLrFg#FJ-9)3pu0GR)@$m!}Yt~4;Txn*oJn@|5vVE;+L zKdJ-q@8{v42mCUizr!NGfERLzKZCJE&Gg7k0Z)Bgq5zw1@RU&`?F*#9rk zzjTbD{IkRMe;xBn@c6%) z$^fZZQ2$0wmXT}0{Mvl}KN$4iK15gFyc+S(0sTq;(--Vt0_;B*__q`w{x!_}3-^DM zK>zMw|9%Vn0`ABo{y?n137&u1V*Xnqh(8hY6aV+X{1=@O{|(?L_1{+D?}`0?gF$so z)6%;7I==X)iuErzhV<_S{G|Sw3H6?9K1DNkbfVg3#0-)#T?8OOiN znBR`Ef876#1^%r#{%yR5`2BeJnf~uGj{of{3l2HVWB)(E|LZaTjZTO^gU|lg zga5Z5%FQ=kjQGoW_>Thr5YT_mkBUmGX-NOyeEd3~f8_J_4*5jHKaAlw@7+fk=f5YQ ze;n}l3_|v=5Bz$o#%PG){Pz{=pQORM`jRzBe@hFXKZ5ml zyXovQ=dGLjY(D;j;QvPUegP>;i2n?a{(XVp8}z?%B`)DmSH!=Uho8y+GqL^ecSHQ2 zu>Ujhe=g{+0`|YXt)lXvG2(y7r@uJZef&O+drxQdw~8-{@)G!uCV`S$j840&%c5d1A~ge{*!t1e+T^AasP9^ z3*z6-WB-#t|72YMHoGGJddx3|>t7AvmjV7HBgFrL;fMHl4cEU>z^@4W_DP6;3f6yA zUv&O+5cp-L$jD`ENBq**|2tv-KMDK;hU)6K)gpdB9{rj4e-88?u-U;W^oFy`SkT{* zkN*JZFSF1uz#I6P{{Jf0pUnS@A^+80j!QTyhWHa0ez5 z_5T#`f0}0)D^jxP+D65dS;O&(uGiK>x$QZxofAudo#Hhw|9}GO+)4T>lT9f%wxg ze=+F4;7-f;myG|Hws#Cah4^P-{cW-SQ^Ehuv}EKKgd+YGSbwJe`vUwPxc-0GfcSgy z@E-#HyO{sp9cLFk&|e+%Gxgsi;O~q3|J5pp{~E&&|6elo-!!oQQtpv9YpH^gUe)%%QABg$4V*6hJ_P?Q}qOxKp;y;f0nfiAW@cZEW=YA6L8)E)x zm|unIe}La^C*pVI;eQAGJu!d7JH+3W&;G^0|FbqYIF;W<`j5o?nYjKLjQMAdLi|No z|L0i$pHTls=ENmT$NWFB{$&3D0sH@x6^Q>Z!;jDZ#)AHhz~Az{qVoQ1#D9h1$LAlV zF#o*<{AbGAJ7%ULeh19Y*#8f(e;=^_Gukq8w{{_Ze;)p8z`qd3fBTP!e;AMcCqe&_ z;Qx`{4o)MA5&wBU|F6XP|D`(OU&y2X{*#f>NjU#&OCbF_F#i&)|2l~O^P+R}2dzT< z`+4mD`Mc7xTiE`un<4&lSpP&E|DNOePwNcgx8mXNG-`m94$gnu_aJ^x%)bQ3KRM7} zcaW}r$OptffQNtPvU#>^!TyalIyhaukN79>*#F#_8@w%n-)uhg|7wW;8P@+g)<2Vp ze;48sYWpDm?>zp0XlGVVB=|ow|BPLO_)la0;aJo_(4!T;~?&&~g^7V#(Z*nbA_FNX8Km0v0< z)8-<6ONO7P|Eq@h-|G>ae`O&4r9Awbfd7~@%)d2dwiOtf06xl^}W9$etkZEMO^=?JVE?@ zF@G1#p9c1yYvmVEHy-gfG5kFH9|M^9pB9(k-5=>+!oxon{J#g}zqq}*`HJfie-@Aa zO#Lqj{Ba*ED(h_!e-w}XZw3AvpnqI(dxu*#;@`-lKjZ(AF#m8>myz2ah4^iG^k@7( z8~R`7{3Es%@!#gL|L0);tAYRiDhHLi{OOoK1IK?0(BH3@uDJvz z8RD7HQ4`$u-tsBjRl40X51`t;IaQr5dZSQ{+aVn$N7lgpHKg#aQ?aH zVSC5J3yA*+=0Ae<4}tu*M?*&L9_G*IvHwF5{|@2&x8x_{e}wgagyY{&sQ+r$I5^FF zf%I>}{7n7(1MI&X_>+MD-eknzpND@i=&uX>uE1Y31o3O~=syYcmj(W$eYyFDK1lz0 zJp2oQe+%%}eyXT6vP1k{n13nO-wF7W9zg!Tg!m8g>7Rk~|C1P`e-01-Mc@wyexu&H z`bIwx|2rQ0XY}t0`rlaX;56+y;=jYg&*VQf$bUEH_yy!lLj0q7_=f}kDVTp4osCQQ zG#K%V^YC{9{&0x@X25@R6XM^(@bkoff5`u(4HcCK79f6eh9AfOnc)9LVE+|&+B0k-K;Z@rN<|xc^%K{J!A-xjl6C4`BW(%;}S}w5x*$rPr&ofx#0hbJ9G2x zd=bAZ)_()`|J6+W`>vwW$sX|=WBqNg{?nlT8F0J35e*){Dh3!9usee>uc$j zH7+4)DB>T=$L|jHZ^dpn|JscBeKG$=%-;s{FQdka%D9Dy-vRU6V*U!qf4cYDJ0dP4 z{!Cu`hy9N&p#Kfv&p(X#BYEuq2k7qv`riQl;9rQpmB;?KLi`^K`kO6xa9Z&S@jv6? zKMDN(K!2lIegTytL z6_uSF5&tO6&-A~8!Tuw_{#V{@?`X|K{C6?`Y0#h9|M9@_-z5(5H(>s+p#O6?|C|r} zO#UC$j`(+D{`V07jspK^;AiUpuN8#^e9X!2cV;{+arB zkqqMB#PIXfzrSGr&2JazAAt0i!1^=uzekOwWoMZD5By?`^J6&?NbAT+F<@?$isgzH8Of2@cSjlC2Smq_+5GQXX;-au>Y*> zx%sz55dRP!ey09gg!A83dBm>+{LK7!1la#1=>MXb_;;hdBeVeVzXpD0{(l|&|3uvX zzN<$3UobzJ|EJ>oe>M>DhXX&!|BHeDKK6f~qliBf^QYka{{;Jg^)#gaJm4qipC^G| z6WhP?V#Gg-Pk(jX|90(EP&fnZe;^P4e$YP)^k?$_EaX z|Hp#4@Kz$Nri6&k^|jlHwB9EJ6Ak^VmOg{wo3X zPi{zV{)4W_{zvlg-+=koYOwzs^%a%Zl#u>nJpNw`^FIryf9@BxcZ|P@_JJJ={FizBpE>_N4E3L)yMt5rWW*oK!@m;d zpSe)~eK7G0_+^av{doBI!2Ht@>OZZNxCAvf#P7(%e+Kxs0KXsbFT(s&dH9+0?|#7F z1^DM;emTsqj`QDb$bZcK+jY$Ug@>Qfe-8A2O#OcY^FIcD-t&(f9RFu+NBrk`?4POs zYH|FZl!W*rcMD&`DgN~fl@;t{-1P{ zl^Y>HLLt^sNjc6^MRg3E|J0d|)7)x4L3;}D-#6CNZ<%ahI0N?o491$y{xQMa!V>B~ zPc<7`JK$fb=;-9^yu`&(#?9RW&c8JJrEB98jvYIms8x}iavJs@`jn=nZ@qmkGaLLr;M%1td2s(v z>taFSP1yhVka>sQf7+k=xa2ADXC%HTe+}aQ&vl+&p#Ox< zKED2N|M%Iiz@T6_|0(+#%I-h>cpn}S1^PFjBdjQB^NNBl~sAD27>{*lLDl&?yDRr#j+O-*A=U43g*L*pmN zf6wF|jgL@d@e`h<{?~Dfawp$bF>0*o{k9 zu7dtOatjK*@@^L0h5Zk;jN*rD&LI62l3tX*0{wp^RM*tO{>!cC2KM~#Tv$_cE7*T} z$j`PvaR0|SKty!q7Nq}LPo#hB3Z(y47g@P*JH&s)7V)bC{{-OwX);+y5AMJ3(l;

^ZoA`v|VE@BkCZ`N}jQEuvAb!bP$o~DVBK-$nK>Ck6 zi}=+}ApUx1#NX1h&>pb=)1mivAKY_nSJkSfE*6?-9tc+~d**e%QVEMHL@ca2}V)Z+Qzz#sCop|KYH|4CD3^QV>< zUw6K^_Ups%7agJ^qN3e^f9-@G;$1-h@%^kNdk>i0f7#^WLwm_imQ#>dVB??dKowO* zi2wb^d1{W)8Zv(7kWN$7^oHmg!u3Zc{y*?CooxjBFY7HOt>?`1u^Q#Gc%hl2kCThD zi_22bzs|;Eg)3wK>oh%AZ?Ie6-EP|EmHu`C+XJ@-?Ew8{vO{-ohxmU!yd+{@nAA#`ESY)d+Ba6 zjltzbh@R z9U%T~wv)D>;jnIDU-$@YgPVc-3Xc_R{8uqs=jjLef1!_#?@~X@&0mgP+!h&pEM#}+ zZnpk;a3(w=4)WjSnDW>iapw*{3OsZ?D>*PF^;9a$Um}G6q7eRgA^b}O@T;keV);!3 z@SB;=WckMk;9u#njOA|?z`rMSC(AD*fd9;?lPtf25dK+0_}2*G&jNlq(0^Hv5krT` z>dHCGPZ}XUe7;!!QBzdKA^wer`j4r9!n;h?(SrED#6Z+=k&(yrNgiuwIhcD`SV8^E z=DD=*wyEV73et!SxdPy_bKgxfVm0A_mHSZw)w>I8xO#9gKsj_8T$%B@+ z-z$H9Z+p}6f~|jhbrpm9UsSx0M4vwWq5dx(HBd?t>feuIe0wvb=*jK62e}_x z9$b6eT2l73tn4MsKUAw;RlbAz_f}mB{C{`zQ(STBr>dr>&EH$Uv-NMMYri|XL;XLp ztBF|u?n=G=)U|qd>Ni1BYM>Nb|H~@K%8i2hr<;T8fdA3cQda*6;BN%|BU@k8zi;5T|6gq#zdJfQgZ@@JaQ^}N-!T#veR}rQ8{|4c zZBX|ideU;Ta%}$_Ag!dV0QIkf-k>q#$Mx0pnxHmON_UoYH{CJ%?qL5WaQ$Hx^nYre z<`xUU|2Nu5*-l^JwMfn1bI#c)7P0-$#pdFN&td-i?P+w`_2;dx&-|`^{iXV5O?_QG@V{@2D*5>N z2lT(sT64a?`SJA6o>w>i^c7JO6$Ah83H<9dCHf2o|DQO}R;u&h3By+EDGnPe-&H|b zNm+R`@TW_vYfJ+FA2h*Rd&H!HQ_W<%Or2}s_Lu*L0RF{pt}K7G5dOhJ_$LVAw-Ul{ zA%s6k2!Egu{yzfviT>q6_{R$1KboAx>c2_=|FxSpSpB;Q;IFEyW%XAOz~8BB7gqn5 z0{Dk3DYE)E3E(#{HfHVrqyT=Re~1A7t-;$_``;pj-$n@k{jYTUC;W|1==M+eJCCQ^ zKjEL;pKkxtjEz|Sx$EivPxvjYR8ef@k`RLvgrNMD0E}q((nx2-PejfZkc2{;z2F!ns4;~e*XCR^H=cy;=8Ti zzrg(G_3!+Sp`FybZBWwfHn@lS-~2y%^gyY;kpHL3I>}8LE-`XmSDBHMR3ub2)HT@p z$GhhQ?XfWb8m1SZzi`^B8Cq_BGZvVxnr&fj!PY-A%WZ7uLjK?4DCe|!vH8*g#tut2 zd6=(Qvue$n^$`Ci&+_tK3;BPMzka~st!X=2l5=+)3QhaVe@h5|jS&8wLinSE@UIuZ zKT2JV<#!UoKT-hyvQ;Zteo-O(?}0x8^#8Co@yPLGTNCS&b5DevdAa-GnXBp1pg&Xp zG4ntDZS?&A(1o`hrZA*RYJ2wA)MD^b@RR6Uz{eNeee=zl*$Y1?~ z1@II7j|<^{D1d+Ys^u(ysSti8A^LX~qW>Bp`fCZ$zlVebTmL>5!v9ME|4gv|d654@ zfZq)4e~yElMS!!bi;K69Td{}N^3AJ;Y0O@?&RJ0YpLd>~|4%^vKLzt|&jV@c*`R-U z@~)gqmo8jMgZiJ$Kh(b6xmN`J@6m$yPo6z(DT{e=?Pc@xvmGz1zSa~%{r{oi9ozp7 zc|h-fC3>LwU)31y{`XFAdjD%~YRdM1XIInvU()}1kE8d$r2jkenBM=A{y({iyZ?U> zPVfIeH+*LKOU}{xN&i3GlDq#S^G^vMdjDr_Y0dV3u0!bkpYJ+fw*Q+v*~ix#`oAyX zm$&T*P71jbx_?*V&Ohm=_iu~Hg#IrwW=<^>k{|hwbp8uTi;GX}K_n^;zT%8xQ^RF)|-18r)JM{Tq z(y^oL{Hs?3_x#UA2>${B{M|&lvGc!t0sN}+s_gvly#W3h1~b_CUxEOBGXLBvfS=4i z-GKjJ=3ho&|M?LAZ=c$B`D$KVeqh1#>y6hg)PKGG@?InOf63z#IR7atQ;K;}{s`v3 zziY15B-gdSE&6cy-m?#XK9@ChG{67;m_7e$f&NECL>$-uF#j@>=pzI5e-9}wDZ3$^ zhH4I8tv_`5cm+`{6bstv21HgvY0vtg;XlaIIWk^npQ{9_OFKcS&PyLay01NBctL~v;o*#F_!z=Yg{ z>SGs=?>_o0)6Zr`vFCpoF#pfX%gMh{a1G|4MMaNx-nn-T_=`)vltewf z|Ll9k>9)5O4=RhQKiB-KFJRCAYN7sVYHDl#)B1<)|7&vT{jco6er*4%=s@rP$4y|) zKXr6G{q*$Bp#QTo;XnVdx1C|{Z|?~6pJg!rc5_?fzQkh*JO3IrmOK7cKcdG!GXJ~% zg**PG?We~-GXGqkL63hV{zuR4CpiS>UxrgugwB7?06*9K=Z*~b{Nt@I_xwY&fDS`k@qg9c8-=>!WBTI!tk* zav$h_N&VBbfxG@aG=RJQP0`}6f6c)Dx$1w97Vi3gVkCF{FPX(%|NofBUH{({z;9}9 z&es3Of!`YHA2su(i8@Vq(@=fO1$Q{x7;Qul4==}3+eR4`7)ISw@2hW~Aw?DHr`^<&t+`F+ma?j@1 zgZ|fVUT6E?%CO>x<Wzf8G=X6~4aFaQ$M#m%A0k4G&8nKVtVE z(qdnfmqPx#UY%I;r}oa9Lw6p%Z~u7bb4$~g=5KKRv*XIowiY=59wA~VI;@LI=S?bO zd&)?t^cmQ%pX4Bje+l+OqzAzHZ)bTIg{g`?Ml9&jceJj04-HMof6V;{&*O%o_7Qj#H-*y4~^DO7G z^>2g_{?h{Zqr;F58_L!{aB{}LDO^Z$#Y^!kUK|0gKW_aDgl|BmbQ{fD@iLu~z@ z_J+RykbmjYl}lj%rP~S$OW^$bq{} z)Sqhj^1krhk%xD`ylE~0`)~T$(e@MUUv|2Ps3e?!5A7}?Eh%B%M@m$ppY6bYUBrgk zNsG!j%FSKddBkw3F3KJ%V@HdQTGXfK*b$nNVE_7)C+kdx^RMNd4GgW}{D0w0b9-xZ z2@6|8b4w{3OCytoL+lM5WS#nsoxWtTt&zKm$J*tF%Z6FZTD!#48vK8qPk^rv^go%v z9|7n8hj;Fcjo4cp79F%Vyeukwd&uDzu|aXK5*~focKm4cf9juc=_<>=_5aEL^=CWF zKl}g5PwL;Hz)#!%K=tn;C;buJ`qv84|DFK-b1z+F_g~HfKiB?8U3E3P{}6te{%j^J)S%M zM?n40#Q)wex#PdWFM9k({m-+qO^506zqzTIjsK39>G7Y$zw-;}@t?%M(9QJtPvT$I zP3I|BI$@$NxnF_(}Zt0{-*h{~3?R6y3U6S`^W+>)xm0u!naa{eG6z`r+B_ z7r)D2yh8hbx+m)D-?H^@=fK8XLg=e`Pl{*FgMx|KoRK&F{PpF-chwldd8Iql{jmmWJqs`I4LI?@pTjSQx-^N(Hz`$gbpI#ypM^iS|BsaA_W$2{-2Ok{6+QkD|5p{lKOOkXp#I(8^zzNy zy8q1or2a`T=B|Hsfc~`oN0R?OcID20rAqYt=j(&^KXq<$*FQ_@==BfDe?5Zf^Yqt=>u1d|Uun_La;BB<+>zc(7R<8uh4~Me z|7}y}o`3ojbI(8Lex%Po(fsRH(Vacq^WUm7^!aZWkxuOVQ_PG$|5cP9&dxuluB6X@ zjSbNLzfCXG*#_YM;_JIvD;lq!ztC=zy{x0rA`934hKpP`x?8~hH|c+^#?t%W(7k)v z{_oNwdjESe?F`%hZu>%?f7~f9X3swx_S5Gdr2jvkLGS;&OY~yTf9&Sc`+su&w|FhR z|DS1Y#?C+b^riRzeoc}%-z)#M9lYzek=3i%u-@blR%|HMB+WfWk zD?9((b&=lx_Zx`z-xC(l`~R^MwAlTxj7{|UpQViryZ`lYFn#{F)@uVh|EZcppa1QT zie&eH^qC;Ly80{F@P+c)5!4f78zn=LbD znJ$|xVLoq8z`PNjtLM$I4S@OA0!Ih-{zvL?H}}P0|8rJptXaO+abp)7vyIDq9DO(W z`?2?5tgZ!zY=!gxvwI(g?b@Grur~Y5!JRRAu?g`B?Eb$`RdUKvn1Ag{`<#9{BmTmT zs67`>UW$kQColgx%)dr%y;;Qk|HEJZJ%3ff&OiGct*fts`tQNV^w00Vy!y8N>HTl- ze!gn^@w=VffBH6!zW>DB{}>MYZ?6_BDUX5q=TvnQ4fk>OnoA~3`aA#6@4_Ab$obDo z4UZM>u>al3dDi-kUh}*s`z+pUy{Wg;<*f&T5~2Uw8;b7##FlZ_KS|%Y>z{kk^!ksS ze?G~f&%c@bPrYFOeX?yo$^KCP^c>OFDF#$?Vl>}m4eWmr|6eT0J^x(i<;B{+i4M2@KQE`-KiPl#-bS~7GXL8bOSk_o ztu3tor(U7^Ke7Mm_H_Rz_HXC=Z}#6?fo}gk{^>)C>od9Ie~dME{6Ff+9seJK{8`SbJg zS^kP`H;W1({^!2>_Tb55{`1c_HDv#->Ga3XZLt5fz3#!cUq7F><^8VxqtRu(!qhH< z#55rO5&g$naofM#dT#q)+@IV2EymOBpVWWPp3v=|)PEgc>GprS_&!_z?KnWUe^URY zou~Uh+5g!fgx^sBKiPj%6u{5Cf4LUUzeIrF5BA?y`%48>1n&8}|NHIk-mq9W|ELW< zd+>0~#n@Nz+mD1LJqbFJbRi`S@;~9f2mJqX{nLGaPT1b!o#Q%?N=C1#K z4CSu>9#7%Uf1_S<=RdJu^!!KazhOf7wFU5#^REX2_(}e26u?jJKWqj56>$GyfS1$K zj?2!Z%_EE(N@HhQ0yeg*cw zZX}0YxP0l*mEU z9Iet_b(Ol-7&)!!lD)M?YRkd--y}U<6#pdX@sGLx$=rWlqs|@wB8%zqkGcQJ#Q)5X z^!Ufz|7Ggm=X>e#zw%WjTmRlnqsM=;|F33FkN?d5Pv-u+_$u!Bzg7T0ssEh>@RR!I zngD)M|2GQYC-MJ)0Dk8FJ9Gd0k`Vr-z%L5%-%oa8@BaP9O475^{`U|i?)wklZqoN3$^8fQI_~=qM#1#`Z|45@JLrF0 zl0U-y^K0|l)`ag-KU@CxKl_F7ll|`;0sLhD{gVKGvj1HqfIl!enB9M$DuAEt|4Rwr zFSv>BzxNivPwsyx13&Hl2ibqAFsARn%PT3d`~PhM_=)~!1n`sl51|71$^Dn50{Dsk zh64D>{g+8X_y-E%|0;xE#g%UVWdDD97rOmByP@|Fd=$Cu|G?cf6V(2%>B2Ik@Wn}>_0H?KitjY&i~5u==q=IzbzZ+`G2m> zJa+ygJ%Bs^kJqB-e{%otPANVAlljkE0sN%@4HCdl^4}o={3QQ-3E(I7kChO96#@L@ z{$nQr{AB(mD}eu8HhTX0uiw({zmfUR0W0rpSg-#Y>JPvT#J z0Q)ELZxYx)?fx5yfBkCd@t?%M?%V0{pY%WSN%Z(9(zPpl|LM6AJ^qpW*Cc?S#J{rw z_(}ZREr6fIzr_OhN&IsZz)#}e6e0XlLil@|(D{k}%I@^|NAzDLMvs4PkpGuK{`Xd1 zwPpkKKTEyjyqkSC{H=eJ9+LX^0=51n`tPpi*8fcixBlfR-1?6f!oNZYe`f*wM1LLN zr``V~_un2B(e0nq{|^Q56a9}0;3xS%UI0J2|G8NJKe2yX0sJKY-50{&D1e{Xe_97 zGg}D%lOuF~GXLqgO6Mo@uWpOD`4zTs^UoYc=O^=Do2g#jo-qIEwaQ}4w!ou7H-qDL zr0n^XdSTCzupFrWNd1%bEIuI>`rp#*@Dry`?M(fcmUT8E>w3h#tP?p2u>VWupAGxS z{O>1q{#RXB$KL;aeuB*Z3aIlxqQ468bG`q7?mtY?*|d>+{=d+1A>03}jB#^!f%q@u zKY#W5b#px@c&*%M>(|qMhTrOd6xjbJ@vrn2v48sfi=2Np{-N7HnSW#m(4W|Uf)M>( zh3Ib}ME@}Y^e6T&4*GN5f9?C4ZvTW|PXPbj;=Anpb0P3^o&S>h*X;s#{l9HKz5XZl zZw%;9yZ=GXf4>Z(*Z-vcy)%)!{vQeU&-MO?=r?-*L*9QFe30J%5PoeT{P%_MHwxe< z^S^BZ_?h?rHi7>qVE%)_pF?Jc*6g0Sw?51!JT4*}-oJ>0`wts{KOOx4LQ+;r&Z&mf zijQ;B&L92H`wyi4)!RX@e@Olx^wPY8y!EHE$Z%2Pt}@+}du>*o*lSoHZMgr0_CKVhXIje2slohj zvNHevw~qEm9a;AM!{M-)nzlQJp`_JNH zgT&R@_b&&-{@=iXa_sx3GBE#jS|zVA2KFDPs2HkN!~Vk`*ng;<;H2F!Sqt{x_1O9M zwI0SMhH(C;>$q`_mE~INfi`CIy&XpTE_ImWWCHW=C2mX5`zP|;??1o;=20f8`=Mud5U}fodfpI_5Mwq2lxE@C-8Hff1UvUr|rLx^G`$2pSJ%( z?0*^9KkfXN%)g~Vx#$17z|VF6kMuu}4e0$Z>3<%8|8u>6lPW^*e@Xwh74)a|f29BM zhxo@e|5#W}?|(`EGY&I8$ld=w0{yw(|M|U%yZ=25 z{?E1ll=9d9XHffJV*fM#+W!&G{#P07pSJ%<&VPHKy?eo65A7thKL13$X|u&;`C|AFlPn^=g5_JR9vn}#gy-djSgk7d8^ z{dM{-)gCdpr?d{#e@gP`{r9ErDyqtG|7V)~qOlVv4AAzOJba4eq&ae%r&$@g!2UnE z|0X79Y3&5{?Y?!UAM zvHusqPkaBI-2a{Wko*3N8|Z%;-v7CLDm(Ra+Uv0GSs9u9??00Me^^lQ!`E>C|JKtx zWw9?#x4nB_@gMi!iT@7~;{W9S%PrvNy8lJ)ezyEVYnSTF;_`etE&-MQInYY~c zUkY|`-+%dWg8Tl9+zk5t7X^9r{`thE^!qP{(El>;pRee~%|C4!Y>N^ z=>DU`A=>>%a{q7mH}d|o%{AKl&*c7Fz}f1W&#?b==%@d?Pap4pKG*c_Yx$QQW!Hba zZ!3rUpQ0k@{r|H9^e6To1p1@=m&}>}ss!*8{cj54A1y$Ca{slD5d8;%{^e{eMC7-?urW{)wm7Kg9n_gy>J~-%5nkKgA2U>YsHg^!u;m{{PN9^!u;m z{%=|XvHyHd`~P*0*nc$D{>lAE6}ukdVo?7q7FpAG(7^w^|4rWiAM8x`e^USX|K(Ta z5Nrt+e<5$on5LO{D%SJ4vhm3i1CJJ7ehikNkg;c8R?I;P!>~{zI=G-O>LC z2IT$2y@P4*-;?(bJUmGK_jDSq{v+>SOd3b8f5`pcSC0dO!r=bbkC^kj_U+}L{~k#o z`S0$}Q>n+{|A*t}(lS?P-HQI4aq$ZO`?uu$FE5zh|C00H?aB21mz;mP7}NV-a{qId z8@>M}^RLHUx%=O@O7!zTa{ou-CjI=6?Eep_qxb)W-%|+xLLvOe1n`sd|DC|E3H#q> z>fNSJH&|@g)!1Q%yV;Z#8_gD3c*FdQoPWhjkp1^X)ctod|DFDnKK~{A&mOJZ^WVNv z^!}gBzxA`}{Xf}%tFoc@|AS>^*!}N2UiAK-?7yEM$UXlj`yb(Y^!X1t|K3qSpMR14 zr@VIh{EN&#OXKMCPcr{)&gGu}j&|UF|4v0+oqhjU&XsQeWdCDv7c&2)zyC(&zs73Z z=fAJ-lldoo{zLA+21U~KC-a}IEbjR~+5fQvey;cL*ZYzAH~s!6IselJe%kztoc|A2 zB=cYT{EO^=d<1^3^Y0rUxX*t|Up;5{|6_ok>;CVqdEEA&xq;jMYX{KhU*!C^Sc^XY zBK|+N^xx)R0|nSWng4bXV*d(4?7vii{$&5X8T6;k|H=MWt_+#~bH9JE7xbs?e~|be zS;QUxGwZqI|I3hn+y7`tr|*A|{pW5L^!*RA|E;iwzW+hu|E50N`yVzM-2Q+5A-Da9 zeWve!kp1u4F#7%nx&K@sfWQC1{_Oj&3xIzNoPSSLnykD_Wuf@EQECGJKNFLt_y45+ z*VWN0ZZS?sEssGYqx$D1eSLpK((*L>J)8`+g{&(=D&p*ifKbivg z$^HNL0{F@QUn0Pd-akHefHwan{om9YGXD*|%r*Zt-%aMfLpkTahpkEf^Of5Fkoj*Y z@T2~>(|E4=?=Mk$|4;hgN(Juuhhic3{G$i()BZmr?_aGB;+}s@NaUV>yfmcGKS=-6 z;X?0!NdLE80RL)_)$IGnsY3WS3g9R2zuW)Se+Q@jYM?*u{ZBIg6iFrhKmGiJ`Tv!< z|M%LHe*Q6TvL5^XwPAnm^N$VV>HQCx|HeF_&wpcM(EpcPzjD8SRCj>;{iFNmt83oF z{O4CgYvaTZM;@m9_|nY({|~wUrN4+?|B(HsL0i-{ro#IdBPXb6znL^kdWDwmfBb(& z-oG2tll%Rvv7_nze^hul`~F|qUGDz>(|gkYbNzqzy7CI}bIm`NSaHw)P1n=)C+A;d z1?W%CKZXd<-`~fF{r|JG5dBp_f3Elc$@$NM!{q#j{{I7+f4U#1`#hzY(f_#rM)se4Khg6) z+5hXmk30XXoTb-4%>T!O!T+z#k(C?C=6?(2F{2DsRn><7egF5xax(wm{{MD|0R74Q zW0es7=LzwD9U=Z-n@zWWa{eh{!)^Z)y~zBN{{A01|FM>(*FPlxo9WT(pWwhi_W#es zSM>Ua{C^tvn_mAA`)`fswtw>fM^-X9|GfT-cK$h}-$3^LKU4D|(ozur=Z+gXVx$uP z{^LaLsgtL&@qffX?)Q&8oZZ;}FS-Ih*ZW80{NvM2dj2Q%k7gY`|C95Ng~9avPu@QY z0Di9jPj?v8{h#FjR5!Z+llkYDu5|w=_0Ll!y8jdYDFXP(`H!3sepdnfB>tHS;Xf;c zzgYmk8SH;r!25THfPW*L|IeB=({797%tf0Q?^!ayrjyI26{0KrR+_C@x^~%mk#*fn zhWRhwG|+#|mIKF*1?>*0-EnAV(XKsVANB?AuRIWWup>J9P$azn6`zm@``;^?l2gtw z@88GAo;{kG3-5oU|1Xy}o%c6k5%`oo8hZ#=mP_kWrFSLXk7J@EgJ zkpFiz)V<&K`)u8pra#S%tv_2EVE^|Ad;Zf|^sQxA`2V`6_^@Fz665=I?caNVi{vtu zwQ{}WyyY~9cT&())E(JRZK3oGm8oiW<3^4jIdGu%D4hjUG;}?uPBR=neXf!CjG40( zW-l_EW?^k<3G=`Cw&x`5;Qg~jPJJzHmuzrf2K%q*|J$vgzd!81yZWr!Ea5m4{y*Fo z6dN4ABL?<=_rm!<^Z&U^K~!`&?7#hq-*e=_xtdc+$){oe{qm_inEz&Ms&JN}vK(&HcLe~wnr;~$xS2e;GXAL;+v z3@}k@V^wmPx{{uA^ed-_(KKolk=a|Limk^ z@J|uIPyYXw6TnaQAG!WhvcAdYF9hvj<($UKi7at`2y7r{72Ht-s{hz~j za{l>-`u;ET{~vSzf5jR4`?nH@UiA8({C`laMz8+^g~UH(|6*dTVE>)q{8K`b|NR#Yg(=D+$|~^wnWDOe#<+1hnrQ!5M`zNC zfB*jo)&FbO_$sgS+ywF8&6ofG%aGj%!cMXAKQ1adIwmFp@*ng5Wn$um=H!$l$baY3 z`Tsx5%zs^2S9lZb|Jt2<_lk>)@c$oWWe>yt{r@Y}|B8$ExBvJ5uTcM|ufIadz;Gt) z|JcmtfB)0ge!7#7lQZ1^Sn1~O?&0CGg3SMCDE!;}KlL8>{Bv6)z5gNe&zRlx{)gOu z%Sfg7KV<%EF@cx~Qq_K$s)IZi1E9Y2Pt+krscEH+a;aq!b$p0k%Y3tMT zUr1mGTmM&8lKJ0h>imz?e>dy7>)+EM^!k_7e_<)y^>3sJcl~?GoxA>hEk^bqmI&DY zT<7Tx_TQ-Cv`TsHzzkipRxxOYlCkysJkLC0Ke^>bMK}l=L zQ>cF*y@2n(UX@kWR#(CQ7xncz`3;R#u>bn8ng9Q@L|C0W{>Il95CI6okU8UE*r2d<_h+hAb z^N)eRKLYl@M~qX~JUC&7l#=cwf&U-(?aY1tt2csv{zu;betDgK{zvY=*1w^j|B?G2 zd$w`!znnVGz5lXy`ar2cF#lR9@l|?+{8)v6uDwV0R9T@qQB7p@w0_;kD^BS5_x)#$ zLEQDvfQj7o&(CM{`iIQFUw`ASe+&* zA@=Vs#Qqn7{iFYXhJB~)f06yy^cmdwuVpEB{)_3xJ^#5nl6(GRc#C`fwd^fD|3A3% zfc^ia&koZ6Osu5sf0FZ`zGvzE54r!)I)}Uexx1R)|B(NG(|dFGKikJ}_dhWO^!|s; zKQgLG|HJ(MQ(F{z=h5Ao+gCroe{{X18S)>g|C=sx$Nxcg-2IP^4|o5wcnID9$@z!z zWN!aI^MZT-kHr6&CVKoQ^N&B_^!QKqA4|^B<9}BX5%&L!nU?hUKT>`q`~QXWI_~@@ z(~mp^;_EgKeGScX90Ko)7nIje|qr$;Z*ql%5E?{{*nFX z>67U3Z;Q_s_WU>HIlcZl5_5#T|99XAcm4DG5WW5(@&EWm?)Yyagny*~elq{+Cxm~Z z0Ddz6Di^>{=3m!@@TUmj-vj*U{!8>H+Weoyf1kPZ_)p@W)mrZOuicm2f8zfCtYsA4 z|H=8c>}|ULllZ6jj_g0t|9>a{-x~b2fBOGlr2aiBK>uMwhOzrEAp-O#_WxCg{^g)Q zy8o-Zns)z}^nWuS(eoe4|Es>x^FO)&)_Fg7{@2N%=l^aZ-Prv9NPzw%|KAg${|V6F z2+sf3TS{8bndf6Q%4hLHGe@7l|9{1f<<9?Wh49-5;3xIZ-3V^}&zW?7vj5k`nr{E3 z|DEi~ZU1v6x%s^`$@zE3M%w)+^8e5Id))RP+epvQ+!~c&nRXR_Zswbl_Vz6NPifI$w*Uwxy zdj;rE_MZlba@YT=3iSG)+<&@ONU#6N{inJb?)-N-h<<5Vl2k7x1&A;E)7oX>j|B6=R{^RKWwEK^w z{uf!!-Ty4>NzOlvIPX6ik0AaZN4@_@-oL2*MfM-FsP`X9{~HYaMG*gw7Faxc_Ozue z=Eb#_&CkzvysY|KQwaMn_3{%qQ`%-|1?hs|775IfcUrBPTGEk!@7li z4Hhif;AZgm|KDMwx&6Q9E`n$^M_@Xs-LukHG%9{{JQWUmpeVllphO5dI+o_>1q{VdsC#|MJuK9}xfd?>fN$ z1@@n0Mfu-9l#}c!HI9A%Z-A_voV>iM0=oaBs`_@Lx`r<7KdWf-zyC7ZWd7_`v&|v@ zFR-+>wy|-tMe{Ewr_3qNF3aHk8?P1o|NnUVE(x#;2!!`<_XLN8goXy~M*m+$M9g^* z6&(rZKYvPSo`hxlf zIRC5t`1x~FQ(H4S|7~j7IFOkloL&W(1f0mZ&CaovU{Qo#gUO_=gNkds#MVXy{ zyb@slB>ydxpxZy$|94iW+kaSS*zSF>|CL!xw|}z#AM%lI|789fy_Y`!A^v|Vjqd+s z{l#-&^dMfoO%)iny`QN{NSopH2x`?U&ZxqA&rUHT?^;`@j?9We{V)EAjllveGWEy5=3^zt+avjcFfSK2^4CD|yiJ z_Iu^e?`>~7UUZ0n|M%)D2It?R;^_Xnq~vMSfl>qD{7*zqSI${}(g^Y4^Tqm)nxZPM zuAnhSbD-t~=zqpe*3r?^(}w*I=KUKJlf45?XHSFpXKu~^{^=T*&F%)U|FV3A>#8;I z{;RJi`v2G0_kz5Cz#fQyF(L4JRp|a*i97$KpWeSMA~PyBdPi&?=zj$6zb3-_ujwgh z{*j*U4E}!+;$QL={{PR4?mc~_HKW?w+^FK2GvieQ$f6)1Ve!hNuLE&xKe|mcF-MziVr4Kqvk2ilRy;aun zqOAOS5Oat*+Tu(`$m-L@C`_(%p2&=(`(O7%nt%HZcYJH!`=du(Y(dM*siV z+t2Le=(G~{pM2bn+*Lf*E;n2@%wpEsC7#w^zTQiH9ew@5{&xlj1qBC(g`oT&7Pk9- zc*G&_|KM2u{|{4A&!>l`p9A~Ph5hGCmooAD_l1RT58W)f0sjB^;fIIek8eL|dvxOa zo9B1S+g_Da{;9cH!~FkM)zH}Z@nb#w|Mdm>pYPuvC;fZ=NB5spRb|%x_xaye_uunB z+W&}%IDPxy^FOM8i;DI}{(JsM_g~xEGLHUx{zv&wL&Ig|IL%4m|4KUi{~w#owzBX5 z{jH$>wY9agv;O=4>C{PX|M33*?%gGi_J+m5{!?glR`iRQt%oxYeab(4^n7Aaa%#$j z)V=Wj^V#%^=Q5%G$;bOI`S~Z}{@wo3`HZ?E)U*gxt2KY{(n zf&E{ODUaO|ckb|`z(dEgk^^!4JDaZgfqVW-^8eiJ|DONJ{$t?Wf6xDD{;RKFD)Zmw z|Mi{?qqz59CW8G(K>qu%m;U~BPIgw#W!V3cuFA{53HKjP*S#%hxO3~?E69Ij{_O?) zT=zfC!2iEP{$JaVe*cs7KQVsv^B;2mXB+UF!T#rRQ}NmJ&75ZqU$@N4$Y!0bqn({S z^Z#Wk_&@FaKXU#vuYx=N`?qt&|Eu|mVE^|a{`0?o`0B-ra_0X>@m%`+P;Zo;pms|DAp+ojw1I`b_LU z@I2T3@Ao0x`aewJo_{KuaL+$|-0AaAGXEVW#y$U?piH0tlKI!&BKrK7?0;0(3z`1{ zKYIU^e*W|O=TBCDcPG03%=?#e%>B0jd4&ng`}d>Nbu`sWH8jS)8*kLLZTyr;{PBO* zZ07wFYl#2y;x@LXb_?z2IM`VPI3IHHbWl2SmNLpx9w4z80Qqe+EC`H;7 zl_V93l0^LGzVAJskB{$p-+q7IKcA18GxM4`bLO5p=NJ_jtrGhoV@qsOf?slH>fK9$ z*Iu5AyjGMM1^FNS|8G?X{rt1BKmGi(doum}^9eQj{THb=^!qRFv(fLrXk9?R|6*w= z{rvaTG5Gh6!LL0Af5I62qhs*Lj=_I>41S$4_#MXJp8))H|3CabI{KRx>Yrwd@&B*? z-#&21?|(u6|4Zb<@Bc>sf4HE4-~WX6|GU^spUaa){ztI1WD_`RGf5pbfGXDSk zbp*fv3+;c3j>YePdim-lWB<{S0{s3XWPcBH{Qjdk^X4%2e_0;H?>}0tyqf&~#~l3r zqwSX48T;>hSL62|A^s^$_|g8eRluK2y#Fk;PtU>p{{`*;-5ZYI|AqelrIACw|0~ac ze*YKRe|LH}e*YKRe|MJK$Ycw|fB3Jq3Els1?ugLGzs>jQQ7-uZE~tMu*oxb`?NZ+>pli6-%GC(oe|UKvg#K5=82r1&;8$nD zkMSh{k8&~g<(AHr(|KP?& zfBzNLO@IFt{0M*lh3E4ci2nW`-T#QW()WLP^zr>))c@v_!}q_P92^+? zZ&pvGpMQ%8i(u?OdGZ~v|Iqz^TN7UY)jfL#{oneAWBEw`d9>5bzu8&i^&isz+UwB$ z4}SjxdjFv$L4W@_m5Ke)``4i{?7x_a{n7l3>KOL72m8N;_dm}+-x%l{Z2EGdsbu8C zXwyIYkK-Kh^DpTBKYJtn{7d~9{7;zhqxmPHG5E)i!GCZJ{_SJ%-yDO#n+ZRf{|y;~ zKXnX#^D+2+nee0YKYa}TiDU4$G2ut^Pi159-(|vYXKlyW{}I82ANhaqd;0j7T!D{& zX#TD73cCNM-~ZDciogFt^G}mq@bOPZLWXhwH*+(6{M)evAOFz(@5U+k`@fK&5XSxA z^?rQ+3*G;HufgB{5%<5<(EmA_^{kfIfAP5S)sxa!qOHH%I^UeFyV&0I{_p#bkss*) zf7^HB^`9Wz{}S^b>(}G;-&%P8s0#T{Sd?D>O=idIKeYd{_bp!k#=!oMBzXT=^8l}Z z3knO0??eA9KLxLU(f(V<0KEQ1{HK`kBmJAjgdhF?u6YdpO5i8||H`);_y0#!{|JA; z@4rR=f1Y27-+z0*?0)%U=>NN3!|UHSoo^WbzZDIp*T1cf`2A<-|8Ijj^z~nuG`{{r z?>|F$@bw@1|C2ove)9e={i-PwezgC0854f=|FaKg@ceA!**Li2{VSI>C<(wbm{~y}_ z>zRo^|7icU0Tcg6|37>*hX20<|HtkBRaIBj(1QEVcv<}UNBh6Cc=6{S#Xsjy`1qHY zntC}M>i_lC8JTy${~x4=}H=L;ml%7a#vo{xi_S`#;)$T`fbu{~GN- z8D?UC^!|N36Z@xUq%-2b2-rUh?*Ewm|3Mr0>HOay67T=$|Ch!4@cu8mSeCK>-j9j@ zYwKz={Qn~p|95b5V&uQ9G5F1x@T2$tc4P3f0ssH!|L-D9{2%pyevje*^-TPqoc}Pl z$QZ-_18Dr8`2VR5_&;v{J<5N%P5AtW`d?M~`16nYUvFdZ=O6L29l)P|`DOAeR>S_2 zIcD_d-)R+n{~P`PF{d4$|4{!o<{rKNKc0x!|K$ABn~uyQc>O;(I?TxbhSvD}kN$sg zU>!dHqxwfk7_WcKEzRNm(+-g{B7jg#m zzut_}_rJMNG5@at_y2W^H)>nL{F~}#BQ!%JVk9qOuv`FHgHr}Lrs{EzEGGO&|HO~M zKQab?ITL=Q|7Do)BmM6Q{5Kf?f5q#6a{l{eGk+dF|0Dgc5KW)|AMD5He{}!VVM?F> z3s&OwKZ^fRv+(gh^xRoS{C6CtkN?_@c>ROwA0a0EpTB-(^gn)_$Md88XFP}S{OJB) z!h-()@2LJi$cB%9DE=pQ;Nu_CKOv>~_=nEmB%FPi_j9!WR<(N>E;|7iXr=m!1y509Xq z|3LcJi;4Y_|LcMMHKG4)r_P~aplPe8c0r*dOKp=rQac2=*5O`%hl%EGxNW-tzS`c$P0(HII7#p{n*_0Y3jD z{qtxH|6e|a|MQLE|NEKnBmWm;!jJs_{uun9nDC?j-<=(U|Hc^nrcC%z|4*F>e_r8T z#{Gv36Mkg>8BF+*{eLpyNA*wh82qXtc>AOH=fH}Oe@Op@zM+qQ#rN^?kI;XmaR2i* zsjR#S?mu2vK5DIg7FPS@#kqzn-32YLTYk1CwhzB~(iU0%{QXGp6S)6I`_G|zQ*dM+B9|ilH!2J6ZSMwcqwtv6>*+=bv z_3@Q_ho65(@4xvT;^*HB^YiZ%!TW#jOZfSBwEsx#INJY{`hjl$&;7ml`46=J|A!vF z{zvmKXJzQ$Kco4dFn;>^A2k2%#>D^8{LdyP{*U?}qD=fB?SJ?lhClyk|HCv_{P{=r zm(s_de`Npla`g5`@81%;@%BgiKTkYD@BfFd(f$9pGm(D&tK|rO{s--UQn04CzvDW* z{n7r1@xu7|Cu0BiPI&*hhQ(peF4%v(!*#CvT#x-;|Lng4e)9jvGlS{=e_U}7?f;j( zNw@$1c0T_7GunS05JP|d9S-2gAdLC3>paR2Mac>ki=P9OiR!Tu@m{w+Lv;KucvF*y}6eg)SGW2pE4 z56cC>|H=IqX|(ZQf{Fdn{LdFA_9yoLOT+umX0ZPfi2stSwkT>UEur53ZqUdBe!Be+ zDE=Lrji3KP@89-*4~w`6@88}Bal~DS|9k(z^-Pri@6@3EFWVpB_Mf8tFFhyl@1KzU zXYRt!KN9a>i2wg7Y2x!g+J9!f2><>G?f=u_MDL#rcH`bZA^Tr$!`q*H|9&_DA~f#u)Z*8N>c%|4;jijA8%T|JZ*N z8~>60?Z&YGCMNbr`mYh}F9`3ycX5f#q0@g~H{fAs#5pNaj^`?udr?2pd>B5nHlXO#cd z7vS?h(*Hhe`1p_Xe^dwl{3HGES4toMzbB*kkDvcU=l|h(`tvV)2!H-j{@-swpZ{4D zQT~6sb(^L4c46!NJG8C%H4Jtg+@lTo57}Ra5AA&Ie~#~ep!)A#4toEAfB%d4=kLb*KZ<{BT6q6Q{f}Le zc>mvKwvF-rU6&jG{vFjnc|G*`KmIZP{1@dHG4g*vI{y44{g-(XfBq4_@fiGvfL|8Q zzv(La_b-b?@bMq%|4FR)_>bOyym^C<|7ibv`F(u+NBy6iBz*mg_WwB?#n-=R|E=E+ zeEp03|Bfo&|55$(oC!bL|NaR0$^W0KxZ(D{BK^M-{GV?B#k`mJ`G2(kZbtz={-OQn z8nO8NhxUJ6KZwtNDE@snNAYhmcK<7ie<$RT{x!z-zmWf%G4X%Y|L|er|0w@0VB-I% z|Ha9~|Iz-3k4*euVD>CV{nNZWZePM5ojBe;QKp z{*T`ORUX6pKidB)Y>W4QwEuaQI^O@0{&N?@pMUiJM{+#!{|&VM-#kk1|81}E{*V6u zc&w1#|F6Z<`@j2Py#EVH$Vx4O`hT0HoPrQh|F2Y*TA+M;O-1M75&ft2ieGrr;f#Ji92!u{(seBnUs2q+5fL} zGw|`hrm3~L5%!;WoNn*vhyKsV`;R{cJ{tFZ>-hLtbg+9!WccJAu94A?A}nfb^Vua> z_e(6{7MLW#JALXD#{Zx5J@Ng|6>`fN{f|(iRf>yX|G$BX`W6fI-y1er7;W^|(bqe< zS%fdcz)1b?{huiRm(IiIe-!^D!Tx0b1E2p<|CgfxNG?HkMiGUjytJ;*x!MP{ZaoX1ne&W{h!sdE#^)TkejoaWtPZNF&1&u z|BKj4U;hs?;YaU3UoqiF`+r4%pDzBP{YNY>Q2!Ud{~Os~go*u;{x1jn^FaM4$)!D! zpGTP6N^a3K_8D?BQT_8lhra#^lg8ISX#ZC(`2TLW|Cx1w=>K{lAZ1V8_X-ajcP;NL%?_kVNz@bCZ7`;Tu{ z^zT2U)$s2>)M5Tv6W%{r3FtcbdE!I8J)dhT3`3%M8w z@1KzWb2H&b?_X`0@T2#ChD`X${fFEWtC;YMh>3_x!u{7fCj99AQw$S+^!_)8iT#oP z?_pwp~&V}*{8E@lNb0udjE4+9=(52cHZsq_x|&P|JeUp5B>a8 z&|~KFPsIE8q4cuyhs69-)w61cXGsHguU<5aG@WmL*z);h=)J}_gPr%f`nvmi`{4ff z$%W4YBk=yC=X>W5_0f>l9IkO}3&(44Oyd-r?7evDLuQ_|%YH8)!|J;9$Ux?R#sQaxos;qU56ypY z_tEP=xuyaNB#dVO!!g%vv~~u4NUln z{oipg|KJ4t2~htOq?KNky_S%P>c8Ff^!f+!uNZ^BZ4CZLO!!g%Cx;0?(m&^!@RR@l z_VEq)jJH3U|H^oVw?Df7tIwvl|L-uo{gM7(=!&;Ly8q$T$NN9B|9&~V|D*kdBgoj1@II7kJzA&Q=Gwm|Ga;z>!!c|K>i;N{ME4kHLvLlcK!wRf3sug`(L{c z;QL>w|Ep$3U;iqt!pA>!|2bneKK`Nlck*}o__wMF-~UGYZw&MC{XfL7%Y+}zf2AHdF;>OY5j^!4A8MEd%F<`I1TkLtgl*7Wt?y>;~U z-z8yu{fp**PO;F>Kce}sxcBt)zu6V|{ujFcs<=Ym|N3|W-~S@_|MSme-G%S}Daa`> z`d{Lj`1uE8ePhP|&%76*{y%>H74`o!`|GqVQ2&Fi4d4Gk|NoRMp+End67c6A-G3ST z;m<$Xe^@vMe*hDH)c@*b!jJktSxop*|J#TOKZ<_`fxj5$KP%Gk|35|NpKFZsFEhsZ zFCF9jH!?Z@X#Tkn&OdJc$=%hRG5-|KiJyN!@z0A%{6qRr2jbuV=l^%J$2kAv-0|lh z>3^{;c>RO)pOyk%{~-PE%7@oKDE>tb;Nu^f|GQI*kAF!2yt#poe@Op6ionM|r2m(U z!9Q~hes3oHNdGNh!jI~o$7Aq+X2Os3Z#3}J=|6P;8FU`6|Iq(m_a4IQKXm`0W`Wm# zX#QoTB7OXyHy0oOQT_Yn2YvjPXh!vKPyw#~Mfd;4$@ut(?mrIsCijV(j{%aQ#el-6l$Allv|Gor%a{g0z8t(oF>7QP0 zy#7J)&rb>;|2$mD`Tx|3`1ptBzr1_#`VaO0w>-h?KlJ`f`f5}2bGZNg(mvX;=51to zaohXerynyv_4L0Q3aG0bdjI{^5A^=;w-^5XFM9vC&=~*z6TN@bT~7c0&x4=-{a?fo zzW;^xKNQsC`(Nn&YkO{4c`5Y2?!Fp&`uy47|Nnc_f%^XsFW~!ssQ<6*gYSQ%{ZG@k z;rst+|GoPfeE%Eme~_Gy?|-BHXZL>L`~Rr_JNz2o|404bup)f_AN7AvGT}%4UmGU; zsQ+)sgdg?)tC;Yk{{K7R_hIzE9lVnEb6ZGxcr%@UL2^(`23IRpUFA&`F~kBeg1##hSxu+ z|5t86uYa=S@%jhl|CllOkA9`+Ppd=oPvO}4Cp7=}u#kTK8|nYfWBB}s^goX+KL4Tl zZ$5Q={zLlTTMW&A)4%`PI0+yB&xM6C_Mazwr1$^tRe1kL^Dm(p^!}fA8t?z;{iBK# z-v813ifTlYagM>Hp`Jc>TW)_J6F0`~Qtf8?`lw{b%}$ zTOtfi|C#@?n@<1!f$aYT2KxUDI^_rRf2SkckF1UQ;avJPEP07fF|3BXO=l@UF zDhi9A!2RFNx?c|;Kl=OsmoJ-}UcF-c|K)Qie*eqJ=m=x~-5eLZ{zLn(CvC>-KeYeX zWeHyY5&Q3U!24f|DR}*N@bE$3qp<(?X+K{7q5U_XYf`EIzu@=ZBK;GYhu1&o{`Yh= zUjL!{U%UNy{fG3=c2m6mL;7d=O1%C<|3CUT%gOl==pS}ly?qA{m>yC-ywfK$;7f8* zKxAMh-2bEf&$HU-_g^h2q2GU_nSkGa)BW+|r!Sws4CwpO@Bb>WqTl~@L_4?-!jJZU@c=*F{!g_3N%#c){-=>$^!u;T{{ir`XB9oyWUK{{}}B*s!hh*AMHPW>rcP`XzGj^jQ`(o?ZNNA zLjV5}(Wc*jh4%kx@Zrxtnt!=6KtKP2=0DqK>xpL1%BNAZ&d%5)S~z&b}0w* z|0^>7J$Z5&_*-EA-=l`XhM1<3mjjL0f4^+|(ox#k+cnf%1^@r;`!xJ{VBqW5-`~jo z-|ydUUMy^!F#k7W;-ZPVlcrBzJAH?O%=8H}*Up|fmsN0;pfJpTNQ;S!OGqq`l32VL z=3f*Pbk&xvSPJtm!fOTAvaB;z-J`PH-c8e7dylT#CcQ15TZ~}-d9#^?g{7ql>_4?( z{Qsjz5Y7KVzW<|f8Q=dx`hRmEz5c&#kJta`{^NoMdjGDs zQT@Bjn7;nqx}3iLRpZCk|LFaH`VhYUMg9MC^?3b@=Kp`@;`J|j|9LqQuYb||CxdnZ*>1L#Q`7x(ft34jr9GWZ4&hNAGTch`ww*g z8`4cb|BvF|`Fr&7Z!nSm{WH4%lRJPv|0w>=F{6)vY^&(wpY&{e{6qDB-FJNa3yq08 ze*xzIel^ke|IqwXYBt{fX#OcQ41fQJ-aiJp(%=6j>C^jvxg6gAk^cEP74QFO{$uKA zy#J&5kLAzs{*UHA_AudZYk$*0?0@Qp{m&mj|Mf%t_jBIRxA#*=e*A>}@0{@eUv|)c zM%>&Jmv&C()tferZ}TYsOpQ5n<|=)iFQ_6SvVgN{q4-8gNvWk}GO`Bp@(R+gmani_ zy;@PUZp~UzRaLdE8RYv19q4~p!2Giz=s!*P|LM+K+qSzx{V#Od&i)|Gzv~>^>udw{ zuW9@~FGraF6FukSdk*UV7N_HZ7oh%qVH}hq$s^_&Y;Qwch@c)k% zxc@@@Z4`ek@LvP|7SR7GGk*OR1OBZaCveEX|DOzje=Y2P_6B|l(Er}RKaKeR8So3h z{JRJ6F9H3(6yARl|36TI_kUi{{}BLwH+cUsJyma$P(I0T0sLEG|H~ra7XW@?;GYis zO2E$p{KH}7{NoYe4}kf{T;LxBet`qw5ocllQ#3XIx^iPuath4968)cRu>a~6)IZZL zb8qKS{AJMpc>wc|W^n&S@T-FU4_2;k&;fprWv^Qgfc@`Ide`L+{5Qvw_8)}#?;pTV z*gqTivw*(<__KgN6ZmU@e-`iy!2YKNz%L5#|J#9o82Cqke;DjP2>kEC{&8t!|BHD4 zLfAi#;@?W~=K{Y6@D~HWH}GEt{++;|3ifvd`}+a^G2kcO{{%z->k8OE0PKGd_=Caz z>%smv!Twh${%qhU>>rj_aObU2QE>_IKLq}A;ID-L--QAH1K^JZ{z~BQX?xQ#2>hMz zN&a8`r2Tt`N&8P9N7{cLCu#o)Q>O9h06*`nIdj*8{Z|T!Ebs>V%Pk`9FS$fs!4dcu ztX!?Q6Zp5PkoGsyB<-)a*BfIxF!u}gq;P-(2cZ&PL{%3uBLmf!}52EZ}6AJeC zj*L3tO3r^LQv4sUK>u_7zvn-n^8e!{_e)<$KP+z)Chh+m>`$Ekh{l)AFP6S;y-wJl z=Uo@#C;DFme+Jlp5co~N{?k89;F#4;^3MSOpAPT;_5=U)!TEyg!2Z&e3&pj-{-O_L zWVZmnXv^{y+ra)yU#wYcdgZ_V?~zBIf5blv_&qTE9Dng6|DQ$iPmKou_qcE|i#q?; zDF0uZm2)FyYwqnu;Q!YQi;4{heu#e-6_1~^{1N|JsQ70A{4Kz52>$<;z3-DQ`2Smh zp>G?2zY_STbd&L)m$H8#*k9l$|4cuMKcC{y0e&Umze@3^0KYWwzXJa^ytYBZG?6_2 zCRF?%F*Ua^yt!?ADP{l4UAy<}4A|>j=K|+HZlBj{ieKFCsQ+2VGCYf{w%;B4E)@{e-rrKfxi;$?@jQFKCQN`f%C6a->?_#9|!jD z=OXi8I~D&Gfd41(D^d0@1N$q1{hz}5XMa1Hw}IlX0e&8^e>dP1Ibl?vGerd3OAn=O0Q}W( z{)zZE4CntY@ZX2?PsG0`y*<4@A^yJ@0Q*D!d;MeN$IUptBTOt0fR#R0Ih5VR_!Iq^E|1`77{D<~`e%-Ts z&n)1#iXqQG@&9jP{$I)esQ)nVFFSSeRR16LNAEx81OEev|A?RX{~;0oy@7uv@Q(+6 zl|Rn^GwS^F5&RJUko`YW_E!RaBL6P~{y510PuW=6c0&FupU6GY5%OON@Owb~L;OVl zi^zYEfZr7CFVytk{GSi}HQ@iZfxqOB_=ocUjvYH}+Q9yMcEkHW;P-TMbFTsW@1pWQ zasNT&{|6^eo)QB7*Wuw2(wO-7IX(UAE=>H(ynnxx6T_eOlH^~?`Ct8a^vjol>EQoJ z|Ba{OzY-7kL>=&dvFX$J48i^~0(0i>0{@>WO6EU4Y100G^DhPccNh4TwY0Po{@^#K z;{QzGSAzUM1^Aaj{zLXh?>`nEJ?ii0{ICBZO_qJ0( z!JTN6qGEpFZFI6c==&rM{5SvLXQTMpCXxC_ zUc&F|5yS)p?}1IpV0q8k4gQz>N%-@ z%`JZQm)PG-DN&ScN|5Pgfe}ei)=i7Wi#83SHg4qAMWEsi7N}1$` ze_t~AwRK7TXJ|y~KTAuJ-_DlgcXlHA_j|gzON0J%Jxta=Cy$Z*$o^+3`=5@7_z(K` zb}H%rIT@t?U%f^0U(qM)A7cMKq5rL`fM5J+^}#xl-~AQnpFiSX4;BCVzLN2;8T8*E z#6Oh(CsOf$1!eywl>NEH$n*cV|4#t^tqp6|F1q?({qwi}TL%17F#1OT_&K9|e1Gow zul^f47alRoH!_OH{lEI}KJW`^{ICAYFT`&Ccf7|~PW&c?a|B3oX zjEeur{_<4(TdTBMQ4{o^&ienJe|_rw8^QTsbbH%&mmsqKae(@V(EmOjr2qe&|DuD* z^MC9tS^p#bkLsV|Yw1@Tf&a=)(*INL;_IK`7o`7xZXxYo_4dE#e}K~e^*>2|S$0;o zpOF6*A^#EeKZ^fTsQ5n*_!B_??V<9Y$5QhA`vN}^|G0pk&_Ap?r2d&}NXGx^z^?@H z5Ai!w{A+-}AM(GnH+lZ+0?70K4ER4l{Cf=igHZpL#>Jlh3FrT3%769GElU5ar1;l8 zAmg7_B^m#=Q2d4z|KI)}Hb|a-l>d?cI|9E8F4_YBqCfO6(tmS-e-Y?E zd*IjnBmblN&mH(RLI3K5{dZyNKOL|?=geQfZ-M>$KmA+(5&!=p>c1rL{}Z5p+Q9x- z!2Zbp6)6Az0{jW!|EGXIFLQ%N0Pqj~p??f0{SyfMTS5Q$LH=6=`9B8iZwT=}9QZdw z{l5qJ;~@X-1pA)_`yU4T2ZR0h0e=A4Klw8BKlCnLzLp8~?}q=4e+}jLOUD6!jmUrN z-}2U%&Hc+?x0ZCh=_r}}Z~jB|f9>y)AH%?p&Og%srZcDU<^5rQqC8gT_(7$V` z{ui$X>Ho7fk^awOVz5OB;ve^R(*G6gci6B4zmhYle-?VVx##_%e^LLd<_xKSQT~sm z;$Io??*jWj1^z3T_#X%Sf9l@`O8+DS|9jxS0Q^M$-|>lze?H&744ei1?*;sX{tx3I z^?xicssGXW7og66o;aERvVouI|6N-_p8wRf87)8dvWf#f(KNzw#qw{4a?i`5%Rm{9n>Y z{vV0|)&E6Q|A!0s3H`@P@z16Br-S{8{J)s8|3ZqtZ8q8e`9<|V4Y^4Dw~d92{~EJN z{i`yS%zpYqatziHr6 z|BAiG1NFiGhavxKf&JS+|5Un>`X_fh`{J)&?{}Rgo^C^C$|55%w zyv^J~G?&bOcF_OaP31o+=zk1e*yptn`X7UlKEA%dKMvx5P{_$s0(SrEA5{Nex|Ecx z1o4lP;z#}8oT7p|Q;bRdQx5vKlF~nCp#NjFroN#7`af21{{5li-b)p9KHMy}S1`2JUqp0{)VV|I5z;{D10S z;{Wf&{ts#3cLDyD*VC`=g!nIcBR|M9|ZhfUTQJNs7PPXT^({!#s_bo{9Q643w4nDARu{7w}AJ|_IEz`q~r zzqw5K7XrU2@UI1al|T5A{(A}hdC>nT1^$FT;yYtzOr2gsr{$Kq+k*a^^Q~8gB%75di{I`nAfAUoRlUhaU zA5l4y-*O|F|BTm>`EQdM$-kfCNA-^r75@z=ehZ2}h06aKRQ}JU_>uinDf|0T{4o@N zIK|&f@xQ%C>K}Cf+eF=eJO%y)jQ*|ojMqORz`yy||LPxszw*BlmH!V>{2mm)6UA@G zgntRezl!3&Pw_vY_+R|RkLur#6#o##|C{3fyZ(=E`mg>&@$Wd;pNRh#!T!;I^nX$R z^Dx+dKh%Fn|IVfKZxYy_{n0}4tH955Pe%48*ni0%@lTEFf7}Cp$3(qNFM;19-`J!U z_)Y(){~am&o9uUUp9T7Vv&+FlQ=tA!J8{(iJ>>r+zmuoh{^i4I05xvb6ZcZ-yivpnExg2 zzh8p>+XeSOwZLx${FPvT;{Kxy{NDxYzhA(w3H9GM;8%hArv~^<;r_1){C_jlf9U*c zQ~Lil#h(TI-r3u>2f_Kbg7}Z>Uv&Q&0ruy_+<)!>`?q2G|C@oI=zpO3hr5*iT?_ty z1?s<&lE+WpLjG$4{-3Mr8=eDy;~)L+UaJ51W0z&QY;IwN(LZHi|8alh|6|ns#}cqV z(f^Q$4v)Cv6B#8E8hbur--U~FuaNzZ=?P@~NBpS&f4G9Ie^C5G_dm0MpSb@={SVat z6a{{w{*wj%63~Bt*T2=XnB4yeNlNMbq5oG==RaAKtbhO3zlFg6Xa5K3zbN3Jj?sVC z6hEqek^a@D_|g3r(tk!-Wc`QyANBv%g8gy%Z#MXUFx>y4{P&acf60lY{THy2`d?`- zIsdYZZ_Zo~=>HM*|2Ini21EWM`hVfeB3BUnS}^}Y=pQZO{S)}VIp|+P|9ipw4{`tf z3+7*l`#(WS|4yau|7Sq|htNM04s$Tx|BN2{8A#m!^T7NEasN5}Tz3?4{@=xST@*@u zm3#^2AH>sQuV%pfixl*~Zo&QEEAan9nE$Z{{Z|J1&kFQ^70iFMLH%C`_y0#B{=b6x zA2HB>?_mCkw|fO+{(TG7KR@CAUwT+}gnx9|Z<%r2tdrTfCJ^=S8)E(e`aea`{~_*w zvSI#(;1?HN$k>0m7z6;Jf0r#?zJjQKla-^_u7mq;V*jfN-2dtUf3^M~WB%O$^uHzW zJHh=gvH#N^?th(te<##Gp1^Mf_0M7A|1WU=OYFbS3+g>}8u$z0{{I}@|8f9-Jm}xa zkpEJFUk>hn2!4Kwe=5cANb$Q+{H_$g9mT&L_&b4rA)J3={$CjQzXHD`*ngCWe~|yj z!~EBCxc{C6{GWh-2Jlw`|2*Iy27aRc`3(AJG1$MC;D`CAR^V3#`>Rv@n}A;z_^*Ne ziTQtBiocHHuc7!KQT#{2{sjLB@DuYt6<~jY{|3ch4D%1f{NL#W|0IGx0Pg<@{%E-W zC+r^#_y2_b_rd)?VSit^|0nFPL-DIq{5}-F7|cHq{A=L;pWxp@@mItBKf&Kf@h4OK zi4=b<#UDZOr&IhNVE%#NKS%MWQ}!Rx|GmWo_zO+5%x`Zc{r~h%vi@@h`xE`o6`r09 z|6hC9o8kY*jvZsnKb<*!8us6XM?{@F7e&-R1b=d3Jj4HC|54g?;6Iannlb1G9EAU~ z@{;~Ph4TN|l>gg<{R#c+Ozl6txa`Gp;{2~#yJjt+|JP}(SJ#5`&#SAs$pGx{V`RWM z|KGv??cw~3+gjU8Ik4^7;iTqF+<&+c^FIex9h!02#W&-~5r+NGg8k2c{r$swA{xN| z3H~z|UtR2={67c$pWx@pn!&LDN$`K-|F>W69bwr28}L5``$s%$uWbPS!%c3@6M+9M z*neH8UYC3~C+xrP`!w_OA_l+UccCBifBs^;f8-dymxH+f+B9hrFCW-{D)3v(Hk-SB zo~57-*nc7L?~x#$zsSg1Q}#Cp{xx8Kk##I;MEx(jk&AKupXkRM5cYpyW?>cv_9yD! zhqhw&dqDrRIypNPg8vi$|ExZ6>!1(#|EeRpM~U+12P`2j@TkV+Q>HItlothlu!}Hd^?bjRh)T@PBQtjqv_Smv_rlea8PE^njoE z|KmPk4+g)F#2G1B;J1^DQdqWvoPYFHUam~!e-3p$bxE*4asF3;{f&XY)Xdj>8`$5` zMq>vN|7>9XgYbU^*LCh1RQz9i__~ij0shO!2tsA-Nf_~hVGe*J2ME!q!tI9Sa z{~d($PxQZh9rYRK-^Fu+7cu`}?d`)j|HlLU15bhfe+m{m;|BH*1^!dfsbK%Ui$wi% zJ0<4wwX~O4Yp-2rynn2?8N}c(xvR?f|9iuO@-o8yg;hCE3Hv*~@NHm){ujY-+NKBl zU*2^Q`p4+Q(mvw;EAi{sZz3@NMDVMzD6>sq@P~o@C-e493E`VDlOOiK&zbv9AVW}i z0qOtG7sX4-fc+mYImOujcYmdy;#%M@P|;hbuCYOb(7(AlD!TeG|3moyTr&j=XDa^_ z_9ytAoSYf^ue~7t9fI>u_`m;=X#YU)|5HJsr>epJWc*8udi_uQYp3Gh(A7*h|2MMx zZhQm#6Z)?J{GaIm+%GFDC*~g?SM8~ONX7pKD*k_^^8aXeZ}(d`|L;F%!vnZm-@?8( zK>X|d%?0sKV7xj9FZjRhqy=Dq4#xhUIlw;w&Oed=CsXln!eS>`BLB~Z{72;fsmjW0 zVvBx(`HOJ=I}#JY|Ka`z{Qpx%M&>~F=NnnT|06&D&hNtFLV`cA ztdr6IJX7@%;$Ije{-1B^XPp1!_KJ?nov94}&xG^;672sO{Qu7P{2%pj{?&k=@PFd` zZvy*Ir~IFYf3v{>&Q{av=22Qs5`_&l>Q5!u}7BJYdBC5{kbm?0q<)e<~>bGXVZi z=)ci4-m6!@{+-z~Z```=m`j}h?!ui##QZ~8ndrl5RQ^9s+25bC|6MBop8)^=1on^m zd~x75@Du&dvsC_b0)Art^(4f9LjRqI_)pmX=)9Ww#Qbvr>_0b!{71yUaJd!oDE>t& zpJU{|c6Bbs{`+p-`HcPlpN%G%j6(c32m6;<%iG+uwYT-%<-MDTe|4@R?w(%zyomdc zro%G6r~UnpL|Sy{|>eAr`5#$=YhJqhL^za_DbnB(f>7s{7;;JBK{HQU+=5l zw@^y|lz{z-^Z$sedg5f_{7;=RjUV{m%=U)=pUxM2DLgDf=)YRvC(eJRoUr@~@c##^ zBvyOE`6u#!o_gE{LjPxj{fYS>eM1A2Md1HL{I|8Tv9<>LJMXIAqvRC4Hy!ey3)tUx zf9s(O-YGsr|NCSBIsbhznAAUkXFVbbenS5i!2Bm+|J*Cz8TmgvD=LT3f5iMZ!T+&@ z%>Sd5{o_IZ5c>ZN#J?tpf4AFKzFG5D>76|2A0q#U_qTr`^xsK{e}w*V8|ThClbzH* z2Y4p)68FE>)3?tg?*AMF1m}qg8!+^*A`!e zYmPF`KWn2eChW?QRej@*g3=5461OLl$jjXTPTP6_xU(drkiSYl& z(@FpTCGY^=KZ`7QDEeR_QU6y+%P`{KGphb+Qo6kM1K3{!__H-PYHy`+jrpn7sCQD;`|f(zY^wOiTqzz%lVwJe+|rk68iUj z$14VZTW@bSVgDZk*$n;P4fCHw{WF(}f1+Gm6Z^paWc-@}`ES~6BLCY!{?`TnC+6Rr ziTDrmuY~=bDE;?-T{nZ@0{E}%=rQ!~7889VBLA~lZnY%zA5?#~ME~oBqZ8=gBzXV4 z&&%b|#zQ_X9=;KY*Yon>S%2=MtcL4Jb~hJ62ZKKj+m z#I&nH*D`|#vT`Fkzkcud(wl!L;%;$dWXb)Wp762wG}Z^Jk;}^>tqd zU;g^sKk%ihvij+xiYJfD9+W?9ZF%{+tfHjs-ow)BdrwO%o<4o}WI4+$&~wXHtb%!7 znC4wACJYe|7M@8+ERvF$H%(^BR9+QrZ3DdxTQ^B9nxV5{z1&L9>BifPwY0WxVo_ek zZY3@yD=Z~0x!6NX%SBhq{h+hM76S)w2OAe2i*44P+wCnku2{0obnC7i#wsgU8R=`A zE)?J7>8UNPcF>qPc{L;I{N==)xR`{Lpl?Z+ zV=tT;y>jhrXgI9NDD3~-|GD$c`<~&!pWpk6E+07^5*!hGEF|{i#lX`S!{X1KJAWiB z@@PQggY!|L;X!_vGLmm5Cg!JHzFc(uW`1^YQc+UwrNqo{U3s~;Z=@&Y>!=uQP%&54 zv(Qqv+_>Fjvx=Ifn!bjr-mD20CYud5s@OT%cvxE>aJF{bzuRZ8-9Be)EAO2yJ8i62 ztvcwobIl4C-njxi+%xzz*G(KhW7_<2tm6&LXUyg07`e3_NXOT-s1kX-EG zEV))je4)#Jg@r=Ort@a=&OPL;Bqdex{N9s$6{U?YE359eHnr4;9;tm;^6Jrp=4Y+- zP4}Nxb#^5cbe2D=YpJLxf7JQ1xum1v{=J&A(#YE8s#k?^zmjrtvr{kS94{TfpPF}d;7jWuGKu&yo?9K4V6Q?f*J&(E= zWX&>D)o!1gr^9X^O&c4l?Y=%f%03=e`(~}SINfMF>^YP++<#!HPiSzKMj@i_P&49JKFK~P4e5mcm3f3b>BKW zdWOG$dePnUsk6HNaZSsKsz1pW-i!A4pl9Xg!sJv2shq&x2IendJ%NL4>ty(QJQ9)jQHM_{9N!+5W6K2ih zovq5jJA10DlRWU45?VR2sW`Gmwv zXK!W|-?}nzvnbNqvrzsLXx9nE?kR?j*9oojfshji#c;HJoMRaD~}!Q+!}7) z*5>L?R(rhH9CEkyoWXAFV?EQ= zOAl*mnM{$9krY?jwOC@Ul(rg=qT)(cIYqgZ3J2s@O0V9kCn!0cOKu#`xQVQioUH zWpvg&um174tof1ugQpK`atgne+;8ozYOZdIc=0+n;eP9}tenh@%SB0<*9tBb7e`&a zopmWWJNM?-+`Ctb+I}QQhMx8hih~6k(E&$}EYny>f~$h zZf&=7;-pQJxeYe2GdGzr&&W(`i>ca1eM4JkkwtcP8`X5R*w^V=Y?58=?qy}Q`_Mke zgKmmTeO-4?9?v#)p1S6E4gq%FNrE${Oyiuu&tarCgPVU6=WJdU0aBDtYtmL+69HZJ#=apSHLC82(oHYp`u7I;3&r z)8J@JWkq>a&AUexr6tudN6V|5o@J+hu6x+};zxZ$J=p(SV&X*!d6 zH$Nxr?wz9IoKqRM((+0B|GE@#F62~tbjXPle&=ICFZiVd#vBQYN9TXWI@^8DJ9VH$ zv$ff<*T>UmpPi!jzP_rCQlUR<>urR zkQA6M$UldLjDI$31$nyPJbu^N-to1n^V7TTj**9N2iv~1jl9kOGW=mA?pVvO+KPL< z_a5B8|FZ4%)7O!eVpIoH$(=y4TbGkc*U~ zozE_AK|ROAcJ5|&_B%K4w{dl{S+>u6myghRD^(L6b?2>?=6hAmG%PlHY~5^XuD;E9 z+pcZ8nie`sofnD7i*DG$y=aw)?D*A+LM#fx%T|h!{;wslhL4j)YRXjp6>}zVO%|Nb zGH=^lzVS0WZI;dE=9{&Yi<|fP+kv6>p)YA~-v8_gK400_G5oo?t^HluXiv}Jm%bm_ zk4s+MPsw@vu({+>S6S(k?$*-Q=Oy=Q%gFejKAM9vK zx}B0;5Sf#i7FlrQSYTL2nE$z;_+v5m1JA^SeeTRYcl_enThV8s!4zAt*K?28VUk~f z!N1jgkCB`E_5)r%UI(0)c&;;1TcBXCVYp6fr`9^n{d!y1o7fm_T|e8vRL5Ltk@q%* zMZ$byax%OU;xghAYpulO#1*z^%v=H2!`w?18`?p@#er|s>(YKFeNZ6D~&9r*EaH2&19d(WSg43#uI zuX_2K;3wnXwZZx)HBH@3PoLIY&$)am^=i`9j5}`=Gwx(omq*-4D9jDW%g89ccq{YH z-LoN~RUuGcz5jmY`shjJh349_x?3q>uIPN(-d%ff#J}%>6=Vy_a z%f%@(mHW$^E|UMmqf-W5Qcr$48{K ze@wV~#+Brov*KxkLMvTm&1E(;re2?%d}bZ#&NRG zm_CzZ;sh?;tvrH)0{k;s1ZVM2m^^Ljtf|~ft(}FWq!x)tDKAr8BeHzCi?19^4l68` zUMRC#X0@V}->G{rx!v^O!NaDf!TwbxuUo4cUX|CrsH~}c9T)zprtj0}`}Ve>;qK0k z(e8}G55IrE8yacv9{3FL;nQHx(3vy$Uk08$5pg>3!r2o6@fR;1Js%h!8+{_yFXY0R zS5M9y9O-}ptBd4%1AuBKW@~z@qKi?K+=^1J0ZZdWD^0fMX!^6h=Pac$@POmTPlla29uOJifBItl$$)1qp~pkR0|J9i zoIQCqHa4gIT|#bJLf4!0yvyl1*K$*CCJyxEMjQ(cjSIgRcQRJZ!DjdFE%tk?o%VqL zljnb~vAeCKi?^Gv_wIvUThz9Q%Gqw(XtBp^n~sT-x{0Cj22%e>`x2ilp}YH1NAKvz)8Umrhrf-s)qnpE4f;=Czw|%9S6%wxUIVFr&-C4YSl9Zn zzqz^bd2?0mi|75V&k`?P&OVxwdgJn)8=aS~-NjITvEX&L6!Q6?OK)#h|Fry<6@-P0$9uomVJqVU z2OPb3ILmwaI;<03W2LF1wsGrjb#+rsV+;3fTa7FY)YL7tr|Vm4Z(pm=DkCW@Md^PV zQJIBH3{+>XS}VVNG4~ARC5qB|6WL~rw{;U`AICd>mNM_WsRB#*XUrw@|87$M*LHsE z>iO~VSI?(+37wz21|m+>Lu>w9-;=&CJ^iImpT28(SlZB1QdUOlpThpemyORqR@F9C zllt#rU|QPEj7RrQBqt|?+{{Y4oPD|eDZzg+CoB8d(a4jhPUWTr1)PZ{^Iu=q`Sa2L z^6T5U+U{Dv!@*wE+Ubxp?*xOxhYy-Ud)~*_L1&Avo4NtdM)fTk;&Q;RYPD&dzOl)= zordd;jSg)$Hz4Dm5r^a=iSdg>C8ZWA3Cb-{SR;N&X}Qcwb1gynC2~vHr^rp5GL4nA z{~>9J4-2Mph|ZccNpRI{fhqbuZG&$D!ph#i>w5XNqrGzI{;#gC&v$+f^$o|KY5fA* zI-WK(-23|eVOeFx+pjmz0v4 zB(iwbL~$`iDFIQ)e{%A&r2g@lw`4Le$G9mxTwE(BjOSR%&ITG_!g3ydJ`SEKGiDgf z6y$0@a`E5#ueG=1Yk&JtN!RcG-#_2q8v6PES3+Ss#K+PHg#CY2G`G~fd3dkxUTw|2 zsG66LTZ&@+uV1^Jcr~XdI3fQ|a?#~G`9*iT2XEx3-Wn__xN^7icZlDa<4v7IDS?3( zLXO6ire6q;kBUDZ_HC%(+_@9cp{Ew>+B?~}?&aXqvbEj6%j%G_x|Q2OI~TXzOSgOa zx*go7w_z5WsD!QN2Fp$ArsED8s~E~FdzqW6S(@na?^>^Ce$ZNV8`xiAvG^Jei3Jjh zD`zP!5EdT4KvGF@x!a1h5-ir%1YN=b$`@tbFlLWzr!!szRn*KgiUKO2yK^yHC~M`O$VPR0iJeYtTV zJ~}?MI3hkOB>Y4GBmcYFx;l80=U-6A7aG&9<~x0ST|7wryOON`_iJubb5>Q`4)vdg zg~lvPvrQI?i+%JrODz)Tlai1Xm9rO;TrI7q!nZ~lIv(T2l~;SnDNC*0?k+NkhnI(K z=`0>DiAnP(lKRJH*(?|{pD#OUuE4{#_HVu4ev|q)x~dJv(uc}B2L>w|ehhu@BlXW+ zvi=!vsi^w>thDy^P)$Px_UnIT53^2VH&Cb%g^WD%DRPjYVpJ&M|5t9GkBkYaIcM!X09GG_)-^RJ6=>WLCJVnV9HXsBSYe+PFv0K+{-MbcvIU=#qt+nv=CP zxOnHzm^_Dn##FxXriSz9aL!`k6<)S{;c}^E_RfoC7cX8ZqUhqYe7XG61&Y!OS1BmV zE?)iO$hmUp;6H!xs;;86^yTB%WmPpTFCIL2RTX=tv9a;>*N;O#yL#RaeC+x)I6U~P zCo64e=*wWoj}PyDjl3Uj?+pqLj5~Vr>`5q}ADlRGBIH>7@!+`7sEGJeLCrNW=VKCY zUcY@OBkkth?kAaPS2GLlre+k~g74e6Zx$7&-Okc8u-vlF#LP%z>oz@}DOy^_n>Dtp zXjvGmY|t~*+-jg=v)9qd+RkmSz5M~Hr4D->TzC4|@3Gyv-*Mjo50@Q>y;%6BPMD&u z%{Ga5;{0*5rwX#p=jJxtFm2lWS-dkjStkiB5|v%3Nc0CCTxBJsWLGPREf!j=xNOm~ zB}+vW#aGC-mNqxst3Q6Bva<4V(>>^Jl-7d$sVysSdC^$g)bOk;F{P`$yJO&E+Ko@0 zAG+Irzj-(E?Opf4Xbi-?SU0A12E!7&#r@0~mzb@oW$sUtCQ@lh`uqR*a= zOT2OEZfbVUt@l0GFJJlCd$lO^V&vejoV;6kH*O{0O}}cuGC{-E)rLb*caN`wrSoAM z50gD!E_ODq`zZgHwNu@!;=W~_y4m;*I;MI{l=m5%m>U>q&D?CRwRMdoyQr|VkkC@r zC6cm=D@BEceH0bMmKkaBDX&$OSt2n{fs37IrZ_hz*Cg)wtZZy5xAyNbg{?55(NkIXZ~W`3c+&d$Rdd<1 zclXPhs#;sh$o$vlmymF&Ftvz?|LHe~{$JpYq^oy>GOyoFF9?jT3_2DO*!3#|rc0ua zp3ggW-2ZmW#qh|}FT&zaUHmPiZf|SrvfEwdu+O1Q*7m!7Z1v$~n!AUii>D`<|CQ|5 zsc9Ijx7@Ma)Y4mP-Fg!h6?YRu9b-+^X(rn@3a_)~5MCfIFRIMCcBQg_q_F4;At4Th z1(HJ3R?DxFUp;LTC+jqJyZzEUJlwNc=7_V8XBD10VZtnaZb9XF^SP&d8NSxtInWh& zw(8TzkHc?%GEDUDEWf`N6&R`iG^pWk2d) z)HS_+T%VcwAq~_|dEe>GjEuabJATQ9S2NBgCuiNgnR%7U+P zFkRyp>z^KXAv`2H{*(&Wc)k(xM!%p>vvoK zm(~v-dm0A@dPW-Sf*+ODmS4{LSY1`sc)z)>Zlt`V>|Ooi#)gXUhSo=k_nHF}(~=X? z3X@Z>UWoViznPd&SdjNUC+}K8OLJ^)U_e;Z+rcEp`6v!Kelq@S3-myvzO@xb#KnZ1 zyk_HIzt>vLe&_%3^_5XsH|ySXN=kPK(w)*N-64v!Al)b}DGkycqI4tODbig664E7& zQt#aG?0tL>d!KcGaVsRAp`qqKU%+}Q&3qjl6z{%ZH!`s%=+Qm*u@~;08 zvX;_Q5|cO5lv0<_ef~_%LRVHvMOK&&9uCnM841;l$s7jOg@W3dn#zHJnTb_fiiDiZ z&hv?YC=)A-B=|R$EETmD6s#sJj2bdJhKrSupoq3EA(0y3CrYj^Z?5y&+RK*~zg(`A zZ^Q%*kB!b()!&?#jg9VxeV_H;-An80jGF!tK0eV~UURy({(H5y@@8}IaA(mcHa@ho z$1g59FfleFEF>zxFMVt#%0D0>G$1i1CMr3*YWa6o&6m9T>Y{?rS%rnAAG2Btn*csu zP*Gn~dwnLQuA-pzTv-MQURg&^TvtU`@8vTwDLjIg@-iAq;!29PuI@(m_BM|NtlqjB zd)s?DJG&d&yBNK8a&fj4WOKL1M#m(z9zMk2^vBB=a_7Cxs zt>YsJ33Kgz;QMY}+1okW-9B2*tJ^=i+}OR?Ke<>sI9p=UsY& zPfBEBOjvS?@5p?7SU^TVT1Y@>T5w2mMd|WdR#Qb){^kQEG4ffVW*}f`TP$5WOdTjc&00* zZ=w4_MqS-Tn}dR#7V!}W?IT*|JN!=?Ux-4GiiJdwUYLbTgcF$<5lapO5ewN|h6RQY zkr75ckb}7>XmPE ztqY^)Jsm&hhPx+|e@wN_)D>Khew*s6i>)rmtG>fOCxWu_{k}A2Hsog1MMaf`^Gn z4y7rH1@L_wbU0`X4i*kt9(GnL0dqGXAA3SUMaN7{`-GN*ftlLfMSz#HYvJq2WbgQU zpP`Pg-$q*p=f|VpkIv1Gew~^a>i9m=ySI9^c6zpXlJ|M%=k?z9_WsJ%+TyS3+Uw!T zh2LYnlV57@>R(r6c5Z%JY15~>{--m&thza@v8b>zBH_dPcRk_38KK+fg+6I%QGxG2 zd}xkN`ZxXyEh(mo0H-Xehe{}=ET-{NTR~G)Tp#qeYWlizTDG=SoL0AgA7@u{b2Cq8 z2U7zs94{A3dmA?|dv^om2iO?UNU+EN$^-WsvYn!hyjeq~&Ngo3_x=|E;T_ zqc}Zbq?F`Huo0&BEk}ULZcHyeL|Bmf|7?i zlLFuShs30$#b(3>`KBdgRhH+Jt!|b!Hr0P=Y{{z0FR5?L%{sj(D=sO_E^R2$lvLG* zLsC-L)zQ?E)m7Ei)R%rp_)=U#Mp{NsSy5Hh(S(Z0%HHA48#h~bYj0b3Z(~EwN1jH; zuT5>-Y~0)}$&t}8F{KnSnCVzpgzx6xmn3YwoI;F*PuSQ6g<(;Q?3thtVbO?L5s{%# zq2criuwigqtl3D2$q6w89i6h{qrCG$8YY}+0oVZ{N~Zd-ap^}Ui-I! zp_$d0zWI@^`L?_7Kl^BAZg#dJA-l9Z^Icy3r^1%nyvC*avQHJ`9qAQyO)dE$jTHp} z<$wTt7yHdOC^ckfDJSGZT!PR0JN)zLOJd;N`G;0|?e2*}CTZm9u4m`%B5n8Tt&^>{ z`Q81`XQ`s9Wh(bvPe)Pd`7QtBqN1&H$Nw?E;o|0iVF&o{U+dQ;0Sq!RH>WTMy&X3+ zKCH1PGxkFQ1_T&@4?*VNJA6zVlL&>x%c2MD@7K+JF#pVMUGJ=1HeVdAp2P+GbN~JN z(=pN4-y7QBz0gxo{u6v3M+jzNMAP zTTf3(CleQE3od(4OK);Eb1iKRQ(0ME1vPz`r^-62?2jP$r=yOdzK0IrKqO0!@4aE|B~*DpG#9$iq;+C!t7m5L4*&Rc(c0F1I5{vj)V0;W zP*(nL{MWCxt~n&Hq9!}Dv99h8|7}l*jf>7njq~$Q4NCX*^Q#T<1-So*_tR-fktr9y zs(|NI+UyO3t(mY^|)$9{xG_bGTLW>(AC@ z`n!#Z$+3UDle2@Wp_8MLyQc)eKRh7! zzlW)gw}}<6otuRR8S-mQEp-=BZ8gQ`dhUw)FaO2=G1@6A$=f_rmZRa|!J=g65{3eU z9tEAC;S;t;92yd2)J*h9!pw96OgKE4D404Mhl5qg-2P4Kq1!VY#h-RLY zWaQwr1R^*bDso(60&JAK`ezEMe^ZOc2fG*FKvK!}&d>9!tGoFxWDMMI{as_@yX>_`Owz2ZG?@M_+(0^oae#~nu=pPSk_>>ixRn_o0x2CWpzAnG0 z+AsR<`#*tv|8=Pm>At?Hfj$kVdQrw1J!qJUXKC zGXgxo?~=np>u6#V5MZLxa|;MQW}srBWw9`)d1Pb7&nMt$CnzG!?&`|H&F)QkIO<%Q(fKt^#(Fk6A4UXsGF_xv_!9RQ^ zf%xzFUx@^cfQ(!Nhlm)Th#1TVD$WCon`@s=k4|>;uYVTZ+~J>JmaY$L|D2p0o?mSP z{fA$7YirMJe@Jg@TU-A~*Z29cxT%?`fzgG*w2_g|`|T^?g=IOPGE0J*^9l-@KQ~m= z=EMNzv!Jx9qU<2#U99iVpGE6i*_qpWxgYnB8k*~lPa7M){N5^0j34S#uz6)-_=e8f z+}jcrLE6&7Kv3P=)7TN8P?HExMORVY+sYaMq_##@^vq_~PL^Q);Sd7NfrABj(Ztc* z-U9duw=b$K3q)AJa4O(ICThv^yzP6#Z`cqR~eX*39D(nMUSxMQK5|ZL*uoC)OiV%$R z#nWe6+9cR&DrP|R3&BuG%0FmQMe zPzWBtDC?1u;-C^fgeJv>$Hpec(zj-2ek4Fo&CX224G01o13_VKCO$R_4lY_2dT?cW zM+^*fe*e}nGSD?KH}Io1GIf4_ysxWsx@WFus4FdEY=7 zW!&$nnYq4+(P_XBmQ>{ymu&^)@@#w^mVPhLx$5-~YuMA%qdplF8vG5tu z3Ou6p#D2p5h>u+eo0X3Ff8$@8wz@(ONih)#$w;(t$dJ_VP>&CaTbIrb_x38qU;+J`8V}LZ>iWT1b6`&A#?Srh<@N2Bo3r(XtL2rA>e2m;-5*=y-?qBD+Bzm; z@*~#4{XhR>dNOr(xIJ-rV6ZRdq_?juCb#TMLEe|*kixuAc_%}MX_ZZ7xqt;Z{nDIw zSX~(VVW2STLwsyupnpJcL~>w8#?N(0B=)UuZs4R@HiB!$iww z!NMuX|B8@aP=JA&28V~8os-5`#|>YI^9eHs5`qE>6t)Z$DH*YZ2r3yo4m6@54#7hl z5;1pUc5H5;w<*S{W%+&hP>%p$!{(-jOZv*oI9n&)l z?^fCuevN!>UsxEPneCZQzJTc8Zo1Y(^K(n;vOg6pHh;;vY|Q$Ydlr&klTlk#82mo; zUBCywxsAh|5MQ7IONyIM``|MJq~;;xr4iBf4vyCLBJjBK00wh4HnE`aHWE@bgsh*6 z2v^0!?ya+?yt4LP|HosXpk<}6FRk@l%uP*8Pf6WYRzZ*J5zUjk{$B!-nTt^fj*f~) zLxG5gQjkuV51m~Q6%LOE9swJP2=h+=C@X|TLiik?kOUPMn?Ma8>-gud-PO(febE1X zS_1gz@1>)q#rpBRoy+f=dzb4&ZDW(a`i93l2cioC76;qfGlu7;x@UikB@B-b%?u4M z6zvt%lvQmO0Q~o3V^L$nO+rIUc|gtQqUG8BsOIvDp?6{L<3l3-zP|I%2+nvHk-Cul z{$0RKa(G-sbXaOwDC<*8TSHAVXOHKvyu55IxiBB;*}1>vcX9E2?d0sr?gI1ys&bNY zcjsU9g@oiYaW!c%LN`TCB{c;fra<8WPA75;_>F@>gN^E|7TwP zyk5UrJzL$W?)|g33G`o`N!hWRT|HfaeO*02+PcTnzt6W0ch1aA#EtcQ8;mLj{6=wA zZdK!2azS%lTIOe<{M(;cOKGUCIc}<~FOCcf><^z2U9VnU*PWi8 zo$Oz2p8Xn(IPIJ49T|_wul*1Dukxe((%hKOHB}$$*E0+1iw4_L8!A6n{b{K^%c!iW zTU%%?3iXQ%=#32eTI%;cINK+sI{kfGR9Qr9S}>&l>AJyHg!F&(HfFRElCsX$76uUh zmjw?-1EPQBcw>k0T0vb^)miT^#~@Z6!S=WqD;TpnpJx5`M(Y zFHA*E?W`Qev0#Y_F$tu+ za|Sk+4}a$Gt>^q$y*aHt2J8xt6w8XQ>_6O!y;WaemR;Uvhzfx;sS!M}yQ9NZlEHJsj>zoy_( zvs05s6W5TGQdYdHe~|vyP*qy`nUoXgsdg37Z=YvY0$bkN=(3p_1&70i7u*8TTX)$Rrvz3u4sUZ;k zZzmM3lKCq~Gdh#kCRWt{#((u)Ro^(;I(s;{JeO24)zX&Ld`ORO2I0TRU#h=UC-Q!w z0R)jM9tzL6nI1E<{k8x0cl$5E&nx7F%g*rx5el6RNZw$Wv5?V82oa&k;P4Dx%7PtQFWUU7A8l(&&{0o50qB^?pO_m=$}_}i?j0=)0=&AYMKE4Q(o1Q^tq`%DlRlE zWMS>(lV50Bc)M>zdi;E2dX_7=rujfOe|~+LH$G1b9FX;!{_wY z$%9kb(q2PGS=#WWl=w49e~8Dz;qgLUN|WfhmX;pwYaKm#MKIqW2+~sX3R2L}yiwGJ zh0MS1{Ji`kF37AbTx@(V4Bc2E`p2j6sPL%bLc}lxBse@!m|*r5bGKo~Av;|x-e28b z%9~p{-`m}+iVT=PKU_YpyZC)}^`+-z=hswg>rh*IPQ=fSzWD*)_Q8e8w6)ppj;}qF zJwr1y2ScALvulbA@_fq*N*iiFWo2!T^ky`F$*exBYN%>BEUFJ2EDQ7nGeBTS+*>(B6?>$`hjhbM$nx*LbQW zM#ETz$?j2>-QL^0DCKW>sMclFQCO#|If{HdaZv*I&-C4C)SRyt3J{+Uqcr6TUzmwH;yu^+K> zGBR>gIm)WuoqrMD$L@Ii{5*pEtb{Z|PcU&&AE4-&+CPHmzr>+X;PeDYv7mV$lAw|! zVMu!!_e1cX+y19xb@TW9;?bY8^@)SS)vK+|%+&C^_4l@p2AzNJ8k>kOskuA<)1&?I zTi*anzVo>_7lMCW_KXBV^sl%5Phc6C|7-G_Ga8#}0%n&F^T4Y*!GL8-Y;SrOlbrB= z=;XWsqJJNYNeN}uP>~aXCzglspVVOf6_FHu{?zKNv8$oJvG-dWOIalaf=5CYKpQ9z zCoc0G>XosV-HR8`)Ltm6Yp5XzQ#^q1pB8jff~Mp&kEtp7gb9TxC@@*Lgy>l~I5}VG zyD~%gUs)V96mmj%LVQsQSYlFq5(GRPakm$25dBZd!P@$-ZJKNjM5#Q`2kVG(*>HuRS)5d2354himogfI~)Avvy$ zoRZ5^u7|{Eq-vH$|>5_>>Jn^v}7M*UO6^+t*fB zOKPY8{QOmOzV+wkHw-T2XwUd_ARd!dD)WtzZ;eh#T{~hT$ zc)XDqxG2o%o&d0)(SVPJmfD?%pO2GG#EG1PoeLRG=cWEDCq@YVD?vbnK%ys1f{#Ol zE#d0K3(>!q9-nS)6!fi~2M2sz+d1EB`MLJzy#D*?#>eE6VF>;mF*P;tEqLqOO#EN> z-)cwi@c7*O1Bm}+xAb#i)&{`8K7K4XIoKYFyQ}|2B^70dG0jo)B@q2bkAIMVSVwbm zT2e+_Pi;nmPts&$WZZ|5N^=Jnk-PmjwJ@@v)HN}7_jF{TW1^)ZqELb8Khd?d6=jhv zWOeZVy8mBE$f;=PBHF7eKDMFeqyhZ48Ktm@pb-KCJ2x+-t);d0LtM5e41)FyI7sLU z^l*4D?&=>5IvL(e3MfQ!LUM5)9CRX8vR{i!+e-y|N0+}pZvp>L=E%>ri?65GhbNbF ze{MDpc4v}P6F)6)LHu9-Gc#X1GA_qDf_~3U4o=5S%>$-CrmC!<;Nw+jZbR|U;-=D~ z+AmiP!2fjK0Q8?nC7}L=z3;CHjPMNz8=08e&CX7a@{j)h&Nn@FvL-pKtpcL|VtQ@z z+TihXFV|Pb=3Kg722KJ_rY`RG-ds)q5Wo;uR8Urs0OwyyR7%EEUsKIV4V-={F&H=~ z@Rk<@|FmLfXJs^lV-&E1rWE4!;-m&gpW_WX3oDa@5YRs=VgdapIs&>BH31=+5YRv3 zDf5%yp%ZB0lRhL=`L=($wz7EKSX~j4-@3m4`=EJ!{pxq!%vtaL`SJAOpMlQ)sr&t3 zN5{xS-1gw;;A}-zVNT6;_tahg*U(Vcbmf~>mR&L36A#gU{)&qWjqLjp3NVXX%bfcGv${TRXntRC2c@rsT0zl2nmK)YTIg zg>jR9sqtF(xe6NZV`qr|2}_QPj>YOR1KlGb0X`NgenVn@L0*I>f>c(|yzwCVKRaAp zR1A5Lu7pZLKnVChQ4%sN0>tOs1bCzmF>tXlCEsNZLi9g*S63_R*?UV%M?V*r4lb5! zk57O9+OHV>)zO=j-Lwhe{{j}KJKNvQEliBhe;-Lap6g#24s8A$2J!!GRaRz!jFjwx z{2b8#W;B41+=AuVgWLL_G!X9d;ahA5Yl#0%6<$hK2U=I+B`Ns-S8DQd;QsSc zRpom0gx6b2|F-|(GNNF3(S8nx!$3P(P97A zm58G1kDEEQdHLD-%f*>ZjZICtf2wL58|#jf02BuNU(-Ja3xbj(xkNd#QbGYjuC~W?=CO;6Eq8-;kYDF?F>6 zyR2=bqqV;^xbrLcwJ-Se{{TY66H}ECRS*U12S0H=Xa#*uZ53i2Z5@4u7rHvwZYtc= zEVRfJ9JG9tG&J<)C=@iD!aRg*R6KOxk#v3x0VZ5Dyk{m_3`j)iQYa)3&=7I;0RBY) z_&+T|90CGTm7T?%p9dhMDR*G?8f<_2$?DO{C76G+lWP9^`e&wQe)NO+FZyWqOJ+r3 zVPW=V&qd&;=7Ob?%(C)^a zim9vUst{^ugP)ob7UcVdKHbbS1r{Gk4$JwQXlmxD)o z@Bmp728l?Nl7kc<3s3QlJU1>fu^KtX((=|C`2M$60sjf=-}cF2)78OJUH9(k-u3yw z>MpM_W(pYEOUvx2U4P<<|Dew9(G-i68UvGvgz}BO{f2Sw)$dpEq)fnt!H;hF`C) zEJgwU*O!%CmTP3p_Z|5 zGI;#j&BFGTC55Axx0k#7D?TR|PY-8DS2impDS}52Eo7c6!OQ6?C_dFBQ`OaZ2K0}3 zS{g5&YpXuCL1&^ypr>b{U=|i;dO|BAg3rjyMoZ1k!)2iBhRueJpa_El2aka!jSQYz zg@%UJBO`f;4eoy}9vtF-{J$*$nSJXQyPtM}3HtoF>DR^i#rf%0)!EUX-=~A!;ro5< zCq47uzYQi7`>p)wm`I)Ini?OTivt??G2s8(EiL?*RgzP(`Z25N<6?4E-ST2mU0F+G z-s0SDbXj!ZfN!jSKxBA#u)ohc5H_}uo*EOKk(4nR6CVcpA6rvNBhdXi+l!jH8}Ojh z{(b*@gQzlPWz`2Va$=%lPZi;$MD--dyv4H&3;>ZZ+Gax=-71NCV1(hufKD8e*SD` zrgMJ&rsK!-U|)Vo(Rt@iNM3!(YC&UBacs z?2k01`NYg7yiZDwP9KW{`VSUKE0@=1jviJv66RJG7Df!%sL$L??0DZ=xOqFfv&jJd zAJtkyT0!9%CKHE|sJIrXnxuxN{!2VHWf?`RH#8Kqv^-D@K;QUC^tOzx?2R98b z7n`6nMBimc2M3D?jSnsXIN<+zM1%r^i9mv&OI99?~QHeg5?o6y$>XC&gIovS48jB@I>(b-2Civd(Z67j~{cBQ*D3+@&)+EX#eznX2nMUd~IX`{nJW7 zeM4qyO=j+wa)5t){rmh!rluu#eggRC$5_9>4}re@=kwLE5y>Ozb_T%z%3uTZPc#ty z&p-UXii^9ov-vApGF~es6-iV{Eopf%WjINoe<633eky6EF0H65tL&^U_nd?3F((@X zmxqit!Xse@Llz1);Qx0M2Ko=@hhYEF01Ns61rJvDwE`_18HyM>fWC;ao|C~oK-T0$ zLPlp^Jv`jmI$deo+yeTyPpRND?b9h~Ovb8-Nu?5zD-2Q#0tLsP4dRyms z>+lGGLwkMe)BfZ7eQUBlmVNr4?|*jr=0EO#K}2#;P&{WLR(%iw(k%k<@T2WO3<*Bx;oVd~*|L3jtOkGDxQ&;b;hKz~^EDJ3I0~M?Y zBhwQR8Y&7W6_3B`KMx}gmLdx@7CO>{$MEDLfcqzhMZm*kL&qUSM#p~mP|7=FVE169 zVEqh00DIZ1e=ZiA=MI1WYCS#Px#_z)-uaXnwAVk`9^5_M^R45XZ#U4tr(J#jTmRx; z4-vhUyhUSu?V+^SmZvV&tQyCVNc9M#+QEMa9F#BV?c}`gi?@0pGuv5H1uV9wPJ; zGCaWl8L(rat(UAEt*-4Y=Pq5}Tz;ItI9Od@oW6wI{{esB|3l#W_djp%>RmYRp6njK z-T#n#`=6NE2=ek8bFwNcqkz6Mq_(IC=>Pv(|H-}e8#=jIGD@nei*leah#Gmk7O^vO zc;o5FXJ>6=$?0KfDT8jTrK0F8320y?d}ah=eF;qxO<)35(ZyAlkyXN^gOi~wGY1sSVwxacTozw3Rod$x`jb2t8g zu#@e~bKrkyI9^%#RsDT$<7D7+Xyj|+r+_V>4iD&^>mT?!kvzDl8xnvj6#U5G!CxYVzRe!k74< z)G;6j0{Z_u{`KSY~{7ZkYs`RTT%uZBz=mL zK=i@*ZZdk$fIpa?f`!dn%IWX^Ux0#=_ANgX3#TIv8$C1{D&k{&SXeYfG8i;yQU)Y! zd{`(FLP7~w8QzB&gepr1D~mgecl_t>`JeTcxucuIKR^FmUCkU_9UT4{?Ed<1|7SvR z$lv$>E&rGGrMhrCG}^zus05OKQ_}cxbL=pw?sLm&Lt#^Mb^TCqd_rvR`{-1kuFti# zX(7H@?~;?_0sj}*b}?A~A>53`*zuLAjnQL6FK-VjTQ?I|A;|h&U7djb%f|Y(vbwZ^ zmZ-S6^h*gjQ9WE%0?(IX>O|U4b)JJeK~qki&4!wXjoF-+UVxES#0cZbBYqfCZa&UO z#ya-+oUHtI&{(h_|3nspgp^1UjerPIANnCQE&=>A{s-s}nMfXx?RKm$1OBJ96Nufj zcdqw9{z>EI((-!E`TqWK#oqO3>(FrM&#|%A-+eO!t@E*E8P{{&qe+uJ(=%f;(UU;` zwVRn&l%3zW7TR13fS^yc3l77u+70;_ovbm6#fn z6gF8G9g!XyW9X=2YGCDLWa+8wX#3jB-POy34u|GB2s`3-v-0qKBZV%ft)hu6DJcs4 zk1*B}IvTo9O`hsVXg}A~c9nYhoQ<22iNQ<^Y$(Tu)g_EDxLlEd6-#p>r;OAtp zqee$VLWAOdh)9f~VkJfe1%vkz8x9AToKy=N0|`x)WM>2Dp9{K9Ks4Rz`Op28l{TP$ zy*xbJ`j}ihe7JKS**e<(b)X}tb$;Qy-{7tPIrXxw{p-cV*uvNX;Cp4|^V6G+Q{fFtK;FQGJ#E}G` ze`GQ2{L{OA5NGV*XUbyPEO^R|5LWN%MQiF5D!2h)$5mLjgRy1LbKwnx;wG(g}+ z#3Ue0FZ=|Bj)Rw)LXe-|SjX<({$oHRqL9JAq{PA^2h1P3FdP{%#_`7PZc9MP-~I0e z$p5IlI61mL+$gpS5>mJ?z_I2>wjk@3WKh zU)zxY{~8$i5p?yvd#`=4t#4xDc&>Z;Xr_O7u>E~g=uv6q=bg`0xt}tN?({#EO%2uc zS-iYvaz}MnolmbY)+N{<}FSBRS4Du;+N8J~1SHA~_7W1Yh0hUrbC4Y+t>l zGPW>r^?YOYM9?tE@;3h`q_2Iht!wCqFU0@$XL4dBye9T!;@kIVV3+tK#iU0Ek3_$6wy-jAp;z|s zbbDd!<;I1{ujgUv!0+<IMg$+J&+%hTTz}_ReAJn^?i0r z{$hDnWm#j{@0OzCg5#F*n)-;iftY}V_aQ0Wh40^$_y)#h1ZLOArbLIf{~0NZ4ohS; z015zWV>$z%f2FoDG6ni4L6ckkKlgKUa~EZ0MS0PeWVEOz($5rN6@chRN1H?!=-=?4 zYpLm|%VDxnuu;+Tn8Q7yV5Q+Bg<=unqI|94M#RS;?EHjDP>2^^2?ZY$R~GIz|3I7! z9R`ye>IKj3{xgzdkYl(1Tmk35;J=;!{kE}=!Kskd9`MZ9;AMMz_gGJGrT_1lt{?H= z`{oxWXZQ1g{(B1){-RGUD@~?;=9xTAl~M`NfDF55J5d8Bh#hD zM1&zlL4QI(D1-@~Wm^OKx3YWp-=EXN-$z?z!(#(eecj*s-d_yO&HxnPczk@cZKnNZ zetKbHa4?|d^1s#p{JbxD6-{Mffd7t*@{3KaOZ1P8Elu`M0{LgDaq*D+x35m;^OS=W^m*9a3>`zV83swE$G`!AE3c~zIJxCrvmhAbaEhW zuxEUbsAPc{gPrD{7i5&msP%qvne0fXMj%4<8~!Q@TO^P2M{Q z{*#d9laT_x|Ec@mzoDU*u!@73hqKxnZ&@z~8++?FqFMm|(;%UPGL_VmgH{q%QGKCG zq$i;ui~CGnT?G?{f}Pum4;A$;ftlmQPy3LX2Nr ze0ucM2VjXEO^dRzWfpyHWo+&N@*j*|TSNLEF?$nZ2TQ>Ja{P^divj-&jH0NXKIuRF zw~dAAKS}&{b&9M?2Lg05VmK-9 zs(bU_0eG3TZv5=*`1%H-|2i1x?>QLi9q9h?V{-!Xr}#cM8&REjJlQiBGdH-9ou9i? zR9;cDUhw(z$K~+M=9Zc=NdFhoSl!%I(hwRQ6yWcZFu&gM86-fpM`nD8PLBVc2JjDn ze@DlR)Z3b=np(J+JByjQdA-$h^K`cJro-X5-~T;*DhX#UF5{u8FQzMPq%HA6RbLiS z2+NU`lg5gM`|%?RrdQO!|75@k{6FrbRMd1>+;ntg?Cih;iBCxi4?_T1zm|=dAQ=?S z3mgRCd=`*x1_!|f%rcSyw&worO|lNoHb=%VCySWQZdk4GIMb9nbL!o zVhu%Z^IzO>`FMBvGmj7VR<1xWs7Lwr1VTp*%2Bizu+o)3;y?zNi@jfDr$`a27| znHjv|cY5>2iOb5v%1coV%|KpW=8fWuw~_$$#bG1&(v?;t)RK9oqppLk1^iFekbJAx zd~|#al){D&L_`E(*&oC4F*A!0@(Z$=7$~qjz=M%RAV5NfMHWW@{wKhH5EDE@B0wU- zLRS^s-`^S8{&l!K{dIe7^SUZB^B(>K$;S$w`u4rA`-kr?$j4foYik=@hphj4`p3vc z+kRwWP1Z(!9Yp`UlneYnmo@dZb!Yhv`AucTzp^93!mC4KV#4b}lfvJnjsI$GP7REn zi4RIlib)!c4|TFBkBm(M1@s>f{~t9e z0u&|(843v@#zPD#@6!K#{#Va-_6`sKluwOMj2`q2kG7w8^>wxP%*B@Z-Jkyo3-dN0 z$(JDCYtgqkH@7^sw!8+&06*6jg(d;B-_Oav`LA33Z!E}v8jpw$4F&ukla_^}qqV4s zwK4s3H!tru-lA5fK>xw2m(rrT&s6X&HI-zZy-}rQah0$|rsrlg zWfTBX7zS^C;D2)C2H(FkJtrLl4?2S4ZT^uUF)9)w5{ZVLzAy;_whkT=0pUZ!JN^G6 z$bZed0QlF^MivDBoIg1~0{Mq0XItk0|BAVXe}R0s>BK%T|BOt3ILfNcEzin|0|{@X zEg?n44K3M)8{@0VwFS*5^+gc<(_jejzXAT^y?t5!24}u4%>U_}UT6iv)r+aV`Pg}o0h0G=x3H|d zdZRGsbLQu~rLe}@nx@S7y4uRJmc_aDzxfZ}jKnm+|D-1+f0(UG1kcQ-f4u_y$N%ns zygc7}+VN|=aj|!B)wQrxlM&TY5w($)R+LuK!($=y&=b`l){xc$^&dwA;NP}XAnk#P z`ZX#o4FwGyA|vSkpHML2@&WXlUWlHZ`Gw(AR#bdw8Dv6KBse57M3Dah_zyx7ED}w= zhuFAytAoF`R(3WTgFbctyxz*%1KCFx2ldx0JDXMKM~5e;T@g#S`Echw3myCI9X~++ z&)4r?!)sFR&p)^MM>o+;jV+Cp{&|gci*slH_xel2Q&Oc34BuG3@fJj)k~ZnO{3$;e7eNQuf}a3kKsKN)Y=ZwSLK#3e|}2lC)} zxY@Y*h#$kkVdJBrAnCq#xQBle+^)Z5WUwXR^Zow+^yJscd3k>q$bW(4<0VHI1TVJF z^<)gUj{@P#o&RIuHvefSA+y3iHz(_3{^{UNYIEk7i!V9V4dpGzU-An|f&V8gGR&_h zF(@&_C%vuccK>q%qf_#~B!5T*_&3Kr{M*jm&C|=1$HL9V!_f-E=vM#Y^h`k(_&=4! z9W>Md{!4mq{UHB}ijT_>or8)NL>^I*GchxeanSQJ68@+87n1-M3k_C7m;?=P1>*l~ z-8fphIw}jxxi|m*8Jy^wnh*IoIyf{m^X(gC{ym#-odb7aUG&-5&`eZTd3I(|=B>Z~ zXMT2l&ZoSq55-xP@dbqqACoEzs$&ypR(kV;<0895Q^LXd{{hJdm<9M(RD|zX^M?;k zApb$e;I)B=p^mqgm7B$z*YpnsbzEP0^SE1jdbzk^$O8X23dp|`)l-AD5fhWV!@txO zpF{c|!h8MC-S_W`&BM&eN`oxGfr$o-53Oe^`8WSZ0u6(xO@f1gi;Sq!y|K5}wtWcn zkL95`ZA({Y%||OIK>t*80Q|2Np#SZeoDJOt{NGOJkFJ@i^Y-EPjuG(vN8kJYf&c4M zef3RZV|G?nKz2c6L-oJbKlI<$Uy>4TZeRnFPfaYX6->NexxV4T@uJ@Af2e5%DgVFyCt%|eV8?;vUqit^ghqf=kvb zpB()G>d(&Ke{MRC7eNN#1nB>7&Nq$^4pOob?#(~*)7{?_u71qV_l)*|{I}w)90>oB zSW?$qnEDCmAImFBcc!=Fzm(J;G)KMj3H9lX0AV*>zCLNbtzTkeqkWQmTY&$kHYzsd zgOj*5#Pk5ke`b3iByM2jBIE!hFy5X#4#5A;?xLuw^uRz=RQjcqp7Apsy?^{qc3LkK zHD#p`gwY?cusyPT#6!zS$!hY5|A~k!1qla@6%~m9 zh6GREUQ`4`qC6u+CMLikP{lt!IlSY4|H?o53PNG8wr4>7zqlOl85qd;oUr}%`+)zz zJR~1A?K|-QCytB*?Eidtx+t%_Fdi^|xw*;N%|%TGF=aUwwe{sY)0_W!{kQo~|MtID zCObJ=8oW`kd}G3aB5;rYaB{R&K(|sblarBEl+zQJknoU`64y2meX1?-QcnU(@WH+L zmsW^U*aPtix1bR7fA;^0{jLAOMM3>lvb}e_nLob1wX}EM1^BP_v!&%~sH^YYbXRvr%G~#_=t@-WK!s%p=*h#~nPc#A%U^7^%dfvl7 z(2(#@pCXZy!-+E>-~<2LV>~i!ToOSda%3zB|MU4C|8seSH)qlz;tIMEB$%`pS>ciS9>*}c4i>a!r>nNza&{noqP~o8i`Ufv*=X?AI8!sc9 zgV1gMoBU%$9B33ibaGOlf5CZzO@u{+jUw)*!G`^SpmY%=An&f^EnZz;WdB^bxokeT zx%zd!QZsvte*ym=!2eE1yZhQd6z(jw%}=F|bxceyj3$hLu;l2e(X!%y@jnqI1w{?n zpQ4+Z>dUGi`S($EwI%6cL9u}~A7TT6_APp9x9uMPX>R3U%V=t0V{PX3Napoxn^&Hu z5dK@k$CWN z|F>XlXsj!&YvOBXYyX}9b7rpnN6O^v!u-@=>^aCstSYYlkd;+hRb0^Q2g*QpRrcl> z(Et1|{x32lF`z5x_Wg^6>iFnbzm%}TjF8~O@KDLub|C-M+?f+u?y0f2IfcEUqn(Ex zKd`-9a5>xAIAVw^$%)CUAz3LX$mxhZcUP6r)Dm@)kWc~Z*VNOO2hL|y(DpMj3)suM z-ot-*!TqlYh4cXABT2KMqdXvng?j{zhd@b4LW<8pgiVM|bXf0O`=8JMKm5nfTmECY zySKaZM`vr#@_0x0NO${U?=(n_9tvpLzlVP|G=Hl5{pI%khr_UX{~$=hM3j*5hXh1K9wGqioSl(+eNd*kOu*80!Y<;JV?wTl`^{z1i$tFejU z_OBzWeQo`nKla;O+YTniI)MK(tTy&^d~o)|LRn^3Vb)d&$bY%he^oX&HeJMi`BYTc zP@5NC4mg#tg!t6?_4#}F*RB3573AMCNtro2x`F(2T_ZzdXJ<(Mp|+!|xea9fO#fy5 zkon(R=Y_r;6E`#~GX)(dD(xd0dKQTO&0O8~9{!1eLI5v?1Pco#`cM8J7Y-&lE|M@7 z)*f)b?QU)KobQ}(oiAU0iTb#3xpr80x^e#d=y%2G_V`G6@3)EGpuX+GKXz5Gi-%~5lCdBk8&ps#;SXlg@bVyZ7dzO!O{-beZO zpZDC;{}>s18Ug*Eho^up@IOBX^Dn?Zfd9u*N?u%E@dXwmp`qAIZ8CLm0`%1IRh5(# zu(j3LnD6i(I9g^VpnoFarlNczAVAJf$HYPiC;*86QS%=EMM8{-1N5KR4^=t$*H*W- zmM4}k{($#i+Al5^f3A%z9sPpvza{th-%gPKc87nh_fEFA?@y1;W>*!L#}`yq7ANHt zd;}iI?UCJ#hTQV=y8H%U13jpTjf?IN4~h@{8WIrT+ujtI9uM+AZu2ke0RCB-Ze#Gu z)P+*&wW*oBt+l5cME}NePyeDIuPuonD=97k?E&<^`l>GAX*+GDw*dckpk$>632;XE zk7;SqsrUtX{uBRkd;g=r!^4ORA%NgJJl@;-#}C%eNw&ARx^~(en0t@^_;t4ZtK#a{ z;86JSz*I+SUdmeMLa)ytP(OViNrvR3#f^>(j7DXH{&%~)>QmvTf8T#m4Gp#LYpSdN zm;Ui~{try^NuNsq{oho0aAHQ(NRy$1rGbV8(EpfuxjOTp3;vh;58$8h`ua+W;^dC% zqA#^fpJ{7)Ysx4;wWfLOD&cVt{}FjY#|`w~oNRU=?Gg(4|MB&faanEA-ZtGxw{%M* zAq@&hOQ)nDB_h%dB7#T=QqtYsDcvndcXxv{@4Usc&whUF^M2v`WUYIyIma0P>yn{C zAb~;_fQ5lU!G+ZSxv=q((8-aXyT5q6ceIkX0y3cU+jsfz^Q*<%+Ec(kuh?C`y*Vmc z>hJA4?(XW@96SS|@1Fmvf8}JC=YaZ0TKZCTeO_tZwNGtLO;|;KR!VSHVUBNDlp6qQs{Rl_&a(|#kX%}s;H%)?5}Z7c1JNG-x>z(M(#U69>b1l)h^ zxY=1BzkmVv9~o?1R1`#UVHgq;JSrG`f`^!7B#d~tn35h(dw2JjbC*w7w-)x&mcjj} z;p}$l?5_W@w|8@W7;@4*1pFt*T>}H_fBK&g{>9nY#Ki1emtTG8z5aJ~T}s1cMRj$> zX+c)QkJ9~0uP;$SU%X4bVnYJ_M|UUc61>8v!aqgEh9-^x=WYm_nYFFiBNOoc(|URW z284*Dt&4%1(>pmEGY@xXefJkC>XPbYbR_rspNbl?irBh}Y>$}eEa7Ol1O=EFo;>)& z_s6E7q!H$4W);R2WEVh#!_+iZ`}h1eF(MSa78xEgzAD5Iu(6!qyn1?g0_OiZmlju6 zPns7`e-}h%j-0Mvm0s=Mv~+a@_743V^K0ywYW&dGI5#;p8vA>!b!hT@>=)qw_!Q)4 zl2d05^96mAK+iB{PEv( zTpn*9FJ3lnt*kF>UFU|T%w4V>RGn{Jo*kbR{|5Enz`oAW9*}v9qy( z^Vzwhnc>OdbdZ1E{P+BSbw*~|MMF(i`AJo2NqL2Df6|AjK;O#G@gGuyd;)wTe7cTW ztH1juj>dk82j_n_a|cJeN7fcjCbVX+j7(nJ-}V2*-GRu>Sq<<%UGtfZ%Ulz)Sg#0w@j!z!(i*O6PLjwLU zSu|7vI57VW3pAVhj|rflo)Ey2U@@W+kv$<>I@vk6{_!d6|Jwfsx_jEctc`TeO%6=_ zssFVt0QO5zW$?ZFM`}i0@)WdA5gC@%Tx6f*Ps*zDLPY{iLhU)2|AT)9?*C9)FQ4j@ zsB3AeDC*-ny_C_$q+n;K{8#?nkyMECu?sIfA3vwCEjPITE1*8WMV5v`M1Y1te){ME z94$B_q>WS-}jkD{W%c+6hmhSGIj;`Lefr+U6@Au~4 zvhzv`w=%M`Qj-doKV;R^l@_H`e9f#Z&I@g*sjA8g@%|S2DROqTGwoA!U`t>^F!0|@ z$9aG78~YmdL%l?JY|L^(*86Q7;_Dr6Gh?ocy_c0iQ#DVmG^Y8V4Z!gwQ z{-^%Czp@q;6%~3h**_3jka#uQI1)S3J_`JJ;Uit$1AAG8rCFQlS(z!R1xtzkfs6Bt zn||q~WeHW)g{6g=%kw|)&A-Kb_l+5kjg0sj5j~VI`IMcLOHk3s#pRW;5eNg>xjP!N z;W6qsTDo(&TX}-|Ke{a7Um}T#Yd?oo5EIqW)Oh&X)*c=j1H(y0QAL}Tn}CA>j`J}; z1>n8>&-~L{bxT|x20>dvR$*9VL^(7pY=Y-dh$K)?(f{=Sb?Di-WJKi@p5UTCTsT^7 z-ak0r-MqTlZQY*Vx;mcE3NN@f|J@JrUmpjWn_4GE;(@^PV{mZb&B)O7*pPR5MR9&6 zIRAL(re`HX&OclK*ZBt|K)y7lR2Db&wX|eLeF?7d2mBKwJBQbQ`M1F1_22a`Ah(qh zlY@7*c>53^0nQN2KA`g={dfJ3SwxtRpTh+Xi^TGkmH7i!epq@~D8N4?Q!!Vkhb2cA z#e&8G{SPg2G;|bIJj@3;p#QtQx4o1#InY&IURyhHvb1n)cHv2i&D_)h~( zWAT~MD{T`!9|q=zeol_Y&H(;jrcZWV(vK4Vtoodks?vsfFch{l zaeQz7-PbF@XD&R>FWx(3y#9~>AV224*(+-+&sX67C-~p@Kd}GHE9oN{ODUT@d!{X; zC*h&4t^8EqQeH|M^gtzD#auC%cqz>o`Itcc$B>?emdZ`&37vqWAQu}qBmFzTKS4pn zqD4X=1^EX`RA>S){|o$wu%u+LqHfBZn1`$Ri^t0=dpmdhqt)w$o4V_Z>d_0}zq`KK z{Mq(%sJW%_w0#one^L3~|NZ;V%U=JSTix&j^uOW(|GT`T)GMc`;77sg?ACwRKN6$I zs-peO5)9K5^;>=b}~Xd`Rz-}gThR2fx&Ht{<-z+tp zUDu6WFP$G<-HdE(>~4-6j)L<~&;NP;%`VvS%FQhS^`B1w1W}Xuy&)wd=~GI2eO2ZB z^zOa>&!6*ee9Q#!zXXH%w~`n@|Fe{R2S&c#LH*y%+K35@@89`9M^!atG(>1X%Ilum~O#VBnyWC|Qb9LX#5c;lSd7`rjS@ zivW9nCx3G*ryt~>$9He97Z#R|n-5o4k3jyf;@cK@Sx1|JDD5gC&2cNAggIjtCd$;R8-=bX3%4 z@bW|EKkxTHk50D_icbfE4|+TLTZY%BXQw76wkJn{4kx7i>%IPGa>fdTfBT_4CAp-u z=u<^bPDA$E%-Oe)Z@#_05q=-N0^59m|FZFW!b~lgf2@fQ_ze7a*+J&^rhxy&CTr?y zMPub|{l>{o$Q9JTp4d2WtC*R;l~op%Q&g2vRWXqR{!;~ATz33>^AC@hEbyoV_^D~A z;3!0BX));l_yX|H-rt-5g@+Ln#Djq*CL!U2K|{vC5C%inPj z$1cb}t@Y2%g88R_lE|~k*1@Q8kO2NQ^);)!qI4@OwX7D%(U<=CA7d)Yf&Xo3;`HAB z=j|UK7aAEgT@mh^5L=uW1NiSXZ;Y*9TLAurpoEFDC6%?O4RGEFK={@?jv)W+=_K*s z&VMK`rJ$f^_Ebg}^uOH2R3x-O{{vA7=U)E{%s*0ab3S$=e!|Epf+rvh2Z#9x@Lxgb zi}1htKiDgW2a5pE*tT)8wBEYD2lBtIhiCIgLqG}xT0)mQvB_cg`actcZLQ$`1IfRR zrhd*$NlGdU%}#G9h|8<1gVg_a?$!Uke29(t68O0}4>*uQAmPlZq`mtJP&B^-GAktD{6>~Jw>#X)s#?pts^dGpe`Y*A`a$XG4IX4QZNZpvvc#f z{_!6|`X6ZgI9MS638iZ)P76{(db>sz{A z+Ff4xy-*m@F!g)uy6k%A_Nucrs0Z+m{ek~!ys!K3`Tu-wVg~S!2gZ9pHtyfczx^oA zNUvz9JNaH;P+6Df9~zbC<>wO=n+o`EA+d?z{6Bv0{BLRG>SFoE{4W2YYwu($=4@~6 zA?N03MM8;mZ~j?JQ9@ixPaoIgrMx;d4m~FaIRC>@(17_D(|hwT=x|t=@G!7a!2b$^ zAx!)L4h~v`lmti4N$uYG7tH@$@BTinA39mRJ?=d{xH%lzy!hET+B=$%5whGq-s{sl z+Yg*Danqpw6*Y3_|K0PcOiuv}=sfSjoUE+6Z#_M%&FqD>9qesvO%$9Q-a6~pSjme5;g<#}J?y>ykA~h0 z1$|B0wAl+CZX5od`eCE%*TAQqslNX9?!<}qj@Fo|pFQ1>{PXT?Nl9k0 zUr||EX6-@G+P(Z&!Jqlxp0tnQaj{?gqaplbQ2&dF_U%0zx>x^$@a>IW-QEB6LH!%> zkHP#KG1UVz8C6k4DQy|xf`HPLkbFiWp&_BADvj%@ET;|2%FIGZ2bq5)hGzlqzonA% zz5W+2Wd4m3Imj`!DICaVL6uc4k&qs>XJ&O=PVtjCdYU-TVD3JyVj9QbU69pRND#L%vp*(bSMa6e7L1|4{(8 zD<7RRWc~qFh)skON83pLUi}lE0GscIcn^Zy7UZBWnp4R|7EvH%GvKbp-s+$M@!6^>pxn2`13@-ot} z@bl3KJ17`F6sF*E<)Y&e;1IFn0{Mp*SeR&-DDX&dfAYVAD9~`A{=xnLn*vjW9I5AdQ8x$-Vjy9zb0pAj3gD{Wt&pF&+{j z+QXFtF#p!IcYIY6kl(m?wcfD5cnH{!Rr@=;TctbO`y>5Ly`2G&^Urzz>{LfDIR7Nv zJO7lIWz}V5rzI`MWY~1@~pz*(y4`>!Y@(You9vd0T_rTzmL8jra$_9 z_lb&$h#D@84YoJ7aeQuJW()eC>}X8)?!VT`3hHvtWn_?C=KYJE#L0}nR?n?L?f zJ8ODUzI*krfASxe`ciVrigL8@)V!1)l5g+zzZn_07&wL4(G=Jb2=I`RNznMfz&k3b ztc~uy`IqD08%JxKD{HB8VIcn# znhFNq%d&m5et`L3@csXN@B9CU|CRFsh(g8L7T|D= z9Sn7>-CdmC&``j?l~aF;EU76a{Y)B05}f}?!ThT>p}U5bs-~)61 z@hUg86WL=WMcly7_Zy#pPN5_~A0LGwXAKH9ae*1`1e@Uur8nm6m^@rJ^9FttWx~P)k)o3ZUKe zo`I-`sl5d?qnW9vqpP*Gi6`)*7;#cM+r2fja0BrW&>tj+LB~hIl2n98!o~$c8xnXJ zbaHUMLn0?cA|fRw(YI%&77*ZIe9TD8%|J~}@#rzFfFM7egDAHkE58yKJ6|F2e;jVF zEM8`S{9o!_{;%fr{Nm*Fc)jG{;H;yww>4m+t8o|DFI(ou3Tv-^uU%anuAiR$zNsrc zI{g+N`YG1mCp;GLU3%wyV&Z&beIoom_{T(sL`260ewuG8{NV5NIl?zEIO}8J2cL+< zh{)ogh%o=4Z{cCX;Ry}~FqG297KWy-rfD7n1@{SRG3XOB1Lfd6#of8YlR z0QqP7&tz2J>%P=~sq$V_TOSz0l$7rLZ`vq~^i0f*jOf&ST!Qoj{7?9V_#ynmJO7OZ z11ymJ!oa~G-TD7ipWwh^;*)XXAhV$CAM70L&o4IbZ|+^5EiWwG`EM#FcTRQ>!2sCB zK7f9ApLVsj^v>?|^-nf7cg9WpY8k7{J02M89O<7eDfi1S$}9Qnzi9W4@b&&0;rnfD zBr4(~q`w6+5T(g0zq2zdveQzkK|UA|VrxoL8qRLY%2LaV^HiZx%>=y1$5 zEKJnq5dNo}8WhRHhXDI6_=J;`l24c!!auz8|C!MV(b0)KKtLhIeMy7|OW^p1mz5QR z5RL6{eS3cAWNqut{}tDCaP=GT-DA2O#Bkf4 z|1;?zy}URny*$;YCM&PuV^(2qc`7*nW&Gnm5A}@*_41AINsaLjO#GMsygDBAKjVX9 zYu_6f-}j%RYC7pS0Q|Lw1qcAXlT=cFX7>{C52a-^pdtKYE=DKKJO7FN3pO?mI%pwU zswYC={(t9x(I@67qQ~VCvgZK|RD);CXehj}1n5X)C=Z}f|MCwWV~{*Vf$-1n{J-H{ ztpH~>GB+{ZJv80d(bqN{6*D>%8~d}rX9MuD(y}&A7Iv3bj&8QDSC8jQvoG3zc8~YZ zj^F$oO)snf^{=dy-1^kCl%=$SjG~{DAHzQ+`b7ta1kVhGB$roYROD7?)aLw1NzU4x zPbvb!=*+6DoQCS|-u}*845eopilVC08ai@PLV|*Vn7B{%)m~~65J;?Y2}Z}Ple5!4( zrN60Vpku75V|rpD`dc@IZ`!zUe7coZd3v&TaC8je1Au(c#qZWqSjU&K{fcXDn zqkO`nyC!23K14=_eD(8-3-I;%9+WuM8x;7ps;WNepZ^btTBoNU&ZmWc{1{*NA^Lj| zB>%$<;U6OsX<0gZnz_lDyfSoqedj;MmUDY!dFOv)a+KE5Gndzsy?g%r;QTLdt}6XP zSNrbwuS5W5|7gjnSZKIuaXEP@`DiH}A^bx;P53d}zpY%H&>3>@vP-&~>-Dzk&!0vIb#EMP99#|b9k-8;4s^ErLHO4N_2B&9 z(--}_xfg)2J4a?_%hH+$!z;5JikBhz|K$4W+|o?oe;jKE{-dP)NY6$=A&i{p=A*G3op{?~)TLzQ(nWT=Gij=mfg5C>KL( z6j(giyZuj_N$`n+xRgxm?%v|gR>#rR<X&W%m9 zWkz}X0snDzQ1Iu7#4le=q+eMXn7{h#e@4(%SC$7PH#P!H*_WEn*ytJYDEV!~#MPuE zou6uf_y3KOgo?h7@}2+rk+qHv<70Y?|K)!h&~nf}RuM&f{Fs9YoPY5|IN;F#$N%WY zhm4F!iieFLLV`~K$$ui<`L82~7M8{icdqtYc9u4mPL}TcPhWa&ulv?7H``A8h6Wl( zK7;I2cixYy?$Pn-(LumJ9G#hKx$~d)hbEVmrqtHot-sx;G9|evJ@tG2kLW-B@4x<| z+uD$r@BZl8CiZv;67FW!9@ggC z9;$!+r<|@Yw6&ykb?j7CmBqB4dg^NG=_x4O`HvI{sCk4y`WcBv5S(v>C|yLr4xlB9 z%*V{ckAp`~f(z!~nGoQ};bBQo^hj{v@$8LhNs%zfFp!@RuK@qYo&PNQ|M7oS0Qc>6 zd~jde;8^?E&}M&o??(v#x3F+&ths5ht!2IES4-PfPfu1_US)npW@&Op+Db-VRl#7R zS4DbKXsZQx{P*4dM`x@oEvGLfWg(}> z&O!gzf2T~uC@f4NOvQ^SAn*kEk60NWbKISO-tofX5+cA6BC|q6!{I(e)Q0fioh=!O z9z4L@JzVQpIas>$-+rIoTw1ukUfZ8NJ-t4?*)2W38Es6dI&J(l*3mK1e>KVNW ze`<1OcBcEze>VI-{*#!(Y9M^S^B+!qtjf;^{;%5NJO9zqpZ%{Y&Ohi=WOQh1Y)s4q zgnxato%hw=-c-ZE*yN3sD~+L*t2GA9Gxyg9@817?{_OT*8qcWN988?dEg*a(OG|bU z!*`aBj<%*AuGY>Lp07n;U_p@}BEk}oU=R}_k&-BCLt~=i6A_W&;SeI}s-aN}Q!+o| zU}F?wwlEZ7S}E7?+l8W9_}Cc3GfEP9W!%1 z6T_2}yNj2gm$P!VyLz^0}Lt!Hw+<+<^(y*xeF@<&6IL zZ=b2-!xO@x!J!etW03;B6EOlNwuTZ45gaBqDK0Io(R*GxzDF!VR5T2n46J-Y^pqU- zuV@75c!e3bIRxeg`o^YOyL&oj2L=Ma&-4P&Q`^u`SI11x*yK-ezui7dsr4+yCjyHzJ{*A%0$;gA@Cj;=Tp{@q73K;d996;Mv>K zl=}Q!aQ>;UPf7ccR#T8xl~$2k2lA1*l{Hyq#l=vVN}@Wl(kdEK(o)h8shO?@V6~}9 zsjCC!t+K3=F(vyuQx`KcHwQ>Qz|sitAspUu^BKFowzD%cvV8}*-YC$}k}@PHc<`{; z=ul+vfN!Fv0!M;?hxzga`a=*1=3r;0XX1SX0C5Zy0`H6{*_mj0S-C-fn3_$1^;hR$ zd!z5yk)F}XiLU;prjhaX$*HNXp$Xu39gqLkyL5ASu(i5$3@DafMPZq475E%&z3XX^XTJ;$`E+Ps! zG72s!u?QD46(l;@gW;QBXehMC8VIE;xCRWZz>@EHMgCD>CY8{&y0C(Hz z{^riUrlFy^wwcNP*l+zajVF6cr~5nL{Cl*wmsbPUzkIc{dT|2jkDc$IEnEage2I#0 zn*;BEqHkh!0$B8?$gcshVR2t$0rEOHW`3tQskW#*rLwvzt*mT!IkgPf0gJLL^76AT zw+m{ERrN%*6-CuFWhM0`G|}N+s)?zHsR6#TED?bei2P~4_po?v0>*+FsGZ+>xSGDP zf9vYVC1Uda&1+{nTL(uMQ5h@*Ok#K>L>Sly9`f1`0pFb*3;Zu2^IfobBxE!+?5|$& zFx~kdJ%uP9^Yen2ftmyO5op)|JlM;Bs26l{x4^zf?Z7K}k(C130>ABG3Df#(bI|3A0321iC000Osp^6vbbzBJOF@OyG% zdT6?)Xm7kb?C<JlPm z62iSA&sMU-!eYW=o>^If{wHMq_qm198}on8f3gnF@0`swJs|#f-MjN2m*-P?aWx4D z|Hl4L{=rjOQHhp{=MfW=84IHbldK9EC9?n*9SPu{!3i+|7}j6@0my&eJ%3jrC>&w} zL?IFc7&HM4fPqB2yZ?p#THjjSyKOnUxLsKpTs=HGTDklFhxHDO^ma7O-1#3HZf07J zMjAV2hUZ2m`u_5tdqaz|yvoWm|DJ#6K4g>t9&BuJWy78Sv#B@=2*JMr|5xH?pO{oo z|L}{;_}~lfM{&M)^$#o!z`uO`Rsp17Kr8g``B&D;?EQNe9h+wgl9K9o>*w{{hHa>~vC^L?Xg86yV4Q$^SYD@v#Z$s62SYf{X$G7@h$8 zZvAFLB=9&YwpMf)L?i+vNO)vC!2i~?y?Fp~FPRYkOK?K#Ah--PbwT)ctpfo1zLK2% z8?;0huTB@2Z_f)dmk$qaw{DJht4emywioyJKYk4U5Z~7H&HH1>$M4_!$3G^3lTLVa zu-})vd`Ibz%;JiivbCN1s+_{C-5Zb(tOw&#>6vw)NM2o8{2UrpLrGTZDNy{pK!euS z2JgSVtc19xs3M+PYM2;x77_PISFC$QK6x! zKA{nx@6Nx-8j6hKYT{y|DA2%Xqx(Wp4fxIQN#$kqLBUf=)x?^L-NxaSla-AN@T1tg z@$`J{;$mRS%l-zO0p7YhAd5-i6A;6K#|%pdC;yTd_5lQIPY#SX1jK0Q1X%Qjt~|W_ z)QoJj)K6$QsaYu~1%w_yqNRSs&L!yP%EZ*3S6cqjYk0Unt>jC<_+-~WQrfrB#^#}p zKhttY+h>W9~d9sZtk3Gnd+Dx|A+s5=l@;!mR(y{Q=8gQlT-E`%)k1S zrso!wm;dAcxm*8rHP{YAeLqEh0{n|Wz`qT?d;f`*94(%{wlJ|aHsVDc2?e;GwQ^rc?9>T7H0x`B;P^`#V^I&k2K8WONm@-ooTG7~=nvu_ks z+(3uR@tBX9U4WO(mP;5P%E*Y*#F&H0++2u__3bM7Yb5W&*s+_jxW~t zPdE1JeypFyG&M#9x3z_S@a^u3@$T>a1e}?JJ~0WQ6SF~I!^7rQDhqP5vR4;>q^D+9 z?Cez6)gJy%0e{af@+yn2FS6z2WnrOIWMsvWF?2=6o=RfkitB4?X%mpWP}fjYQYL3~ zd~0oHZeeL?@`%#K(CC$$yN8RDGY|h;dk4pNHt%iG&>vtRNj$@XgGa!UlOcqCND4~7 zYRb^Kcu+c8sMt9CEJg;LjDn_CBA_io`PPz7h(W~8k%Gm^g_)Aw-Gy03T_v4pvAD^!v-xraP_$7T|udKA>&-o7s5$hZFuhY{~ zbBd~R%F+u8if?u_3@jb3EDco*T&*q4sEyygeus_n%+B+bjidVuJ8M@DaRX5~brAZx z^WO?OsVQo?s!8fS)6!Ab!`9H3rWBLMrKCrsW)@(3B63&%(}oqK;dFQ`z#^pUi6{c@ zM~uvzk6Bo_8HA~X1lhQSgt-NuFfj1&aEfpVFnf6c|Lt6xU%+t7m*BDHkcghy$(f1B zZ_U4k$Kn!Nhqse6&eu1tGINhs5Aq66c8W?j_RA`FZmPl-e$V|J{+e8vQJkBXvYwpz zqu@tsVe0Ll`S044vi#EA`of(3BCoGeb1TPn|MFiIMTh%-Y&qXZ`j`Jm%Hox^8JK^0 zD60#pfAG0FJGyhdFn{BqAW1|G?{Vk<(U+3fehU1zBa`?`A3ep-{j8uR6KZZE) z6p#3sc?_tzIOymEsaPHPMHm_0BUmVEQX=4E0S6umDmk+HTL-3l`Oogvm4&5~($6`A z_xgXoE^k^}2Uj~Adz#y4LUSum@AdzGmz9@%$*m~4>f8vu$A1f}E3K*U3Xb$H_Y02s zxjA0>-~1;V%Xja=_m5WVm6MBwmZzN?yO!fycS{AMH%_$VxGwklKZ<|?Mku2W{68vM znAohmyp(7Rl=N0)K!C?Y4e>u(t2vS#V58yf)$kg>snrv8+SoU+oGyxJex zKWYZXii#lo+w}2~+Ny@Cg517uz<=cxv9{#@+5dAwe0JQ|2(RPQsBdxSM<4}M|7ZRi z4#V`-8yOR0Q)5dvOd?yycZ$Y<0!NJP?(Cu=Aqu|#v<#AR@~0<7KcNxAu96?%5McfPlOGt!!p z-_|wM4)Pu?6BC<_9djjRfdAe$G166AGu=OsUYOC+lU7lgpIH!)R1J`?MHv+%gSsf6?L%`9~At}+~}2~m4z9DDTq5d zQMkJr+1YyFaygos+1om~L;61tv>&J_E1@VzYDz+DzI+DKUoRCEm3bL8Wn>jqb@jLy zSXgjrxaes4WfWLM1X&rCv^eSLd07egS%LpaPa7H=8U@|N41t6ggNTrsm;^Hag=zm9 z9~In>kRD^9HZA?>|M>Z?pPvE$RnzUt%+kSSWc>N&#?9`=&|cd>NYp9$!)?8LQ)9m-e$*s`{!da{cSc=(VMbkbTuypQS*-F z2J-%s;fSfK%Yyu?f~=ID0<-W7ZE-DKG2It`_;fR~V^is+&21SV1e#yl-BbW1AmypvH3dbZ6A<`MbzwkJa%kA60PodZi2UPoqF+SB z!C9#Hx36KqgyH@D`Yhhw`n9zoJd}xzyS0oHI;xeqiK787fju1|i8JFvOm_(X@WFE# zDHUx=NhS&qdDK*OQCCw^k&xwM)YJw1*T)T8Oli%Rlq<~IQU;!{vcX5r?1h_APoPk4%VY*5JW+nBK6 zu*le0C=^3$69;=)Q?s{kJ(%0)o%-onDdNodsm*~Q9XbmaN(qutBt zfq~xkjv>(h$gb~fY@X>p>1v*AAN={h`(H`(SY~#1S-w|6W_B>(|CJR(`X6cIbJdlF z#Wnf!E8c-#UaRw8KKXqD_n*Csu=s@Ufd7`CTXA`s5E^C$_%Ak&7KqpuCLjT1;3nkm zZf6Vbw>;)%c5j^CSW1g4QBzAw%V$K_#S=Oboh*`1b^1r=H zJ1dJzC;q-0%Zp1*wd|#eNYXfeBly z!GQb_9_kk#6aD*l%s>7EQOkE`wnz`njg4M`{=SpDyOPyw4+}T*7xwR5$yuMu0|B&z zikPIh>$9g%*(jBDf%sDm!oRduQ`Ax9=6)&<{7+m$G;B!J)clWxX_a+&`JYhpv2!uJ zlmq@NJXjPIe0rRRB(TW9`eN;Y4d%bVqyQ=hIWZw2n&%h5|Jz^Q+qi7rI}8linZH^2 zeLWxHd;I(NE~synUUvQb**n(zv%jMwr*@#Zaq8Dxd&1DvukmT%KdUSK>px%5Nyz}G zpWdeYq#x;ke|45pkX1MNtD&SQ7w|8W0s;9mD1LJ}q_=SF`3OGrpgVC{-c`jGq~4h?w! z$=qB>@A#*_AC}k7kG{n1F5GU6U#%UiogA(M|M^~gIN-le9kq>hbWdd$cD9T+P8C%Q zwfyQBo9wBs8}A7}>(tQ7hhO`9W|ER0 zpAa!EoZebFTgrHtxSCoi*@F7Et;;(*V;5ygEi%s*&pxT!tUQPzAZr!2$A zp{by$FG~x@%*!JvDZ?bd_UH)%5-X(t!=fg^^+-^FjgN$xA0EO#;=+L;MYC|iAV&rH zKYn~XEF5?tJaT*@92C#c#r;*#{}=%N?@n;P*<4=<^9BD7P~V66-yz?>eQ+@v80wqK zDH)jS>2Dpm0Q~>M(cbahu9@M!pV^T9e%okTOtjo-+6 zTE8_`_fU{f0OvoEyXSABtt0wMLsVQ_Q(V_mM^j2!PEu5Zl7o_llL`v(pUJ2M1h}Dv zIr#-1(`ribG4csM;%CIb#>eEr#zRL!wDf%N5CsdKKnRZzhmr^ngN*qh0lHWC!ok8a z;6Gljfa&*zrN8IjsH%(go9m6s0id{>Tp9d1(AP2BJkSey{ue(dhbDh^R~O#B|KBn* z^D9%+>i*{623w1hi_20<0_t)Lsta;rqCa|rdT{vKc|de<(DyI(5ng_ge%>_+k#QI6 zckln-{@-2y9|QKifrGuF?ce^7w3RgQpVM$isL6x#kB5}5zP`Sgy0NObxUMWeoBj(O z;D5)Y#6ksOhqvc}~Qk3AJ1J$VZm4Y_`pVtX3J=E4 z!VUDdq%Ca}#1$N6ZJw*WkkezLRd^`_<{w|bcH^N0<1irqW+_Me7@wJqf&qh(o|T?a zL_y;*H7_RxJ2Vdm3-F&I^RU1}A#h?r;lP-gAQK^@5u@O8Jw!x6b8sU;B_{{(KmNkb zfzRjF6sp%=xUi<`!zT^+Fo8W z($~`o`2W9$($Z>LdNNAOYXJY+D<#{fJhQT4WTHGJC%2(I>vM6LZ^+_`-`DVPzu3)P z|J0BV5eKJH5#CY1ui|4vzF(ij8rqvmSONozn+pnp8Q{OEdl-1w8yda0Aj0$n`=6Yu z3h=+vGCY&kec>q~Dr*iTsLG<;A}U()`c}G1vf85ZbX4pVfPpQ<#z!Tr@SKb5G4B&F z|D^d+SXh{WPyaPMA`~h#tf?_N1`;eXDhoLON{HHf;*b*KkrRO=fLHkHDk$$QPF$_r zZfwk6pWm(?o}M2?g^JFGc<(Mzrp<<`0rj}5jlc#|10;GcHs9}Dj+c&$_0Rm1yaX^I^HK_N@e5N4YU@4$6m(c*qc>21 zfk_Mp3y*x4f3$VQ!ht14CBVie-(CF}u()`&eYU!?v^96Ibab;85q$)*FB=EP z`xpDeeN!{(SuKElHqq4D*u6O0(J@qBH#{^qJ=;G%IGx@!argTVhSdL)YRj`Tfchdm zExe+(0lfde?)W$5--5nxF8Y6r3-~ktakBg^G$0ln1M36cTPeJGWn^t`jf`(?;$dy& zX7|+7-Vl)g?Nsf|?3^5)iK@%U%Bs<`$v@M4A@@?2T|oWiJ4H3km%x82$gZsjN5etK z1NwWEkJ)4;87WbOf&WiPfI~xHh?|t1frZz=8Uc+81s4UGl-eOo&VzTr!211skX+ZWS7g}b^oaeIFKJ?i#o>R_j@arC6EYq&YfyT5h0 zx3Q;XVt9P$czk$weDvq|bob=9w4(ClO5c>4`qcdNg2LMJl%LH-H6KfA>q_&h|7ZSp zWy{B}CdAjj;Y)&VxVPW&O;~hpoIm*fMfsXQqncW(zq0o*Fn^1NYGL61&ea{4?7b7n z009z?g_{&5%X3*paanoe7y9x5{i>(R$*=fa>$$szg0zGXrw%78Cp-@g?Q5B9S(s~^`ZY5;1{eS% zopm)+L&>QH8A)x8pazm&nBCW$^P{A`vN)XgcQuP=9~< zvVRs89~Bc8RuuQ)awGa{oVAImvW>-iqc>VkMn=w%=Z{Tf<7)5Z=BD6bY)*zR@j^^a zRMb{YoRLc5se+g+;J-ZPRn$~@BCM~b>8vkHDJDbrh!TU5nVC~ohJk{CPlQ)RoBaux zi4-9e0tEqNJd#&NP)ImnKF-n{tRDvHy(<<83_SEBatt>Q63kWa&#PNY&EWmt0Kfm> z&nNS1hi9viVb@#h$Gh=gFNPuhzusR17vRg+m0#c8(l#~wt7-UfWO@wje|<;kh0RmR z6(t4fSw9Ny<{zUPia}>1FQTD*x~(=Z+&={D|5=}W1M333qQ5kRMte1c#-1+52Ly+O z#s$PE8oV^%R5zwV#N|0^t!guOJ02t(*)KFE=MWJ%^emCvc#!GH`KlP(L6*$22j6!6d>! zB81F;5I#h}vNyqoLIxAU4MP_yK4tqOLO4+pP1GG za8K!lO#2bfeeb>TnqU4AvVINCNd)QoA22{Tv%9KSfqH)ute+#K>P*o zWc!c+`2oHl#Q*p)bbjmlVzCp#zwZF-t6wYN8*m*HdU3ej)iyi?=0h@)``gFc`z8v& z)xQxu|C8~tpTo1){p*t@O#|t- z`ve5}B}8v8_BU|F6e{1%?^g%lzqY%bQrgHh<-U0qf{)=xk@C1^h?y z?lzV#??h#lXt+hCw8drh-Q->>=)My(QF_ipuct1ht}FdkkCTFp4&*<$q+T-92ve{L zKv4_P3-Is>JP>4H0Ox;w8$@(0WH@YYbVBk6&j`NJdd<=g%qalL!?ulxUGPEz+=Pxr}Od(+HdTV?TF=j_DUU{6U^ zb4LoW|8zFyRi`F})Rk7%4iA^6Rh5U==1%uj*93q33OWBILMuo>kehU8( zmgg0aFi;c}4FzNJ`W-y7ft98&dPE&?YUJTVywA;ka2N_IHExBt-}^fx=t*H@N%PnW^_A9HxL zG_iSpc+$VQ(>2hRTGkBs&wu9sR{LjvfeKi8=HN`<Ih5QASeHXnR9lK}=S5Ud0EX`_1_X@~;~UfuFxa{1>(HVKJvGAPQ0Q`Ez99dt>9* z4hCk%R^a?&Y38B;_?M!ExuftU>+HP*F!hg_lWB z|AmLLk_s&aGY%~;J(%xdhjvy%G}Z&lg!@1 zK>d&Z=q~?A$sjJKWFjRY4*XXv)KY2^Ho$)h_>XVQ90ggmw7FndSh;wqK_`b5@W1E< z7?ni1I4A-CUr2RpD-}~=}_@(*7*0r_e z-M#a*`J2h#r^_Q3H@jC|d%IVg7aij%l`V}wmpeLI`v)dE8fUMDXMX)0ZmUY2neDt8 z8Y)Oj2D6`85&3o11&JX4m{wMvGtvZEe|dRHRXxQ2QyJv%1Ll7>FTVK4R z58?k}g6|&-A~G12fUQcUt^JI2c=uIdL3U1ZWJOADS8HB&bx=}6 zO>G5)|B^c~QdgZFUlbDX1@JFFM+9xmgZDq*D={WE((mN_YkW}k=kIZl`EPX#a{~k% z>o*=!9%dF6;Qs&KN!`Q2!ot(~A&#?)s*$3ksHl{Rn3SX@vw*aW_gFRX9-d!A4j-SpV?#?d9(NM*q!7S4uv3{!4uW zJ;3#S)7acrQa3!n3r|CwD7@GswK0vVpOssC_4GS*|JhstWSSV~NOd?n!JVp!>JR2vles+8kbUb8ucYDH> zlh)1k`Oejq?Eqhpf9pD5T3DOj|9y156%%@SwmC7-KMLmGGqMLj{-tHEuX|)^roC~n zqIPb&|GIyC>Y|{$sHPQIud*thkc6QTm$ca zW}K4&-a3I64wfp*Zy#)dWuX0Gny7KUaIh#l?z@K2TP+#EgLo9jA?sVGS* z((;JP>l*26>U!wvD{F{8*B0lc0{b7t|B6lXn1_;w6OTpc2`3yg2tadDs3>thrp6M| z)&M;K{0G?hFhG3)Yi5N0fRF}~jd+NOW$%nhiVpIREad15+iSlUet!zw+rDYOy}8^x z9^U$v2=@P>&5hmj^X~nm^0xND-InI@_4ek;&dtf@LCAbyMH*QD@0tGYo79xdqROO{ zZ^>zeJxu_59#;hNpGB3W^|_Ov1DNy8J1nx`i+6~(cVO_=e8893y02d%KP89w#h)L= zegDfpfx`pyEfT=~?EG5PLCn_B;59xu`2J})Te{j(Fi1!$n!k_+oew5z8Ce}SJtbvL zJ7pD_m(o&#j9~vmXQ7~Fgs0*cqGq9GWfS10Qr3UWPDR4YFTjh&$;@Q%4juuM5g85{ zo}UOF79F@R$ZXxg{>Oz+M2t!3VYjw?I%jg$ z4fel?^19lpyt(~Peok`e5B08z4vF)x{^tKR777{c|5Bz_rmiT^ z;QRN~#?#i$#uT3ztY6*TME9Mz_*+RCS`qO4O=VR7)<1PY{mbm7lsWK!02Vqe9Xk@e z2s*tmA1#B>3q2lQdLk}XEM6ugE>IQHe}jNXzz9digpP<#4rk?rga`r3bGX7mrtEwnbScaJyE z&kPJ#7R}BL9E=POmNfOGB^Q6m%B#yL=$**UNsG&DNUF=I$jOf`%Py_?5f;6;?Bf-d z{rO|a))ILBKf*tKjP;8ziwX=6xjF&A{})zPrUnQYVE+?;`sS4dCL#F#X}@+bf8$9) z;Nog&ts*NfFZ*1U;R*0x8EMKqeWs(KWb!BfFTkU%Ba2Bv$v{g*{alBYP2?dnB>!^9 zKS5=u5Pqoy_J0moEO;bXz`wC@MnxhP!Ggsh0}Lp5ekd+tEJBaB;Q0@){9fAx?(g$~ z#mj}|(8S||h4#};kndYR+ulyf0`LD_{y#SreE;VM8#_)XTPCK*JDck&CML^5Q;RA~ zvdi@#rvlTGV{OaqnqN@vYDn9vpgY#c{$Y(E5{{rhz3Xk%x{QA)+H1S(> z{O8y}C=~Gg)m|C7h?zJ!p~IWmn>=-}F_m<(up&lvb+ULNA+BhvE+-`}rX~BFnF{Rx zhI-1{Iw~p_p#QHW#HUHijm^x&_~-mbLQO@5A`JM)s>)#hN9JZ^&{cl|kH?9IkA-LY z_5lXDAPlLsGd=+#4I)s&5xUrr5Tbzm+uq{o)hB=O`=7d5UfNw;A6rmA}I3p`VZLu|MULWkQf?y zyq1z02K>Lzylz=MvI7hNF9z>Fa^U|MH*Q>Gzb?r^ML>c}X6X*~ zpEv;l2JIzW^y|dbgycy7Lrrs$my&#Ee*0%Y6x6@tn@cNer>igh!2a8Q9UZKnj~)JA zI~p9wd=L3=ujfn4bjRuE$uFfJVLxzwprd=by}hpVdrrm6w-qH><38e(Zz&S41SGN2Vv|`US-KzKjm^PYV4#1M#mg#mUvm(NP`lzn>xd z{pMbJ-d0{_q*wpQZ)g3?R^LudL*`HXf1+t@pmtZ@9QK0^6hy8;{I@swd;ceY6Y@VL zuc!nAorKWe{3iqX5BoX~4_x|Vao}7=BfW%6L5oIf?+W!V{bfwdt5Z$E2>xfe43g!*@RsH0#70QVRCYI z(k2SG)ue^Qw2d`vOq9L<;6KVKGcaDGWT9c8XQLNm(lLVi7eiEBK#KRSCiuSq9tr^p z5i>ObEZ0Pjm*pijGs{r_SH{Hq1xNhc0sqf_=?n3{%0E0hJw7BZ?KOh`@elspB~-vb zly@|9buu@3^2mDui3(l^`H2m24jJzu{cPPCR( z4UaU=d>Q{(0rD^RjFtK3ynR>N+Fe{&|GFWs7Ri5uU&=mYMF9R4?7x&g4~g*)Sy=Ur zNNY%XkzV7Q8u#kg64XCMq3JL3V&YTm|GEFVc)`y8iGkO{dp<7SPNbM%|LXAm^;Ade zriB9Bhq6j&YMDM&msQmFF;;r4C#$RKWd!k$7vdiY0~;3uHtUUxJRA%X9NO|i!gOdN z;`}#A1lefCuM-esv0_|CvoeGDCx&~0g4D?e>OZIdjRzRJ~u5aF(%|i zYO0)zouj)9T=$q+8d;cI*?PE|6O%hzI(yjp*cjSA@>bVnU{?Rbf6Kun4~_6$eRs9n z+KR~gxA-+!24do7CEyem0RC4|0d-lp0R;YAOg=sV4hccg>&AK_s05e zc3|DRimG=||7^Z^{_^kqD=I24JSB4LcS=lfNkCv?*wIPctB6v#??t;}XXR?}?7okM zy}5;=tC<-9ACO&kdSXZ*vDLJBKeK=2q4K3j9aR%afBIiK*LrKVK#9Zw(G-7Ib$G54N_qeJ#vy z1^%D1N(BEYq53Vt{|&c)l?AJC+E57ZKRJb&KOpJ7$t}tJ)>;bu&%YXg|Ge8b!PnnE zDD8Qu@9XAt|JMO2FJA^6tRwu7gXy8JHe7)J;_c1+)W*^i>VJg)Sj^SJ!coNLiR%-W zr+7Nrin7dn2>%&|hQS>r8d!yZ{+B}S&P_GL8!Wd4g#_%F`2={yIT(28*(`1d35(Ow zvGU$l;s^Xc7ubIw(89;KgoT1fDUXRqDo0C-W{OIQa)Ic{J%s;Sn+o{v-mlCrFK(_K z=FG0`XU?B3?i|KPov*F0R-Er_ZAWxB_p~%M4+l*)w{|p*!~P$_|DH7T-~GR3g#T*= z!N1ObkFda<*H?V1Dyb{jt*fhlR}z!d>l+dfAJ-Bb7W87h9l<}DMDV}UQX*qwV+Ui* z9V{%(-Pz0m?%&eemC?-k8NC0K3M2dnLO#xpP71Qf`^QC96&)-&6?IirSt^~oGDfl@ ztO)))v9UG}FWi3u^1%Zp&THoMtn@;jK>8`bAZ6f<;Qu&E5D?&EVgdFKrV=SrqVpal8Dx|3-uGztX$9x;mSC+!J&3eC7ks z0kr%G|I;N|SxseAB?A;iX*mS}ZiN5$ioU6qzV4mN+`xaqEyOA$Va3LDosolwflXHi z;Xij1W)lz>bk{Ow=) zGqTGvKjzgp9w`QSm~IbuyP3V2ucYEI9)`+#kz>1Mv8}r zMvje7LFM=m!T&bE!nuM^LQ6!Xvp#e5et+i2#u2=KoJZs#{10`o3^R9dw0BUpdA!%s zG!i+|*x5SKIPukQ8ZIT9zx6d%#7%eibjEl0_IDL$78S2n7Uvh|6fYM4x&Jx;R9aqE z_HOj#Ltg&-?U%6;iD3C*ttqdQ10z$K0-ndF!TLww@T<5a*oBO?LM40T=;X%!}#Z6%*lq;`2isIx&JLUD4-i#uA{1S8m60V{QY@*!Ga1R0R9}fwrs3{a^F49tvUchvDfZ+e#zWDe3uW)v4 z`{zz@^6c69N!H2g?EJyZOy|N5)PD)PE32E4gKd37o$W(C`PI$eN2mOnhCVlskGBlO zj}3nv=%~*{@E=#J^2=%q=Zo_Tifd|f&a?i!|9&ki&MVp~ih}uXC*Yrig-5o&_D_ZU zw~E|{j)o>gMI{f$ytFX0=eciw@3Gxek4xnD0sl+N9QcpLADi3xSo3@Tss98K{%6cP zF#l1$gQ_d9e^W<^QeH>SRGjCgsv4gIKNl+x0qbhq>T1k_OS4oDC&dAtI={CT>-c(U`G=lqo zdO3A2Hs%|=0C=x)n?+K9PWU>XC?f?Ok0dWPmn1$BIwqXEqgg*g!+`yt3#7D|!nlN_ zf@HWP1fJGpmtoy+WA^9H@cQBT#`*7_nT2!6|0jpLt0xDr|JHpp2*?QE8W-9p+olTg zzch9Yca)V+jt>kp?oWLBIMg=^FJtfCW#zBrzye%G-q+^phVt5@+z+4XKIX&wPgQA9 zWOpLWf8$<-1gEa81p6fi?)**z6v&$JS82&nXZtn|kKC0kU0m(3Dem2~eEPu2>zR#{ z_7g`ZcOOb}S4RhspGiaQrk;v|A}6!F^j&%FyV^R6!csc7RkUFLNmp8x^S}6C8cHmZ z%p&wGbS#Vpde?+FBm{)fQLbQG+7ghkU&0{2dJ)IL{3@ydj70FoP@o;4!lS6H@Q+>J zZ`(PU-CQ_C_FoTAcH_d&=SSBzegXgYe)FIE=-sB#3Aq1h?<}tR4iqr0(?cIi2HVev zyZQ^V-!``wRs`f`*SxDQEX=Ct|6Y@u0H@#;4aL=y! zGi7BXV>xA0YiZ;@__p8;Q+ZEqV-qbENp^MtOfG&_4h9S^Ne&5SdU_!eMi!y#x3u{= zXob1hSxxOPUS`3;#74v8#KgIP3H|?7RACC({Gz6Q`j`}79B*f1t95mL{^$C7sQGt<(%H}HO} zmTd=T6@DuI*wEAbHY>?L=SN)Ghj+F4c@u3_h54m%!-c-Dvx5Utl41WpG_E8eG3aGr zz~M@4NJUXdhu-4{vS#M4fd7eudEZ;f+|B#8*JJmm&Yt8b&JMSqx?U!-zonuicT-kb zMcU3tOA(4cQyn1|6EznTZ9Hrr6?!%v1{QWkB^kQwSloh~H-v3}2=GV<(+EgN zT*JV)=Yn^I<064E9R~$27TRTEnk%mNY0-tQUL+(Z=c3r#nVVgn2}+zjg8A3l+0yy> z!N|hQ!EsXhujRq@ z32$ria$)_u_EkmB>)NXFnon;=hs%m18jAeFgA@G{%i`0*SC;}`*N6K1r~3NB{_o*g zY)D~Lbg&01!9(}k77uKdA6QvDee{Ts>Y16MBV7G@dN}BKKA^p(tm7mjqjeMb?>zqA zM}z)FP30fnHPO{HFjj-ZHaTT3AyV$^u>Z%!#VDksCdP_&jfETd&j>{MI0ewJT&AYG zh>3$iKw#^QMa4jZibhF^L3Evfl8BIsig>{S#Rqr z2b${M24=!0e16`?8rb{{j`Uktk4&u#^nLNXC^-80&RkkhW>V~{xIDN4jW7rPTl7l~ zu6Lh3vWD|FduK0qcM=*OOLbeoKLRjdd3p}j+m6bBb)&_}ByarGL|*2Wwh+IGnuF2p zn}(7C>;j|=!klRA$bC1PhAaysrkEHVA2aYDNEw+5a!Z=oTp}bRL_^~seE1ZD2DY9^ z`Kc%3QT;7tHPd~AZNG;Hdq(Suhv1yqKd-PPGZpSTVg0|dZ=kxeXt=MoF5^Xg z-sD($U{dtl%8TH*S6_nR9y}x{K7Qv2?w_8QCcb_haJn69?(C*xZe#J_nXSn^nEzk! zbbRFYK*92ft(%9dBQ?2;jDeDlj0_96yy`u9Q$uM@V|@WW18oBl=>2XwsKEUz6SJ7S z90vn*evIle3>=ceEaH^ch=jRB@96M}0w)q7*@cT&9^6C6rNT$Cvq1y=SAJAtx2Gh8 zBs5|~xJ0vi`-}U5;XeWYcYOW)boEDc-1+J8_RebZ>w~?G)%4EAUnRW^{l>uGrFuCBDSysoI=Eqx=8+a|2M+0Bl8ngytG=7@1U)t75dd%yTfV_Y3G0ur>Ev(JeEF9$@*va1WeroUS>`qLJgnn-yA5-Xm=vcI5 zRRRC`4m-P|hA|JDv9_$7v5}65puUmHZ3iROYjkwH^a^quuJp^Y8V|KXF7ID3$j;0uyb(~6K2dpkk>+X(#EKYpG9|J7M! z;OW-t$kFWl+39LZ{PxM>x0c4%){(`If!4`)nV-8y=7y(-+a@Y%$3{Qz4-9f_>~izC9GXS_%a2@MO`I7xdMnw#pE z2p6z%@yV}|_fID%PApuXS=-s;qFX$)biDWM*)tb18do0&SLdg+9O_E9Z>cNTsK{$^ z@=B{1^9h>XxdZQCLi&0)^|Z`Yn6L4}{k!4=W)>a>4#9u!KTxk<5f_u+|I#~p}f47g7;17#br~zAF9TN zCO_5EB$H&S(R>uY;%&mup_-BNN zMZHMhSWoxc*-H&f42pPlbP|iie_JOsl!tEj9$;g^{KHJ$$Mc>Ka3H!|C42nH!^PN9 zQ%gx&TU|wkjYq*qMNUcPHoq9ufbu|!sw~c+t8QRVC&0kU&1l2P!pg$Pc*9&mQb-W? zA3Xs7&d3h<&o%$@pQ~KL#KMI89~mkdQWBT~7z$pYzDa$Vi146o*)RDVa# zhfmE1fd9Mr7yozmLw!U2r%yS*%PZ?@D?U^M|7T!N;ET|Z^x~jUaDm+9INz9{gx8UW z6TtsE_?Q2c+{IxTNLWns0v^3zo<(N`=5S=Yx_U)77v$K=2o)SXHHK5|LD`vA#k7V z9e+63>FWvK>}r~6?V4;E9i9jNyVAmm)`6sv_V1&Eqp_2HMTJF0RX^Y56~E8QDVWR5 zsOmoe{PV-V_~*asN+QA|A`-hoL!u&|2ZlALA^aB!Nh#5zhv)Gx;!_f%Q7+iIIbx7l zdwH37*jae__*gtJfrW7IXXM1LHa4CQ6_sHBol!;iuA!+Z7n95#dEo!h(KiwizNK!W zsdd|k`7$FvGnWs;Rdx@Bo(#w@#_80j7_T1j_i}2(1^UWV~(dl!0OECW)+uK_{T>P1lJN_Gm$a!x7=rkbAWO2&MWvbu_jdcgl-bjwi1NaLE!t!vC8LJU{M znWY$c=ovBp-uK^DW*2AY6~z_1OoBy7$WDNbXX8PM?0@`Q|M5{#0$i%M+GRxjo<&TdRHN1-r_^}xm5E~cq5B}NKp8w1A znEjoQl;~Gcrw38@T>$k1?td=gJ+-p4z{fW8_I~PadxhBA?Xf!*iM^wihLwi)ZBte{ zLj`$ha~X9beqlKyC0P-Er2l4JcuibIMVwJmQ=U$eo)w+-y5J2CcB4Do zT#{J0XcqRQBo{;}F)m`$P&;^`qFg~IK_}w8LQY7I?fqoy*UycGgY#g&--kbb{N7wY z*^Nn^UphZr-P-u^DtTkQw<$Aw=v&8^p6;&&1%UrOIP|%(Jg2qg+Zf>gpN&iv76JQn zMr+5%PdRz;{*_jdkz1MjW@IX_pyFfo`=XMV+4z2W|Yi-PbXrBC42!6Ll>`u{%JJX?wIKU>&c+Hc)j`f<9t zks7x4^Jw_X_E={tOaa?krl!xkrdozRx0TnoHBEL;Zw~d2Pi22@D=U3hmf7C<@!hAM z&bo?{%E5`;50$m?A1bOR$BP1eBO~YM;{0Ev1O~3n!2O4RT3S_{e?lCbm~O%Smy5lv zo~^qT^#52MHnv`7-kSFS+UmaZRlL9WjtVYHx;oM~_3V^PWDWFr#AWq$ZW+tz-8WX& zg8tVH@*nI!X$vxNaxvZj{2vKANeMcBLQzJ3Zbl;&UU3PGizJxBD5PXqSmgLj7YQzr zqB(m4J{S!S6*++*6(Qx?JW%xgJO3P=jf$N+*o#Y_-#P0)-aFhrUeCzyYi-&c1bN?O z0zPDC)6hg;e?@7_BwD@DP_Z=y|vPJl~<^56SMPc$+bN;G0A>Z>#~_+Bn}3v26pvpsW*Yioyx;r=J9 zr)Tr~Uv}sB_n;iwNb_57>Fyc+e9$~H**!YF&^*@JH1MstaqP?9=c5tu|N8W@?Bc?* zjJV8C)j2s0dBJb8%K_XiC-dXG)U2eE+M>dl^_M{}LSOpV`oaCvi;a2zxb#>4FEbM2 z!qbn>qf#SLFdZK}wsEkr!^ZQl@q7mR-$WN4I9lEFdT4Lp;^GMNUo~}AX6~CB2Bw;( zhL3LF)D^g)`1k(L%h&|=pG9Rgh3P~D;QbSiosENoLFev2_?|?PH?E7r{jbwyR1)F~ zl+=tEu<(sbiV9RPm~KyTs4n4?IVMfk%_kjW8LFZ9aCdxEkkW1<@I0wzW;BG_m_VjF8z!DnEXBy?%%Vs3KD9{%RklC zjrG6(%l}zfS-7~K5}x|{#p{$B|D>J6pqIh^u>YPIADeoP?_%ZbWae`3pZiCGs}}C} z%snk({y}TwVgKwA9k;gPZE0PHKm0c)JZ!T1G60OJCvr{AR9R0)`3@lupNPDMAcHsy z%MG}HQeqIK2f%A)0fSp?lDypf{KV)?7ZCryY;&I+n}!C3<^q{GlmL{}m!3M{U&SNX z+}vL5`MLAs2*H2;adPr9ZDxN7`v23;jiY6#e+OXyXYz0TpF7n)HreqH{^!REl@0beDn6hx8Z_@_r)J03SxW{f}X!x+)N2iPfCJ=x2;*< z1pkz@nE%%Q7l>fq_1N6Y!SJvCm1K-bsqTB(TRU4nQPZ%YXV#HbB!Qa^4lWZl4Fv!6 z8o!dEyz)&k2~Ax!EiO7PIav{Q0WmIdH5Dm%{}00YJBAtCPN%V^iI zu4AA>{|np?C`9C#Zmu}EB+tC?y}f>w`1>7H1w|e&pKMJ3hJk-{{LI1b;cwXgOWHi! zPk%e`x$AR#b8hBXM{`GG*DP?}eW+<2o}L_>HinE$;6{)@Quk`=F>})(Hn#Ab1Si&e|M0gZ5JQv8ZD6yR^ zP%cx#KExmVPvJlBp8<*R>bw3g|JS$EnUfzINntBzJHP*(|86vO^z@DvW>0ob4bQeg z{;$gHoF4wZ(mnOQEcJhzen_~^cY zf$x(-Uc^7&`5g@NzvzUxhWMznJr_$i9;=5ow&u1t=svLj?c(fiVW#Bn;p1sTi2?R+ zuA!-}_dx5W+FeCe6CX7-xc}FdRumRjyQ}$BSw;O8gP<^ryrvL?#6>A7CUjsu&@rXs zrzd0J;>Q*R8h9}?N5IEpA|^%=!$!kK0sa>f5*Kq!VzleH_{6+4S18u!`xfWc_5kQS zH1K@+={aB)xL4tY_a9%tb~kgDH}kYKv$4U)uyB2#{p`szXE)gYaCq|Yu9mu%qNKJT9hClCMK|873`F}t-emlOeze^cAr zU%!0**3;Sk>+_dh;6Hv}-8|H~1z%xcyk~G|;OoX%S!_{dNk(hWr^5P>tSqR3a|W8= z{X4Fz>OBmH)2a~u&zB3o5cvcAlf(Q1DpGv|QUYIxzl@JR*^EeWm4yR9sQ)yqY%HxU z&8!Jg&0+uD!^76o)d8LXp6IJEa6|mFR8lh0lr?;MOIlw*LJ8UbzNMpUXs4sc&LA$Q zBg_c==gf4>Y}8Uh0w}`QBmm!w?#@kKQP}^2?gv%)%7sf55AV}nq~M~ZA*Z;^a^Wft z)l&z8t2h#{|G&7py}3Bw`D69$?C0^z^x6HHk@=mIlikg|lbtnaXj=Oghnl*9|GlNV zX#()CTPFKq|Glzfs{J?IzyHJk@Tp_`V}5n$yUG&Se@uh=r#3HdVz8(tE+;rJ@MUOh zMo@D4+F@E~Kt*DFyl=|$iX{Jnyx(IDBGwems3)B5m z{}}Ozb25{np?*q-iq6uV!JfB|T3HzeTq#$o&Z z`R}=(-(Uuun6P@jIkvNQ($cck-QVAw+c4HUjoklD4E<_toE#kO?i(FzAL{RK94viX z5|Nu4BpL%)f1)Ji*2BdT43rWoAZ-X6a?83-6ysE(+>O8rlyu74O{9GUd8q ztSaxJs3d=1-oOakC53(f#w!H>2Mf&6 zNBW+PqL;ZhAU{5_@UU}nb*9F*Rk&%dt}5%Mct=@D6Y!6CZ^+(O^3d0Gy=`pjZm1{8 z0`lWC-@ssFVdZ5*-oF`9Md&e+{AZ}ocAcGA@+vFdWnmm*VmzDsSFy<%@h^%{qq0yS zNGMO;!Ttqie}7$E{jswd5(N5h+&pO7-9K8~+MD~C9I^a+W$^6#&PY>V-|)}RZM|zf z)7@Xc4VC4z^)(%he%=^rpBOkPE-nN>|6-Vbbv8BR)%aKB)YO&?PXp>}Vtrn5Rqf0o z_@KyM0J4?3*Zf=k$7m zl^6wC&;SaE$3TN!k`0fG@9I^?D};Dx*iTSaYmW`3xg(PC+u~Vj`0MHy!C~P6 zS&8ZKe(8Q`1?lPWQNaI&O6+Lvq<;?v-cM`{Js-FO`x6Nk_`jNuwXL(Gt%8-Dw6?0g z3EOo;O#>gbJ97HwN{afjx~9@%f*`*gFXTTGc2;&4sQ(otImOWg#Nhq+mI&&BJK zoZJ}Zc7&*xh43-q_fP-hgGG3OiHw{03OUiGOTIDNi(4Zr^K)xw9Y6rI`lIQ1Wjj7- z9@&39-#`Aj+uqyS)i?0R|IbHzn}>IR0&{A5boyjyY^r~Jys-oPKcVbxZDD6C=>JuA z$=kQ^{`sM9^jltaenf#^eB#Tz@DTsy1MvUqsIE ztJX7HZB2D;8D>s>4In)8R#sJn{#Qxxx`OF#O>e+IczBDONmf~eo1G7xg&X0&(=uid zlVHS@6y@OlbN_Rj7@Ld?1?MuhwI%HQljERZ(TY%`U=g^x;Q(#MWy))ti@^V|w-+36 zG`H|`_GjDP*8cute9SyRqNXNo?CtlxDeUX*9@%PaoccPA#K6)|jpMV^W0NDjy|vXn zeOd3CTB_dV6vAonctd7Uc16z1w`G8D8dj1~KQUHR5+4;5^lD)tE+usJ2jBzOrpF{F z`0o7*2v16Wd9o4_W^QLCZEJSl*4)Yt0KaT49zQ0!VrA}r-T zuNUT4PmcE|&$riy-n{SY`_|mrvfBD(8s7iwn&AF(ynkn8Y-4C)kkCYB zGg~Wbz<;>_U=OnQR3F0oA2Fdh%>T&=9pL_#Pg_GyRh5}RPg>c8lg&_3QI20w>F!+x z;)~a>Y3Xa}DAP0Yu*k~@@Gx_!%84>Da$rb_v+CLYGt|qV1I3SCdB`2{_F11YCjyqt;eU$9gOW9ZA`4K zfBv4C-Pif!Yj5LJLB{CkrRgTve{KW+hqs==&Hhfy(S~@dFo*Q zo9xO@wPO=`#SOJ#0WatFqT_;sQoKY4@-scml^?-=eB0H)@B!R zy`MPPTM&@mxAO8bzd~jE=lxgHPRr1UkxfTWMNwVRM*|pe3P*N=T|I z3Cbvn+z?_^Qj%a|xFO0(D*y^Tytk3QRf>}=(vaL>ZV6Z?WU@W047dAbv$ zo85PQsPWj*!{uN68(v)_bpZT>`5)hPC21WcgIhQ6igBuF8foyr{WA%Zkf0QcD6hJ- zD364wFd04I|MCk98tHIKUBkxU!X;!vLGEAfQ{Y~}B$1-V5+)=>>R%TeDxxd%Gy97H zzFUj)C!1lR$D4BtyIHX@a|g%kCwZx&J^augWg5&W0)$#Gb0`r1Cw4#m;??4~ats)|1prR8*et*Pm4e4CT~ zyF4qaq_(`cq8{*{W}8zx;-3TU_3QK(FPg*sU;FuvMkl8vzl<9RkBLdJu(onUr}6Q2 za%5JNVR-n6&+rbr#}h8gM~o`US~o23-=P;~Na3MpXA|b&V&E3x6_FC)5fl&=VZ44r zNP%Sr5)l}l62%#m@G90nQNYhAJ}v%LRXT3b z+rq4doHzM-zV8Y%i-L-rvIO3I_*5DDNuyY+xUQx~W0<_@PV?uE_O-84qt_>Ui6=)! z3_s(IC{9npf!qWq9M&z_Y~NUP+gx8*`?^j!x6OaPwf$_4@zC=0_}AI=g*~zZ*igV? z$2p@pr0GE;qb0&4oy8!$LXJVcj6X@t%F7_m#K6WX#Lmsk#m~bg2?Q_K#D(d_W_dsI zit$OA$tY;ZKTwobRIyi6lheGVee15C=1m<#1E)J6zk;ci!#y9%d)8KNHg@*`Jm1>e z>86XzGdEq2x`&?5UIyOs(SZ>Gxxp@}kpW>v;ZI*BrpH9aKYac&AvrKLHTHE#2FPE6 z$X|=dUs+t$P?lY#TIXD0Sz23Gmsji1LoH5}bMT^>Gm@_2IZDadc(6CCBA z4a6P6g)cz=IoZ|x^{C(H%h1^H{D@mG=>ONW)BfVxT>HH8_U6)R>$=<_ z$ltPaXKwrVVbAf+)uXL*VU$8rv`d(5*tt}=7x1L;>#;z7X0mF08WKt-suFC_zX)>* zd%kI0`MCK)7^{=)BNX-r&7d{tt4 zVoLCpIdsqOnfn%vYF^^ z>KW?&Ha*ZfG|=_!>v!_r@xiH{8Ib>UVRmJ?*G}{+(v}POHP1$mhZFk*;ynjvH_w zGIn(}=$Cl#uu%yGh=

    MF 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 O;QxgC*ReWRxBmk>9Qkbk literal 0 HcmV?d00001 diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py new file mode 100644 index 00000000..21a24cb7 --- /dev/null +++ b/tests/testDalvikDisassembler.py @@ -0,0 +1,185 @@ +#!/usr/bin/python + +import logging +import os +import tempfile +import unittest + +from smda.common.SmdaReport import SmdaReport +from smda.Disassembler import Disassembler +from smda.utility.DexFileLoader import DexFileLoader + +from .context import config + +LOG = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format="%(asctime)-15s %(message)s") +logging.disable(logging.CRITICAL) + + +class DalvikDisassemblerTestSuite(unittest.TestCase): + """Test suite for Dalvik/DEX disassembly using a real DEX file (blockblast classes.dex)""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Load and decrypt the XOR'd DEX fixture + with open(os.path.join(config.PROJECT_ROOT, "tests", "blockblast_classes_xored"), "rb") as f_binary: + binary = f_binary.read() + decrypted_dex = bytearray() + for index, byte in enumerate(binary): + if isinstance(byte, str): + byte = ord(byte) + decrypted_dex.append(byte ^ (index % 256)) + cls.dex_binary = bytes(decrypted_dex) + + # Write decrypted DEX to temp file for file-based disassembly + with tempfile.NamedTemporaryFile(suffix=".dex", delete=False) as tmp: + tmp.write(cls.dex_binary) + cls._temp_file_name = tmp.name + + # File-based disassembly (primary working path, matches CLI: python analyze.py -p -r dalvik ) + disasm = Disassembler(config, backend="dalvik") + cls.file_disassembly = disasm.disassembleFile(cls._temp_file_name) + + # Buffer-based disassembly + disasm_buf = Disassembler(config, backend="dalvik") + cls.buffer_disassembly = disasm_buf.disassembleUnmappedBuffer(cls.dex_binary) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + os.unlink(cls._temp_file_name) + + def testDexFormatDetection(self): + """DexFileLoader.isCompatible() correctly identifies DEX files""" + assert DexFileLoader.isCompatible(self.dex_binary) is True + assert DexFileLoader.getBaseAddress(self.dex_binary) == 0 + assert DexFileLoader.getBitness(self.dex_binary) == 32 + assert DexFileLoader.getArchitecture(self.dex_binary) == "dalvik" + assert DexFileLoader.getAbi(self.dex_binary) == "" + # Non-DEX data should be rejected + assert DexFileLoader.isCompatible(b"MZ\x90\x00") is False + assert DexFileLoader.isCompatible(b"") is False + assert DexFileLoader.isCompatible(b"\x7fELF") is False + + def testFileDisassemblyStatus(self): + """File-based disassembly completes successfully""" + self.assertEqual(self.file_disassembly.status, "ok") + self.assertEqual(self.file_disassembly.message, "Analysis finished regularly.") + + def testFileDisassemblyArchitecture(self): + """Architecture is 'dalvik', bitness 32, base_addr 0""" + self.assertEqual(self.file_disassembly.architecture, "dalvik") + self.assertEqual(self.file_disassembly.bitness, 32) + self.assertEqual(self.file_disassembly.base_addr, 0) + + def testFileDisassemblyHashes(self): + """SHA256/SHA1/MD5 match expected values for the DEX file""" + self.assertEqual( + self.file_disassembly.sha256, + "70f65a5dc2d9eea731effe48acbbfdd2f1a7efe151b647f30e4a124691fcdc30", + ) + self.assertEqual( + self.file_disassembly.sha1, + "8241e12361e920e09e7cf1c6f2a95dc30a4609c3", + ) + self.assertEqual( + self.file_disassembly.md5, + "92bdf8fc9165fd128d6b4de076530a0d", + ) + + def testFileDisassemblyFunctionRecovery(self): + """Correct number of functions recovered from DEX""" + self.assertEqual(self.file_disassembly.num_functions, 2219) + functions = list(self.file_disassembly.getFunctions()) + self.assertEqual(len(functions), 2219) + + def testFileDisassemblyInstructionCount(self): + """Total instruction count matches expected""" + self.assertEqual(self.file_disassembly.num_instructions, 9593) + + def testFileDisassemblyBlockCount(self): + """Total block count matches expected""" + self.assertEqual(self.file_disassembly.num_blocks, 2219) + + def testDalvikFunctionNames(self): + """All functions have Dalvik-style names (Lclass;->method)""" + functions = list(self.file_disassembly.getFunctions()) + named_functions = [f for f in functions if f.function_name] + self.assertEqual(len(named_functions), 2219) + # All function names should contain '->' (class->method separator) + arrow_functions = [f for f in functions if "->" in f.function_name] + self.assertEqual(len(arrow_functions), 2219) + # All function names should start with 'L' (Dalvik class descriptor) + l_prefix_functions = [f for f in functions if f.function_name.startswith("L")] + self.assertEqual(len(l_prefix_functions), 2219) + + def testFileDisassemblyStatistics(self): + """DisassemblyStatistics fields are correct""" + stats = self.file_disassembly.statistics + self.assertEqual(stats.num_functions, 2219) + self.assertEqual(stats.num_recursive_functions, 0) + self.assertEqual(stats.num_leaf_functions, 2219) + self.assertEqual(stats.num_basic_blocks, 2219) + self.assertEqual(stats.num_instructions, 9593) + self.assertEqual(stats.num_api_calls, 0) + self.assertEqual(stats.num_function_calls, 0) + self.assertEqual(stats.num_failed_functions, 0) + self.assertEqual(stats.num_failed_instructions, 0) + + def testMarshallingRoundTrip(self): + """toDict() -> fromDict() produces equivalent report""" + report_dict = self.file_disassembly.toDict() + # Validate key fields in dict + self.assertEqual(report_dict["status"], "ok") + self.assertEqual(report_dict["architecture"], "dalvik") + self.assertEqual(report_dict["base_addr"], 0) + self.assertEqual(report_dict["binary_size"], 247668) + self.assertEqual(report_dict["bitness"], 32) + self.assertEqual( + report_dict["sha256"], + "70f65a5dc2d9eea731effe48acbbfdd2f1a7efe151b647f30e4a124691fcdc30", + ) + self.assertEqual(report_dict["statistics"]["num_instructions"], 9593) + self.assertEqual(report_dict["statistics"]["num_functions"], 2219) + self.assertEqual(len(report_dict["xcfg"]), 2219) + # Reconstruct from dict + reconstructed = SmdaReport.fromDict(report_dict) + self.assertEqual(reconstructed.status, "ok") + self.assertEqual(reconstructed.architecture, "dalvik") + self.assertEqual(reconstructed.num_functions, 2219) + self.assertEqual(reconstructed.num_instructions, 9593) + self.assertEqual(reconstructed.num_blocks, 2219) + self.assertEqual( + reconstructed.sha256, + "70f65a5dc2d9eea731effe48acbbfdd2f1a7efe151b647f30e4a124691fcdc30", + ) + + def testBufferDisassembly(self): + """Buffer-based disassembly via disassembleUnmappedBuffer""" + self.assertEqual(self.buffer_disassembly.status, "ok") + self.assertEqual(self.buffer_disassembly.architecture, "dalvik") + self.assertEqual(self.buffer_disassembly.bitness, 32) + self.assertEqual(self.buffer_disassembly.base_addr, 0) + + def testFunctionProperties(self): + """Individual function properties are well-formed""" + functions = list(self.file_disassembly.getFunctions()) + for fn in functions[:10]: + self.assertIsInstance(fn.offset, int) + self.assertGreater(fn.offset, 0) + self.assertGreaterEqual(fn.num_blocks, 1) + self.assertGreaterEqual(fn.num_instructions, 1) + self.assertIsInstance(fn.apirefs, dict) + self.assertIsInstance(fn.blockrefs, dict) + self.assertIsInstance(fn.inrefs, list) + self.assertIsInstance(fn.outrefs, dict) + + def testBinarySize(self): + """binary_size matches the actual DEX file size""" + self.assertEqual(self.file_disassembly.binary_size, 247668) + self.assertIsNone(self.file_disassembly.oep) + + +if __name__ == "__main__": + unittest.main() From 2ab6055b79695db8ba2e11063a0ac07576481ea8 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:23:55 +0530 Subject: [PATCH 10/25] Apply suggestions from code review Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- smda/dalvik/DalvikDisassembler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 03ab1dff..0a2b5372 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -327,7 +327,7 @@ def _resolveSwitchTargets(self, bytecode, switch_insn_idx, payload_idx): Returns: A list of absolute byte offsets (relative to bytecode start) for each target. """ - if payload_idx + 2 > len(bytecode): + if payload_idx < 0 or payload_idx + 2 > len(bytecode): return [] ident = struct.unpack_from(" len(bytecode): @@ -512,7 +512,9 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): # Fallback to in-memory list() conversion for raw buffers try: dex_file = lief.DEX.parse(binary_info.raw_data) - except TypeError: + except (TypeError, Exception): + dex_file = None + if not dex_file: dex_file = lief.DEX.parse(list(binary_info.raw_data)) if dex_file: From d42f1050eb8b32c37134c4adcb56458eadf0606f Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:08:03 +0530 Subject: [PATCH 11/25] fix(dalvik): simplify in-memory LIEF buffer parsing to always use list() Remove the redundant try/except fallback that attempted lief.DEX.parse(bytes) before falling through to list(). Since LIEF silently treats bytes as a filename string and returns None without raising TypeError, the fallback was unreliable. Using list() directly is the correct and only safe path for raw in-memory DEX parsing, matching LIEF's C++ raw buffer overload. Also expand testBufferDisassembly assertions to validate function, instruction, and block counts now that disassembleUnmappedBuffer correctly returns results. Co-Authored-By: Claude Sonnet 4.6 --- smda/dalvik/DalvikDisassembler.py | 10 +++------- tests/testDalvikDisassembler.py | 5 ++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 0a2b5372..f08e0333 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -509,13 +509,9 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): dex_file = lief.DEX.parse(binary_info.file_path) if not dex_file: - # Fallback to in-memory list() conversion for raw buffers - try: - dex_file = lief.DEX.parse(binary_info.raw_data) - except (TypeError, Exception): - dex_file = None - if not dex_file: - dex_file = lief.DEX.parse(list(binary_info.raw_data)) + # Always use list() for in-memory parsing — lief.DEX.parse(bytes) silently + # treats bytes as a filename string and returns None without raising TypeError. + dex_file = lief.DEX.parse(list(binary_info.raw_data)) if dex_file: for method in dex_file.methods: diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index 21a24cb7..b5ff1862 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -156,11 +156,14 @@ def testMarshallingRoundTrip(self): ) def testBufferDisassembly(self): - """Buffer-based disassembly via disassembleUnmappedBuffer""" + """Buffer-based disassembly via disassembleUnmappedBuffer produces correct results""" self.assertEqual(self.buffer_disassembly.status, "ok") self.assertEqual(self.buffer_disassembly.architecture, "dalvik") self.assertEqual(self.buffer_disassembly.bitness, 32) self.assertEqual(self.buffer_disassembly.base_addr, 0) + self.assertEqual(self.buffer_disassembly.num_functions, 2219) + self.assertEqual(self.buffer_disassembly.num_instructions, 9593) + self.assertEqual(self.buffer_disassembly.num_blocks, 2219) def testFunctionProperties(self): """Individual function properties are well-formed""" From ef7c28224fcb5a34849584e6db6fe66a3e1f5d69 Mon Sep 17 00:00:00 2001 From: r0ny123 Date: Fri, 10 Apr 2026 10:20:22 +0530 Subject: [PATCH 12/25] feat(dalvik): harden DEX decoding and analysis coverage --- smda/Disassembler.py | 12 + smda/DisassemblyResult.py | 1 + smda/common/BinaryInfo.py | 2 + smda/common/SmdaFunction.py | 19 +- smda/common/SmdaInstruction.py | 9 + smda/common/SmdaReport.py | 8 + smda/dalvik/DalvikDisassembler.py | 911 +++++++++++---------- smda/dalvik/DalvikFunctionAnalysisState.py | 17 +- smda/dalvik/DalvikOpcodeDecoder.py | 628 ++++++++++++++ smda/utility/DexFileLoader.py | 63 +- tests/testDalvikDisassembler.py | 217 ++--- 11 files changed, 1347 insertions(+), 540 deletions(-) create mode 100644 smda/dalvik/DalvikOpcodeDecoder.py diff --git a/smda/Disassembler.py b/smda/Disassembler.py index e890d4d2..647a43ef 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -60,6 +60,18 @@ def _callbackAnalysisTimeout(self): def _addStringsToReport(self, smda_report, buffer, mode=None): smda_report.buffer = buffer for smda_function in smda_report.getFunctions(): + if smda_report.architecture == "dalvik" and smda_function.stringrefs: + if isinstance(smda_function.stringrefs, dict): + smda_function.stringrefs = [ + { + "string": string_value, + "ins_addr": referencing_addr, + "data_addr": referencing_addr, + "type": "dex", + } + for referencing_addr, string_value in sorted(smda_function.stringrefs.items()) + ] + continue function_strings = [] for string_result in extract_strings(smda_function, mode=mode): string, referencing_addr, string_addr, string_type = string_result diff --git a/smda/DisassemblyResult.py b/smda/DisassemblyResult.py index 0210f5fc..1b39dd95 100644 --- a/smda/DisassemblyResult.py +++ b/smda/DisassemblyResult.py @@ -24,6 +24,7 @@ def __init__(self): self.exported_functions = set() self.failed_analysis_addr = [] self.function_borders = {} + self.function_metadata = {} # stored as key: int(i.address) = (i.size, i.mnemonic, i.op_str) self.instructions = {} self.ins2fn = {} diff --git a/smda/common/BinaryInfo.py b/smda/common/BinaryInfo.py index b65a72de..a1b0205e 100644 --- a/smda/common/BinaryInfo.py +++ b/smda/common/BinaryInfo.py @@ -139,4 +139,6 @@ def getHeaderBytes(self): return self.raw_data[:0x400] elif isinstance(lief_result, lief.ELF.Binary): return self.raw_data[:0x40] + elif self.architecture == "dalvik" or self.raw_data[:4] == b"dex\n": + return self.raw_data[:0x70] return None diff --git a/smda/common/SmdaFunction.py b/smda/common/SmdaFunction.py index c113ae9c..163ff1d3 100644 --- a/smda/common/SmdaFunction.py +++ b/smda/common/SmdaFunction.py @@ -24,6 +24,7 @@ class SmdaFunction: code_inrefs = None code_outrefs = None is_exported = None + architecture_metadata = None # metadata binweight = 0 characteristics = "" @@ -45,6 +46,7 @@ def __init__(self, disassembly=None, function_offset=None, config=None, smda_rep self.inrefs = disassembly.getInRefs(function_offset) self.outrefs = disassembly.getOutRefs(function_offset) self.is_exported = self.offset in disassembly.exported_functions + self.architecture_metadata = disassembly.function_metadata.get(function_offset, {}) # metadata self.function_name = disassembly.function_symbols.get(function_offset, "") self.characteristics = ( @@ -62,7 +64,14 @@ def __init__(self, disassembly=None, function_offset=None, config=None, smda_rep if function_offset in disassembly.candidates else None ) - if config and config.WITH_STRINGS: + if ( + config + and config.WITH_STRINGS + or ( + disassembly.binary_info.architecture == "dalvik" + and disassembly.getStringRefsForFunction(function_offset) + ) + ): self.stringrefs = disassembly.getStringRefsForFunction(function_offset) if config and config.CALCULATE_HASHING: self.pic_hash = self.getPicHash(disassembly.binary_info) @@ -93,10 +102,16 @@ def num_instructions(self): @property def num_calls(self): + architecture = self.smda_report.architecture if self.smda_report else "" + if architecture == "dalvik": + return sum([1 for ins in self.getInstructions() if ins.mnemonic.startswith("invoke-")]) return sum([1 for ins in self.getInstructions() if ins.mnemonic == "call"]) @property def num_returns(self): + architecture = self.smda_report.architecture if self.smda_report else "" + if architecture == "dalvik": + return sum([1 for ins in self.getInstructions() if ins.mnemonic.startswith("return")]) return sum([1 for ins in self.getInstructions() if ins.mnemonic in ["ret", "retn"]]) def isApiThunk(self): @@ -229,6 +244,7 @@ def fromDict(cls, function_dict, binary_info=None, version=None, smda_report=Non smda_function.outrefs = {int(k): v for k, v in function_dict["outrefs"].items()} # provide some legacy support by assuming functions are not exported for SMDA reports < 1.7.0 smda_function.is_exported = function_dict.get("is_exported", False) + smda_function.architecture_metadata = function_dict.get("architecture_metadata", {}) smda_function.binweight = function_dict["metadata"]["binweight"] smda_function.characteristics = function_dict["metadata"]["characteristics"] smda_function.confidence = function_dict["metadata"]["confidence"] @@ -278,6 +294,7 @@ def toDict(self) -> dict: "inrefs": self.inrefs, "outrefs": self.outrefs, "is_exported": self.is_exported, + "architecture_metadata": self.architecture_metadata if self.architecture_metadata is not None else {}, "metadata": { "binweight": self.binweight, "characteristics": self.characteristics, diff --git a/smda/common/SmdaInstruction.py b/smda/common/SmdaInstruction.py index 257f8bb8..bb305ff7 100644 --- a/smda/common/SmdaInstruction.py +++ b/smda/common/SmdaInstruction.py @@ -25,6 +25,15 @@ def __init__(self, ins_list=None, smda_function=None): self.operands = ins_list[3] def getDataRefs(self): + explicit_refs = set() + smda_report = self.smda_function.smda_report + if hasattr(smda_report, "data_refs_from") and self.offset in smda_report.data_refs_from: + explicit_refs.update(smda_report.data_refs_from[self.offset]) + yield from sorted(explicit_refs) + if explicit_refs: + return + if smda_report.architecture != "intel": + return if self.getMnemonicGroup(IntelInstructionEscaper) != "C": detailed = self.getDetailed() if len(detailed.operands) > 0: diff --git a/smda/common/SmdaReport.py b/smda/common/SmdaReport.py index c41246b0..5f03c79b 100644 --- a/smda/common/SmdaReport.py +++ b/smda/common/SmdaReport.py @@ -50,6 +50,8 @@ class SmdaReport: version = None xcfg = None xheader = None + data_refs_from = None + data_refs_to = None # on first usage, initialize codexrefs objects for all functions based on inrefs/outrefs (requires knowledge about all functions) _has_codexrefs = False @@ -92,6 +94,8 @@ def __init__(self, disassembly=None, config=None, buffer=None): self.version = disassembly.binary_info.version self.xcfg = self._convertCfg(disassembly, config=config) self.xheader = disassembly.binary_info.getHeaderBytes() + self.data_refs_from = {src: sorted(dst) for src, dst in disassembly.data_refs_from.items()} + self.data_refs_to = {dst: sorted(src) for dst, src in disassembly.data_refs_to.items()} self.xmetadata = { "exported_functions": disassembly.binary_info.getExportedFunctions(), "imported_functions": disassembly.binary_info.getImportedFunctions(), @@ -262,6 +266,8 @@ def fromDict(cls, report_dict) -> Optional["SmdaReport"]: smda_report.statistics = DisassemblyStatistics.fromDict(report_dict["statistics"]) smda_report.status = report_dict["status"] smda_report.timestamp = datetime.datetime.strptime(report_dict["timestamp"], "%Y-%m-%dT%H-%M-%S") + smda_report.data_refs_from = {int(k): v for k, v in report_dict.get("data_refs_from", {}).items()} + smda_report.data_refs_to = {int(k): v for k, v in report_dict.get("data_refs_to", {}).items()} binary_info = BinaryInfo(b"") binary_info.architecture = smda_report.architecture binary_info.abi = smda_report.abi @@ -321,6 +327,8 @@ def toDict(self) -> dict: "timestamp": self.timestamp.strftime("%Y-%m-%dT%H-%M-%S"), "xcfg": {function_addr: smda_function.toDict() for function_addr, smda_function in self.xcfg.items()}, "xheader": self.xheader.hex() if self.xheader else "", + "data_refs_from": self.data_refs_from if self.data_refs_from is not None else {}, + "data_refs_to": self.data_refs_to if self.data_refs_to is not None else {}, "xmetadata": self.xmetadata, } diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index f08e0333..957a6bd1 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -6,530 +6,587 @@ import lief from smda.dalvik.DalvikFunctionAnalysisState import DalvikFunctionAnalysisState +from smda.dalvik.DalvikOpcodeDecoder import ( + decode_instruction, + parse_code_item_header, + read_sleb128, + read_uleb128, +) from smda.DisassemblyResult import DisassemblyResult +from smda.utility.DexFileLoader import DexFileLoader LOGGER = logging.getLogger(__name__) -# Complete lookup table for Dalvik opcodes per the Android Dalvik bytecode specification. -# Each entry maps opcode -> (mnemonic, size_in_16bit_code_units). -# Reference: https://source.android.com/docs/core/runtime/dalvik-bytecode -DALVIK_OPCODES = { - # Moves and misc (0x00-0x0D) - 0x00: ("nop", 1), - 0x01: ("move", 1), - 0x02: ("move/from16", 2), - 0x03: ("move/16", 3), - 0x04: ("move-wide", 1), - 0x05: ("move-wide/from16", 2), - 0x06: ("move-wide/16", 3), - 0x07: ("move-object", 1), - 0x08: ("move-object/from16", 2), - 0x09: ("move-object/16", 3), - 0x0A: ("move-result", 1), - 0x0B: ("move-result-wide", 1), - 0x0C: ("move-result-object", 1), - 0x0D: ("move-exception", 1), - # Returns (0x0E-0x11) - 0x0E: ("return-void", 1), - 0x0F: ("return", 1), - 0x10: ("return-wide", 1), - 0x11: ("return-object", 1), - # Constants (0x12-0x1C) - 0x12: ("const/4", 1), - 0x13: ("const/16", 2), - 0x14: ("const", 3), - 0x15: ("const/high16", 2), - 0x16: ("const-wide/16", 2), - 0x17: ("const-wide/32", 3), - 0x18: ("const-wide", 5), - 0x19: ("const-wide/high16", 2), - 0x1A: ("const-string", 2), - 0x1B: ("const-string/jumbo", 3), - 0x1C: ("const-class", 2), - # Monitor and type checks (0x1D-0x23) - 0x1D: ("monitor-enter", 1), - 0x1E: ("monitor-exit", 1), - 0x1F: ("check-cast", 2), - 0x20: ("instance-of", 2), - 0x21: ("array-length", 1), - 0x22: ("new-instance", 2), - 0x23: ("new-array", 2), - # Filled arrays (0x24-0x26) - 0x24: ("filled-new-array", 3), - 0x25: ("filled-new-array/range", 3), - 0x26: ("fill-array-data", 3), - # Throw (0x27) - 0x27: ("throw", 1), - # Gotos (0x28-0x2A) - 0x28: ("goto", 1), - 0x29: ("goto/16", 2), - 0x2A: ("goto/32", 3), - # Switches (0x2B-0x2C) - 0x2B: ("packed-switch", 3), - 0x2C: ("sparse-switch", 3), - # Comparisons (0x2D-0x31) - 0x2D: ("cmpl-float", 2), - 0x2E: ("cmpg-float", 2), - 0x2F: ("cmpl-double", 2), - 0x30: ("cmpg-double", 2), - 0x31: ("cmp-long", 2), - # If-test two registers (0x32-0x37) - 0x32: ("if-eq", 2), - 0x33: ("if-ne", 2), - 0x34: ("if-lt", 2), - 0x35: ("if-ge", 2), - 0x36: ("if-gt", 2), - 0x37: ("if-le", 2), - # If-testz single register (0x38-0x3D) - 0x38: ("if-eqz", 2), - 0x39: ("if-nez", 2), - 0x3A: ("if-ltz", 2), - 0x3B: ("if-gez", 2), - 0x3C: ("if-gtz", 2), - 0x3D: ("if-lez", 2), - # (0x3E-0x43 are unused) - # Array operations (0x44-0x51) - format 23x, 2 code units - 0x44: ("aget", 2), - 0x45: ("aget-wide", 2), - 0x46: ("aget-object", 2), - 0x47: ("aget-boolean", 2), - 0x48: ("aget-byte", 2), - 0x49: ("aget-char", 2), - 0x4A: ("aget-short", 2), - 0x4B: ("aput", 2), - 0x4C: ("aput-wide", 2), - 0x4D: ("aput-object", 2), - 0x4E: ("aput-boolean", 2), - 0x4F: ("aput-byte", 2), - 0x50: ("aput-char", 2), - 0x51: ("aput-short", 2), - # Instance field operations (0x52-0x5F) - format 22c, 2 code units - 0x52: ("iget", 2), - 0x53: ("iget-wide", 2), - 0x54: ("iget-object", 2), - 0x55: ("iget-boolean", 2), - 0x56: ("iget-byte", 2), - 0x57: ("iget-char", 2), - 0x58: ("iget-short", 2), - 0x59: ("iput", 2), - 0x5A: ("iput-wide", 2), - 0x5B: ("iput-object", 2), - 0x5C: ("iput-boolean", 2), - 0x5D: ("iput-byte", 2), - 0x5E: ("iput-char", 2), - 0x5F: ("iput-short", 2), - # Static field operations (0x60-0x6D) - format 21c, 2 code units - 0x60: ("sget", 2), - 0x61: ("sget-wide", 2), - 0x62: ("sget-object", 2), - 0x63: ("sget-boolean", 2), - 0x64: ("sget-byte", 2), - 0x65: ("sget-char", 2), - 0x66: ("sget-short", 2), - 0x67: ("sput", 2), - 0x68: ("sput-wide", 2), - 0x69: ("sput-object", 2), - 0x6A: ("sput-boolean", 2), - 0x6B: ("sput-byte", 2), - 0x6C: ("sput-char", 2), - 0x6D: ("sput-short", 2), - # Invoke operations (0x6E-0x72) - format 35c, 3 code units - 0x6E: ("invoke-virtual", 3), - 0x6F: ("invoke-super", 3), - 0x70: ("invoke-direct", 3), - 0x71: ("invoke-static", 3), - 0x72: ("invoke-interface", 3), - # (0x73 is unused) - # Invoke range operations (0x74-0x78) - format 3rc, 3 code units - 0x74: ("invoke-virtual/range", 3), - 0x75: ("invoke-super/range", 3), - 0x76: ("invoke-direct/range", 3), - 0x77: ("invoke-static/range", 3), - 0x78: ("invoke-interface/range", 3), - # (0x79-0x7A are unused) - # Unary operations (0x7B-0x8F) - format 12x, 1 code unit - 0x7B: ("neg-int", 1), - 0x7C: ("not-int", 1), - 0x7D: ("neg-long", 1), - 0x7E: ("not-long", 1), - 0x7F: ("neg-float", 1), - 0x80: ("neg-double", 1), - 0x81: ("int-to-long", 1), - 0x82: ("int-to-float", 1), - 0x83: ("int-to-double", 1), - 0x84: ("long-to-int", 1), - 0x85: ("long-to-float", 1), - 0x86: ("long-to-double", 1), - 0x87: ("float-to-int", 1), - 0x88: ("float-to-long", 1), - 0x89: ("float-to-double", 1), - 0x8A: ("double-to-int", 1), - 0x8B: ("double-to-long", 1), - 0x8C: ("double-to-float", 1), - 0x8D: ("int-to-byte", 1), - 0x8E: ("int-to-char", 1), - 0x8F: ("int-to-short", 1), - # Binary operations (0x90-0xAF) - format 23x, 2 code units - 0x90: ("add-int", 2), - 0x91: ("sub-int", 2), - 0x92: ("mul-int", 2), - 0x93: ("div-int", 2), - 0x94: ("rem-int", 2), - 0x95: ("and-int", 2), - 0x96: ("or-int", 2), - 0x97: ("xor-int", 2), - 0x98: ("shl-int", 2), - 0x99: ("shr-int", 2), - 0x9A: ("ushr-int", 2), - 0x9B: ("add-long", 2), - 0x9C: ("sub-long", 2), - 0x9D: ("mul-long", 2), - 0x9E: ("div-long", 2), - 0x9F: ("rem-long", 2), - 0xA0: ("and-long", 2), - 0xA1: ("or-long", 2), - 0xA2: ("xor-long", 2), - 0xA3: ("shl-long", 2), - 0xA4: ("shr-long", 2), - 0xA5: ("ushr-long", 2), - 0xA6: ("add-float", 2), - 0xA7: ("sub-float", 2), - 0xA8: ("mul-float", 2), - 0xA9: ("div-float", 2), - 0xAA: ("rem-float", 2), - 0xAB: ("add-double", 2), - 0xAC: ("sub-double", 2), - 0xAD: ("mul-double", 2), - 0xAE: ("div-double", 2), - 0xAF: ("rem-double", 2), - # Binary operations 2addr (0xB0-0xCF) - format 12x, 1 code unit - 0xB0: ("add-int/2addr", 1), - 0xB1: ("sub-int/2addr", 1), - 0xB2: ("mul-int/2addr", 1), - 0xB3: ("div-int/2addr", 1), - 0xB4: ("rem-int/2addr", 1), - 0xB5: ("and-int/2addr", 1), - 0xB6: ("or-int/2addr", 1), - 0xB7: ("xor-int/2addr", 1), - 0xB8: ("shl-int/2addr", 1), - 0xB9: ("shr-int/2addr", 1), - 0xBA: ("ushr-int/2addr", 1), - 0xBB: ("add-long/2addr", 1), - 0xBC: ("sub-long/2addr", 1), - 0xBD: ("mul-long/2addr", 1), - 0xBE: ("div-long/2addr", 1), - 0xBF: ("rem-long/2addr", 1), - 0xC0: ("and-long/2addr", 1), - 0xC1: ("or-long/2addr", 1), - 0xC2: ("xor-long/2addr", 1), - 0xC3: ("shl-long/2addr", 1), - 0xC4: ("shr-long/2addr", 1), - 0xC5: ("ushr-long/2addr", 1), - 0xC6: ("add-float/2addr", 1), - 0xC7: ("sub-float/2addr", 1), - 0xC8: ("mul-float/2addr", 1), - 0xC9: ("div-float/2addr", 1), - 0xCA: ("rem-float/2addr", 1), - 0xCB: ("add-double/2addr", 1), - 0xCC: ("sub-double/2addr", 1), - 0xCD: ("mul-double/2addr", 1), - 0xCE: ("div-double/2addr", 1), - 0xCF: ("rem-double/2addr", 1), - # Binary operations lit16 (0xD0-0xD7) - format 22s, 2 code units - 0xD0: ("add-int/lit16", 2), - 0xD1: ("rsub-int", 2), - 0xD2: ("mul-int/lit16", 2), - 0xD3: ("div-int/lit16", 2), - 0xD4: ("rem-int/lit16", 2), - 0xD5: ("and-int/lit16", 2), - 0xD6: ("or-int/lit16", 2), - 0xD7: ("xor-int/lit16", 2), - # Binary operations lit8 (0xD8-0xE2) - format 22b, 2 code units - 0xD8: ("add-int/lit8", 2), - 0xD9: ("rsub-int/lit8", 2), - 0xDA: ("mul-int/lit8", 2), - 0xDB: ("div-int/lit8", 2), - 0xDC: ("rem-int/lit8", 2), - 0xDD: ("and-int/lit8", 2), - 0xDE: ("or-int/lit8", 2), - 0xDF: ("xor-int/lit8", 2), - 0xE0: ("shl-int/lit8", 2), - 0xE1: ("shr-int/lit8", 2), - 0xE2: ("ushr-int/lit8", 2), - # (0xE3-0xF9 are unused) - # Invoke-polymorphic (0xFA-0xFB) - format 45cc/4rcc, 4 code units - 0xFA: ("invoke-polymorphic", 4), - 0xFB: ("invoke-polymorphic/range", 4), - # Invoke-custom (0xFC-0xFD) - format 35c/3rc, 3 code units - 0xFC: ("invoke-custom", 3), - 0xFD: ("invoke-custom/range", 3), - # Const method handle/type (0xFE-0xFF) - format 21c, 2 code units - 0xFE: ("const-method-handle", 2), - 0xFF: ("const-method-type", 2), -} - -# The size is in 16-bit code units. + +class DexReferenceResolver: + PRIMITIVE_TYPES = { + "VOID_T": "V", + "BOOLEAN": "Z", + "BYTE": "B", + "SHORT": "S", + "CHAR": "C", + "INT": "I", + "LONG": "J", + "FLOAT": "F", + "DOUBLE": "D", + } + ACCESS_FLAG_NAMES = [ + (0x0001, "public"), + (0x0002, "private"), + (0x0004, "protected"), + (0x0008, "static"), + (0x0010, "final"), + (0x0020, "synchronized"), + (0x0040, "bridge"), + (0x0080, "varargs"), + (0x0100, "native"), + (0x0200, "interface"), + (0x0400, "abstract"), + (0x0800, "strict"), + (0x1000, "synthetic"), + (0x2000, "annotation"), + (0x4000, "enum"), + (0x8000, "unused"), + (0x10000, "constructor"), + (0x20000, "declared-synchronized"), + ] + + def __init__(self, dex_file): + self.dex_file = dex_file + self.strings = list(getattr(dex_file, "strings", [])) + self.methods = self._index_items(getattr(dex_file, "methods", [])) + self.fields = self._index_items(getattr(dex_file, "fields", [])) + self.types = self._index_items(getattr(dex_file, "types", [])) + self.prototypes = self._index_items(getattr(dex_file, "prototypes", [])) + self.classes = self._index_items(getattr(dex_file, "classes", [])) + + def _index_items(self, items): + indexed = {} + for index, item in enumerate(items): + indexed[getattr(item, "index", index)] = item + return indexed + + def _safe_get(self, collection, index): + if index in collection: + return collection[index] + return None + + def _safe_attr(self, obj, attr, default=None): + try: + return getattr(obj, attr) + except Exception: + return default + + def _format_type(self, type_obj): + if type_obj is None: + return "" + if isinstance(type_obj, str): + return type_obj + with contextlib.suppress(Exception): + type_as_string = str(type_obj) + if type_as_string and not type_as_string.startswith("" + + def _format_proto(self, prototype): + if prototype is None: + return "()" + params = [] + for param in getattr(prototype, "parameters_type", []): + params.append(self._format_type(param)) + return_type = self._format_type(getattr(prototype, "return_type", None)) + return f"({''.join(params)}){return_type}" + + def format_method(self, method): + if method is None: + return "method@" + class_name = self._format_type(getattr(method, "cls", None)) + method_name = getattr(method, "name", "") + prototype = self._format_proto(getattr(method, "prototype", None)) + return f"{class_name}->{method_name}{prototype}" + + def format_field(self, field): + if field is None: + return "field@" + class_name = self._format_type(getattr(field, "cls", None)) + field_name = getattr(field, "name", "") + field_type = self._format_type(getattr(field, "type", None)) + return f"{class_name}->{field_name}:{field_type}" + + def format_proto(self, index): + prototype = self._safe_get(self.prototypes, index) + if prototype is None: + return f"proto@{index}" + return self._format_proto(prototype) + + def format_type_by_index(self, index): + type_obj = self._safe_get(self.types, index) + if type_obj is None: + return f"type@{index}" + return self._format_type(type_obj) + + def format_ref(self, ref_kind, index): + if ref_kind == "string": + if 0 <= index < len(self.strings): + return '"' + self.strings[index].replace('"', '\\"') + '"' + return f"string@{index}" + if ref_kind == "type": + return self.format_type_by_index(index) + if ref_kind == "field": + return self.format_field(self._safe_get(self.fields, index)) + if ref_kind == "method": + return self.format_method(self._safe_get(self.methods, index)) + if ref_kind == "proto": + return self.format_proto(index) + if ref_kind == "method_handle": + return f"method_handle@{index}" + if ref_kind == "call_site": + return f"call_site@{index}" + return f"{ref_kind}@{index}" if ref_kind else f"item@{index}" + + def get_method(self, method_index): + return self._safe_get(self.methods, method_index) + + def get_method_target(self, method_index): + method = self.get_method(method_index) + if method is None: + return None, None + code_offset = getattr(method, "code_offset", 0) + code_info = getattr(method, "code_info", None) + if code_offset and code_info: + return code_offset, self.format_method(method) + return None, self.format_method(method) + + def get_string_value(self, string_index): + if 0 <= string_index < len(self.strings): + return self.strings[string_index] + return None + + def get_method_metadata(self, method): + access_flags = getattr(method, "access_flags", 0) + access_flags = getattr(access_flags, "value", access_flags) + if isinstance(access_flags, (list, tuple, set)): + normalized_flags = 0 + for flag in access_flags: + normalized_flags |= getattr(flag, "value", 0) + access_flags = normalized_flags + access_flag_names = [name for mask, name in self.ACCESS_FLAG_NAMES if access_flags & mask] + method_name = self.format_method(method) + return { + "method_name": method_name, + "class_name": self._format_type(getattr(method, "cls", None)), + "prototype": self._format_proto(getattr(method, "prototype", None)), + "access_flags": access_flags, + "access_flags_decoded": access_flag_names, + } class DalvikDisassembler: + MAX_SWITCH_TARGETS_FOR_HEURISTIC = 32 + def __init__(self, config): self.config = config self.disassembly = DisassemblyResult() self.disassembly.smda_version = config.VERSION def addPdbFile(self, binary_info, pdb_path): - pass + return def _getPayloadSize(self, bytecode, idx): - """Calculate the size (in bytes) of a data payload at the given index. - - Dalvik embeds data payloads (packed-switch, sparse-switch, fill-array-data) - inline in the instruction stream. The first 16-bit code unit is a pseudo-opcode - identifying the payload type, and the remaining data follows a defined layout. - Returns the total size in bytes, or 0 if not a recognized payload. - """ - if idx + 2 > len(bytecode): + if idx < 0 or idx + 2 > len(bytecode): return 0 ident = struct.unpack_from(" len(bytecode): + if idx + 8 > len(bytecode): return 0 size = struct.unpack_from(" len(bytecode): return 0 size = struct.unpack_from(" len(bytecode): return 0 element_width = struct.unpack_from(" len(bytecode): + if payload_idx < 0 or payload_idx + 4 > len(bytecode): return [] ident = struct.unpack_from(" len(bytecode): - return [] size = struct.unpack_from(" len(bytecode): break rel_offset = struct.unpack_from(" len(bytecode): break rel_offset = struct.unpack_from(" len(raw_data): + raise ValueError("Invalid try_item table") + handlers_offset = tries_offset + tries_size * 8 + encoded_handler_count, cursor = read_uleb128(raw_data, handlers_offset) + handlers_by_offset = {} + for _ in range(encoded_handler_count): + handler_relative = cursor - handlers_offset + encoded_size, cursor = read_sleb128(raw_data, cursor) + catch_all_addr = None + handlers = [] + for _ in range(abs(encoded_size)): + type_idx, cursor = read_uleb128(raw_data, cursor) + addr, cursor = read_uleb128(raw_data, cursor) + handlers.append({"type_idx": type_idx, "addr_units": addr}) + if encoded_size <= 0: + catch_all_addr, cursor = read_uleb128(raw_data, cursor) + handlers_by_offset[handler_relative] = { + "handlers": handlers, + "catch_all_addr": catch_all_addr, + } + + try_items = [] + for index in range(tries_size): + start_addr_units, insn_count_units, handler_off = struct.unpack_from( + " 0 and "exception-obfuscation-surface" not in heuristics: + heuristics.append("exception-obfuscation-surface") + if ( + decoded.mnemonic + in { + "invoke-custom", + "invoke-custom/range", + "invoke-polymorphic", + "invoke-polymorphic/range", + } + and "advanced-dispatch" not in heuristics + ): + heuristics.append("advanced-dispatch") + if ( + decoded.payload_kind in {"packed-switch", "sparse-switch"} + and payload_size > self.MAX_SWITCH_TARGETS_FOR_HEURISTIC * 4 + and "large-switch-payload" not in heuristics + ): + heuristics.append("large-switch-payload") + if decoded.ref_kind == "method": + operand = decoded.operands + if ( + any(indicator in operand for indicator in ("Ljava/lang/reflect/", "Ljava/lang/Class;->forName")) + and "reflection-hotspot" not in heuristics + ): + heuristics.append("reflection-hotspot") + if ( + any( + indicator in operand + for indicator in ("loadLibrary", "load(", "DexClassLoader", "InMemoryDexClassLoader") + ) + and "native-or-dynamic-loading" not in heuristics + ): + heuristics.append("native-or-dynamic-loading") + if ( + decoded.mnemonic == "const-string" + and metadata["reference_counts"]["string"] >= 3 + and "string-staging" not in heuristics + ): + heuristics.append("string-staging") + + def analyzeFunction(self, dex_file, resolver, method): + start_addr = getattr(method, "code_offset", 0) + raw_data = self.disassembly.binary_info.raw_data bytecode_offset = start_addr header_offset = start_addr - 16 - - if header_offset < 0 or header_offset + 16 > len(raw_data): - return None - - # The LIEF Python API for CodeInfo does not expose insns_size. We manually parse - # it from the code_item header. It's a 4-byte integer at offset 12. - insns_size_units = struct.unpack_from(" len(raw_data): - LOGGER.warning( - "Bytecode range [0x%x:0x%x] exceeds raw data size (0x%x), skipping method.", - bytecode_offset, - bytecode_offset + insns_size_bytes, - len(raw_data), - ) - return None + raise ValueError("Invalid Dalvik bytecode range") bytecode = raw_data[bytecode_offset : bytecode_offset + insns_size_bytes] + try_blocks = self._parseTryBlocks( + raw_data, + resolver, + bytecode_offset, + code_item_header["insns_size"], + code_item_header["tries_size"], + ) - # Addresses are relative to bytecode_offset (where instructions actually start), - # not start_addr (which points to the code_item header). state = DalvikFunctionAnalysisState(bytecode_offset, self.disassembly) + metadata = self._buildFunctionMetadata(resolver, method, code_item_header, try_blocks) + state.metadata = metadata + for try_block in try_blocks: + for handler in try_block["handlers"]: + state.addBlockToQueue(handler["target_addr"]) + if try_block["catch_all_addr"] is not None: + state.addBlockToQueue(try_block["catch_all_addr"]) - # Use block_queue for recursive traversal instead of linear sweep. - # This avoids misinterpreting data payloads (packed-switch-payload, - # sparse-switch-payload, fill-array-data-payload) as instructions. visited_offsets = set() + payload_ranges = [] while state.hasUnprocessedBlocks(): block_start_addr = state.chooseNextBlock() idx = block_start_addr - bytecode_offset - while 0 <= idx < len(bytecode): + if any(start <= idx < end for start, end in payload_ranges): + break if idx in visited_offsets: break visited_offsets.add(idx) - op = bytecode[idx] - - if op not in DALVIK_OPCODES: - LOGGER.warning( - "Unknown Dalvik opcode 0x%02x at offset 0x%x, aborting block disassembly.", - op, - bytecode_offset + idx, + try: + decoded = decode_instruction( + bytecode, + idx, + lambda ref_kind, ref_index: self._resolveReference(resolver, ref_kind, ref_index), ) + except ValueError as exc: + self.disassembly.errors[bytecode_offset + idx] = { + "type": "dalvik_decode_error", + "instruction_bytes": bytecode[idx : idx + 2].hex(), + "message": str(exc), + } + LOGGER.warning("Failed to decode Dalvik instruction at 0x%x: %s", bytecode_offset + idx, exc) break - mnemonic, length_units = DALVIK_OPCODES[op] - length_bytes = length_units * 2 - if idx + length_bytes > len(bytecode): - break - - i_bytes = bytes(bytecode[idx : idx + length_bytes]) i_address = bytecode_offset + idx - i_size = length_bytes - i_mnemonic = mnemonic - i_op_str = "" - - state.setNextInstructionReachable(True) - - # CFG handling - if i_mnemonic in ["return-void", "return", "return-wide", "return-object", "throw"]: - state.setNextInstructionReachable(False) - elif i_mnemonic.startswith("goto"): - target_offset = 0 - if i_mnemonic == "goto": - target_offset = int.from_bytes(i_bytes[1:2], byteorder="little", signed=True) * 2 - elif i_mnemonic == "goto/16": - target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 - elif i_mnemonic == "goto/32": - target_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 - target_address = i_address + target_offset - state.addCodeRef(i_address, target_address, by_jump=True) - state.addBlockToQueue(target_address) - i_op_str = hex(target_address) - state.setNextInstructionReachable(False) - elif i_mnemonic.startswith("if-"): - target_offset = int.from_bytes(i_bytes[2:4], byteorder="little", signed=True) * 2 - target_address = i_address + target_offset - state.addCodeRef(i_address, target_address, by_jump=True) - state.addBlockToQueue(target_address) - # Fall-through is also a valid path - fall_through = i_address + i_size - state.addBlockToQueue(fall_through) - i_op_str = hex(target_address) - elif i_mnemonic in ("packed-switch", "sparse-switch"): - # The operand is a relative offset (in code units) to the payload - payload_rel_offset = int.from_bytes(i_bytes[2:6], byteorder="little", signed=True) * 2 - payload_byte_idx = idx + payload_rel_offset - switch_targets = self._resolveSwitchTargets(bytecode, idx, payload_byte_idx) - for target_byte_idx in switch_targets: - target_addr = bytecode_offset + target_byte_idx - state.addCodeRef(i_address, target_addr, by_jump=True) - state.addBlockToQueue(target_addr) - # Fall-through after switch is also possible (default case) - fall_through = i_address + i_size - state.addBlockToQueue(fall_through) - i_op_str = f"payload@{hex(bytecode_offset + payload_byte_idx)}" - elif i_mnemonic.startswith("invoke-"): - method_idx = int.from_bytes(i_bytes[2:4], byteorder="little") - try: - target_method = dex_file.methods[method_idx] - # Include class name for context (e.g. "Ljava/lang/Object;->") - i_op_str = f"{target_method.cls.fullname}->{target_method.name}" - except Exception: - i_op_str = f"method_idx_{method_idx}" - - state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, i_bytes) - idx += length_bytes - - # If this instruction ends the block (return, throw, goto), stop + i_size = decoded.size_bytes + i_mnemonic = decoded.mnemonic + i_op_str = decoded.operands + + if decoded.ref_kind in metadata["reference_counts"]: + metadata["reference_counts"][decoded.ref_kind] += 1 + + state.setNextInstructionReachable(not decoded.is_terminator) + + if decoded.ref_kind == "string" and decoded.ref_index is not None: + string_value = resolver.get_string_value(decoded.ref_index) + if string_value is not None: + self.disassembly.addStringRef(state.start_addr, i_address, string_value) + if decoded.payload_idx is not None: + payload_size = self._getPayloadSize(bytecode, decoded.payload_idx) + if payload_size: + payload_ranges.append((decoded.payload_idx, decoded.payload_idx + payload_size)) + payload_addr = bytecode_offset + decoded.payload_idx + state.addDataRef(i_address, payload_addr, size=payload_size) + if decoded.payload_kind in ("packed-switch", "sparse-switch"): + switch_targets = self._resolveSwitchTargets(bytecode, idx, decoded.payload_idx) + for target_idx in switch_targets: + target_addr = bytecode_offset + target_idx + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) + fallthrough = i_address + i_size + state.addBlockStart(fallthrough) + state.addBlockToQueue(fallthrough) + self._updateHeuristics(metadata, decoded, payload_size) + else: + self._updateHeuristics(metadata, decoded, 0) + + if i_mnemonic.startswith("goto"): + target_addr = bytecode_offset + decoded.branch_target_idx + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) + elif decoded.is_conditional and decoded.branch_target_idx is not None: + target_addr = bytecode_offset + decoded.branch_target_idx + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) + fallthrough = i_address + i_size + state.addBlockStart(fallthrough) + state.addBlockToQueue(fallthrough) + elif decoded.is_invoke: + state.setLeaf(False) + call_target = None + call_name = None + if decoded.ref_kind == "method" and decoded.ref_index is not None: + call_target, call_name = resolver.get_method_target(decoded.ref_index) + elif decoded.ref_index is not None: + call_name = resolver.format_ref(decoded.ref_kind, decoded.ref_index) + if call_target is not None: + state.addCodeRef(i_address, call_target) + if call_target == state.start_addr: + state.setRecursion(True) + elif call_name: + self._updateApiInformation(i_address, call_name) + + if self._instructionCanThrow(decoded) and try_blocks: + self._applyExceptionEdges(state, i_address, decoded, try_blocks) + + state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, decoded.bytes_) + idx += i_size if not state.is_next_instruction_reachable: break - state.endBlock() - state.label = method_info["name"] + state.label = resolver.format_method(method) state.finalizeAnalysis() return state def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): - LOGGER.debug( - "Analyzing buffer with %d bytes @0x%08x", - binary_info.binary_size, - binary_info.base_addr, - ) + LOGGER.debug("Analyzing buffer with %d bytes @0x%08x", binary_info.binary_size, binary_info.base_addr) self.disassembly = DisassemblyResult() self.disassembly.smda_version = self.config.VERSION self.disassembly.setBinaryInfo(binary_info) self.disassembly.binary_info.architecture = "dalvik" + self.disassembly.binary_info.version = "" self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) self.disassembly.language = "dalvik" - # Try raw bytes first, then fallback to list() conversion - # LIEF's Python bindings for parse() interpret bytes() as a filename string! - # Parsing a massive binary blob via list() is extremely slow in Python (taking minutes). - # We must prioritize loading the file natively using C++ fstream if a file path is provided. + if not DexFileLoader.isCompatible(binary_info.raw_data): + raise ValueError("Buffer is not a valid DEX file") + dex_file = None if getattr(binary_info, "file_path", "") and not getattr(binary_info, "is_buffer", False): with contextlib.suppress(Exception): dex_file = lief.DEX.parse(binary_info.file_path) - - if not dex_file: - # Always use list() for in-memory parsing — lief.DEX.parse(bytes) silently - # treats bytes as a filename string and returns None without raising TypeError. + if dex_file is None: dex_file = lief.DEX.parse(list(binary_info.raw_data)) - - if dex_file: - for method in dex_file.methods: - if not method.has_class: - continue - code_info = method.code_info - if not code_info: - continue - - method_info = { - "offset": method.code_offset, - "name": f"{method.cls.fullname}->{method.name}", - "code_item": code_info, - "raw_data": binary_info.raw_data, + if dex_file is None: + raise ValueError("Failed to parse DEX file") + + self.disassembly.binary_info.version = getattr(dex_file, "version", "") + resolver = DexReferenceResolver(dex_file) + + for method in dex_file.methods: + if cbAnalysisTimeout and cbAnalysisTimeout(): + break + if not getattr(method, "has_class", False): + continue + if not getattr(method, "code_info", None): + continue + if getattr(method, "code_offset", 0) < 16: + continue + try: + self.analyzeFunction(dex_file, resolver, method) + except Exception as exc: + LOGGER.warning("Failed to analyze Dalvik method %s: %s", getattr(method, "name", ""), exc) + method_offset = getattr(method, "code_offset", 0) + self.disassembly.failed_analysis_addr.append(method_offset) + self.disassembly.errors[method_offset] = { + "type": "dalvik_function_error", + "instruction_bytes": "", + "message": str(exc), } - if cbAnalysisTimeout and cbAnalysisTimeout(): - break - self.analyzeFunction(dex_file, method_info) self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc) if cbAnalysisTimeout and cbAnalysisTimeout(): diff --git a/smda/dalvik/DalvikFunctionAnalysisState.py b/smda/dalvik/DalvikFunctionAnalysisState.py index a44c0f40..eb02814b 100644 --- a/smda/dalvik/DalvikFunctionAnalysisState.py +++ b/smda/dalvik/DalvikFunctionAnalysisState.py @@ -15,6 +15,10 @@ "invoke-direct/range", "invoke-static/range", "invoke-interface/range", + "invoke-polymorphic", + "invoke-polymorphic/range", + "invoke-custom", + "invoke-custom/range", ] # Any return-* or throw is an end instruction END_INS = ["return-void", "return", "return-wide", "return-object", "throw"] @@ -33,6 +37,7 @@ def __init__(self, start_addr, disassembly): self.processed_blocks = set() self.processed_bytes = set() self.jump_targets = set() + self.block_starts = {start_addr} self.call_register_ins = [] self.block_start = 0xFFFFFFFF self.data_bytes = set() @@ -41,7 +46,6 @@ def __init__(self, start_addr, disassembly): self.code_refs_from = {} self.code_refs_to = {} self.suspicious_ins_count = 0 - self.is_jmp = False self.is_next_instruction_reachable = True self.is_block_ending_instruction = False self.is_sanely_ending = False @@ -52,6 +56,7 @@ def __init__(self, start_addr, disassembly): self.is_recursive = False self.is_thunk_call = False self.label = "" + self.metadata = {} def chooseNextBlock(self): self.is_block_ending_instruction = False @@ -60,9 +65,13 @@ def chooseNextBlock(self): return self.block_start def addBlockToQueue(self, block_start): + self.block_starts.add(block_start) if block_start not in self.processed_blocks: self.block_queue.append(block_start) + def addBlockStart(self, block_start): + self.block_starts.add(block_start) + def endBlock(self): if self.current_block: self.num_blocks_analyzed += 1 @@ -76,8 +85,7 @@ def addInstruction(self, i_address, i_size, i_mnemonic, i_op_str, i_bytes): for byte in range(i_size): self.processed_bytes.add(i_address + byte) if self.is_next_instruction_reachable: - self.addCodeRef(i_address, i_address + i_size, self.is_jmp) - self.is_jmp = False + self.addCodeRef(i_address, i_address + i_size) def addCodeRef(self, addr_from, addr_to, by_jump=False): self.code_refs.update([(addr_from, addr_to)]) @@ -88,7 +96,6 @@ def addCodeRef(self, addr_from, addr_to, by_jump=False): refs_to.update([addr_from]) self.code_refs_to[addr_to] = refs_to if by_jump: - self.is_jmp = True self.jump_targets.update([addr_to]) def removeCodeRef(self, addr_from, addr_to): @@ -120,6 +127,7 @@ def _finalizeRegularAnalysis(self): self.disassembly.function_symbols[self.start_addr] = self.label self.disassembly.function_borders[self.start_addr] = (fn_min, fn_max) + self.disassembly.function_metadata[self.start_addr] = self.metadata for ins in self.instructions: self.disassembly.instructions[ins[0]] = (ins[2], ins[1]) for offset in range(ins[1]): @@ -183,6 +191,7 @@ def getBlocks(self): ins = {i[0]: ind for ind, i in enumerate(self.instructions)} potential_starts = {self.start_addr} potential_starts.update(list(self.jump_targets)) + potential_starts.update(self.block_starts) blocks = [] for start in sorted(potential_starts): if start not in ins: diff --git a/smda/dalvik/DalvikOpcodeDecoder.py b/smda/dalvik/DalvikOpcodeDecoder.py new file mode 100644 index 00000000..65edb4ef --- /dev/null +++ b/smda/dalvik/DalvikOpcodeDecoder.py @@ -0,0 +1,628 @@ +import struct +from dataclasses import dataclass +from typing import Dict, List, Optional + + +def read_uleb128(data, offset): + result = 0 + shift = 0 + current = offset + while current < len(data): + byte = data[current] + result |= (byte & 0x7F) << shift + current += 1 + if byte & 0x80 == 0: + return result, current + shift += 7 + if shift > 35: + break + raise ValueError("Invalid uleb128 encoding") + + +def read_sleb128(data, offset): + result = 0 + shift = 0 + current = offset + size = 32 + while current < len(data): + byte = data[current] + current += 1 + result |= (byte & 0x7F) << shift + shift += 7 + if byte & 0x80 == 0: + if shift < size and byte & 0x40: + result |= -(1 << shift) + return result, current + if shift > 35: + break + raise ValueError("Invalid sleb128 encoding") + + +def parse_code_item_header(raw_data, header_offset): + if header_offset < 0 or header_offset + 16 > len(raw_data): + raise ValueError("Invalid code_item header offset") + ( + registers_size, + ins_size, + outs_size, + tries_size, + debug_info_off, + insns_size, + ) = struct.unpack_from("> 4) & 0x0F + reg_g = raw_bytes[1] & 0x0F + word = int.from_bytes(raw_bytes[4:6], byteorder="little") + reg_c = word & 0x0F + reg_d = (word >> 4) & 0x0F + reg_e = (word >> 8) & 0x0F + reg_f = (word >> 12) & 0x0F + registers = [reg_c, reg_d, reg_e, reg_f, reg_g][:count] + return count, registers + + +def _decode_register_range(count, start): + return list(range(start, start + count)) + + +def _signed(value, bits): + sign_bit = 1 << (bits - 1) + return (value & (sign_bit - 1)) - (value & sign_bit) + + +def decode_instruction(bytecode, byte_idx, resolve_ref): + opcode_value = bytecode[byte_idx] + if opcode_value not in OPCODES: + raise ValueError(f"Unknown Dalvik opcode 0x{opcode_value:02x}") + + opcode = OPCODES[opcode_value] + size_bytes = opcode.size_units * 2 + if byte_idx + size_bytes > len(bytecode): + raise ValueError("Truncated Dalvik instruction") + + raw_bytes = bytes(bytecode[byte_idx : byte_idx + size_bytes]) + registers = [] + operands = "" + literal = None + ref_index = None + ref_index_aux = None + branch_target_idx = None + payload_idx = None + + if opcode.fmt == "10x": + operands = "" + elif opcode.fmt == "10t": + branch_delta = int.from_bytes(raw_bytes[1:2], byteorder="little", signed=True) * 2 + branch_target_idx = byte_idx + branch_delta + operands = f"{hex(branch_target_idx)}" + elif opcode.fmt == "11n": + reg_a = raw_bytes[1] & 0x0F + literal = _signed((raw_bytes[1] >> 4) & 0x0F, 4) + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, #{literal:+d}" + elif opcode.fmt == "11x": + reg_a = raw_bytes[1] + registers = [reg_a] + operands = _reg_name(reg_a) + elif opcode.fmt == "12x": + reg_a = raw_bytes[1] & 0x0F + reg_b = (raw_bytes[1] >> 4) & 0x0F + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}" + elif opcode.fmt == "20t": + branch_delta = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=True) * 2 + branch_target_idx = byte_idx + branch_delta + operands = f"{hex(branch_target_idx)}" + elif opcode.fmt == "21c": + reg_a = raw_bytes[1] + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, {resolve_ref(opcode.ref_kind, ref_index)}" + elif opcode.fmt == "21h": + reg_a = raw_bytes[1] + value = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=False) + shift = 48 if opcode.mnemonic.endswith("wide/high16") else 16 + literal = value << shift + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, #{hex(literal)}" + elif opcode.fmt == "21s": + reg_a = raw_bytes[1] + literal = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=True) + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, #{literal:+d}" + elif opcode.fmt == "21t": + reg_a = raw_bytes[1] + branch_delta = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=True) * 2 + branch_target_idx = byte_idx + branch_delta + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, {hex(branch_target_idx)}" + elif opcode.fmt == "22b": + reg_a = raw_bytes[1] + reg_b = raw_bytes[2] + literal = int.from_bytes(raw_bytes[3:4], byteorder="little", signed=True) + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}, #{literal:+d}" + elif opcode.fmt == "22c": + reg_a = raw_bytes[1] & 0x0F + reg_b = (raw_bytes[1] >> 4) & 0x0F + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}, {resolve_ref(opcode.ref_kind, ref_index)}" + elif opcode.fmt == "22s": + reg_a = raw_bytes[1] & 0x0F + reg_b = (raw_bytes[1] >> 4) & 0x0F + literal = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=True) + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}, #{literal:+d}" + elif opcode.fmt == "22t": + reg_a = raw_bytes[1] & 0x0F + reg_b = (raw_bytes[1] >> 4) & 0x0F + branch_delta = int.from_bytes(raw_bytes[2:4], byteorder="little", signed=True) * 2 + branch_target_idx = byte_idx + branch_delta + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}, {hex(branch_target_idx)}" + elif opcode.fmt == "22x": + reg_a = raw_bytes[1] + reg_b = int.from_bytes(raw_bytes[2:4], byteorder="little") + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}" + elif opcode.fmt == "23x": + reg_a = raw_bytes[1] + reg_b = raw_bytes[2] + reg_c = raw_bytes[3] + registers = [reg_a, reg_b, reg_c] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}, {_reg_name(reg_c)}" + elif opcode.fmt == "30t": + branch_delta = int.from_bytes(raw_bytes[2:6], byteorder="little", signed=True) * 2 + branch_target_idx = byte_idx + branch_delta + operands = f"{hex(branch_target_idx)}" + elif opcode.fmt == "31c": + reg_a = raw_bytes[1] + ref_index = int.from_bytes(raw_bytes[2:6], byteorder="little") + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, {resolve_ref(opcode.ref_kind, ref_index)}" + elif opcode.fmt == "31i": + reg_a = raw_bytes[1] + literal = int.from_bytes(raw_bytes[2:6], byteorder="little", signed=True) + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, #{literal:+d}" + elif opcode.fmt == "31t": + reg_a = raw_bytes[1] + branch_delta = int.from_bytes(raw_bytes[2:6], byteorder="little", signed=True) * 2 + payload_idx = byte_idx + branch_delta + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, payload@{hex(payload_idx)}" + elif opcode.fmt == "32x": + reg_a = int.from_bytes(raw_bytes[2:4], byteorder="little") + reg_b = int.from_bytes(raw_bytes[4:6], byteorder="little") + registers = [reg_a, reg_b] + operands = f"{_reg_name(reg_a)}, {_reg_name(reg_b)}" + elif opcode.fmt == "35c": + _, registers = _decode_register_list_35c(raw_bytes) + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + operands = f"{{{_format_registers(registers)}}}, {resolve_ref(opcode.ref_kind, ref_index)}" + elif opcode.fmt == "3rc": + count = raw_bytes[1] + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + first_reg = int.from_bytes(raw_bytes[4:6], byteorder="little") + registers = _decode_register_range(count, first_reg) + operands = f"{{{_format_registers(registers)}}}, {resolve_ref(opcode.ref_kind, ref_index)}" + elif opcode.fmt == "45cc": + _, registers = _decode_register_list_35c(raw_bytes[:6]) + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + ref_index_aux = int.from_bytes(raw_bytes[6:8], byteorder="little") + operands = "{{{}}}, {}, {}".format( + _format_registers(registers), + resolve_ref(opcode.ref_kind, ref_index), + resolve_ref("proto", ref_index_aux), + ) + elif opcode.fmt == "4rcc": + count = raw_bytes[1] + ref_index = int.from_bytes(raw_bytes[2:4], byteorder="little") + first_reg = int.from_bytes(raw_bytes[4:6], byteorder="little") + ref_index_aux = int.from_bytes(raw_bytes[6:8], byteorder="little") + registers = _decode_register_range(count, first_reg) + operands = "{{{}}}, {}, {}".format( + _format_registers(registers), + resolve_ref(opcode.ref_kind, ref_index), + resolve_ref("proto", ref_index_aux), + ) + elif opcode.fmt == "51l": + reg_a = raw_bytes[1] + literal = int.from_bytes(raw_bytes[2:10], byteorder="little", signed=True) + registers = [reg_a] + operands = f"{_reg_name(reg_a)}, #{hex(literal)}" + else: + raise ValueError(f"Unsupported Dalvik format {opcode.fmt}") + + return DecodedDalvikInstruction( + opcode=opcode_value, + mnemonic=opcode.mnemonic, + fmt=opcode.fmt, + size_units=opcode.size_units, + size_bytes=size_bytes, + bytes_=raw_bytes, + operands=operands, + registers=registers, + literal=literal, + ref_kind=opcode.ref_kind, + ref_index=ref_index, + ref_index_aux=ref_index_aux, + branch_target_idx=branch_target_idx, + payload_idx=payload_idx, + is_invoke=opcode.is_invoke, + is_terminator=opcode.is_terminator, + is_conditional=opcode.is_conditional, + can_throw=opcode.can_throw, + payload_kind=opcode.payload_kind, + ) diff --git a/smda/utility/DexFileLoader.py b/smda/utility/DexFileLoader.py index 9edcafd7..7e7f3a1e 100644 --- a/smda/utility/DexFileLoader.py +++ b/smda/utility/DexFileLoader.py @@ -1,7 +1,52 @@ +import struct + + class DexFileLoader: - @staticmethod - def isCompatible(data): - return bool(len(data) >= 3 and data[0:3] == b"dex") + SUPPORTED_VERSIONS = {b"035", b"037", b"038", b"039", b"040"} + HEADER_SIZE = 0x70 + ENDIAN_CONSTANT = 0x12345678 + REVERSE_ENDIAN_CONSTANT = 0x78563412 + + @classmethod + def _parseHeader(cls, data): + if len(data) < cls.HEADER_SIZE: + return None + magic = data[:8] + if not (magic.startswith(b"dex\n") and magic[7] == 0): + return None + version = magic[4:7] + if version not in cls.SUPPORTED_VERSIONS: + return None + file_size = struct.unpack_from(" len(data): + return None + if endian_tag not in {cls.ENDIAN_CONSTANT, cls.REVERSE_ENDIAN_CONSTANT}: + return None + if map_off and map_off < header_size: + return None + if map_off and map_off >= file_size: + return None + if data_off and data_off > file_size: + return None + if data_size and data_off + data_size > file_size: + return None + return { + "version": version.decode("ascii"), + "file_size": file_size, + "data_off": data_off, + "data_size": data_size, + } + + @classmethod + def isCompatible(cls, data): + return cls._parseHeader(data) is not None @staticmethod def mapBinary(data): @@ -23,7 +68,11 @@ def getArchitecture(data): def getAbi(data): return "" - @staticmethod - def getCodeAreas(data): - # We return the whole data as code area - return [(0, len(data))] + @classmethod + def getCodeAreas(cls, data): + header = cls._parseHeader(data) + if not header: + return [] + if header["data_off"] and header["data_size"]: + return [(header["data_off"], header["data_off"] + header["data_size"])] + return [(0, header["file_size"])] diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index b5ff1862..6f97e66a 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -2,10 +2,12 @@ import logging import os +import struct import tempfile import unittest from smda.common.SmdaReport import SmdaReport +from smda.dalvik.DalvikOpcodeDecoder import decode_instruction, parse_code_item_header, read_sleb128, read_uleb128 from smda.Disassembler import Disassembler from smda.utility.DexFileLoader import DexFileLoader @@ -16,13 +18,27 @@ logging.disable(logging.CRITICAL) -class DalvikDisassemblerTestSuite(unittest.TestCase): - """Test suite for Dalvik/DEX disassembly using a real DEX file (blockblast classes.dex)""" +def build_dex_header(version=b"039", file_size=0x70, data_off=0x70, data_size=0): + header = bytearray(0x70) + header[:8] = b"dex\n" + version + b"\x00" + struct.pack_into(") - disasm = Disassembler(config, backend="dalvik") - cls.file_disassembly = disasm.disassembleFile(cls._temp_file_name) - - # Buffer-based disassembly - disasm_buf = Disassembler(config, backend="dalvik") - cls.buffer_disassembly = disasm_buf.disassembleUnmappedBuffer(cls.dex_binary) + cls.disasm = Disassembler(config, backend="dalvik") + cls.file_disassembly = cls.disasm.disassembleFile(cls._temp_file_name) + cls.buffer_disassembly = Disassembler(config, backend="dalvik").disassembleUnmappedBuffer(cls.dex_binary) @classmethod def tearDownClass(cls): @@ -51,30 +62,38 @@ def tearDownClass(cls): os.unlink(cls._temp_file_name) def testDexFormatDetection(self): - """DexFileLoader.isCompatible() correctly identifies DEX files""" - assert DexFileLoader.isCompatible(self.dex_binary) is True - assert DexFileLoader.getBaseAddress(self.dex_binary) == 0 - assert DexFileLoader.getBitness(self.dex_binary) == 32 - assert DexFileLoader.getArchitecture(self.dex_binary) == "dalvik" - assert DexFileLoader.getAbi(self.dex_binary) == "" - # Non-DEX data should be rejected - assert DexFileLoader.isCompatible(b"MZ\x90\x00") is False - assert DexFileLoader.isCompatible(b"") is False - assert DexFileLoader.isCompatible(b"\x7fELF") is False + self.assertTrue(DexFileLoader.isCompatible(self.dex_binary)) + self.assertEqual(DexFileLoader.getBaseAddress(self.dex_binary), 0) + self.assertEqual(DexFileLoader.getBitness(self.dex_binary), 32) + self.assertEqual(DexFileLoader.getArchitecture(self.dex_binary), "dalvik") + self.assertEqual(DexFileLoader.getAbi(self.dex_binary), "") + self.assertTrue(DexFileLoader.isCompatible(build_dex_header(version=b"035"))) + self.assertTrue(DexFileLoader.isCompatible(build_dex_header(version=b"037"))) + self.assertTrue(DexFileLoader.isCompatible(build_dex_header(version=b"038"))) + self.assertTrue(DexFileLoader.isCompatible(build_dex_header(version=b"039"))) + self.assertFalse(DexFileLoader.isCompatible(build_dex_header(version=b"041"))) + self.assertFalse(DexFileLoader.isCompatible(b"MZ\x90\x00")) + self.assertFalse(DexFileLoader.isCompatible(b"")) + self.assertFalse(DexFileLoader.isCompatible(build_dex_header(file_size=0x100))) + + def testMalformedDexFailsExplicitly(self): + malformed = bytearray(build_dex_header(file_size=0x70, data_off=0x70, data_size=0)) + malformed[0x34:0x38] = struct.pack("method)""" functions = list(self.file_disassembly.getFunctions()) - named_functions = [f for f in functions if f.function_name] - self.assertEqual(len(named_functions), 2219) - # All function names should contain '->' (class->method separator) - arrow_functions = [f for f in functions if "->" in f.function_name] - self.assertEqual(len(arrow_functions), 2219) - # All function names should start with 'L' (Dalvik class descriptor) - l_prefix_functions = [f for f in functions if f.function_name.startswith("L")] - self.assertEqual(len(l_prefix_functions), 2219) - - def testFileDisassemblyStatistics(self): - """DisassemblyStatistics fields are correct""" - stats = self.file_disassembly.statistics - self.assertEqual(stats.num_functions, 2219) - self.assertEqual(stats.num_recursive_functions, 0) - self.assertEqual(stats.num_leaf_functions, 2219) - self.assertEqual(stats.num_basic_blocks, 2219) - self.assertEqual(stats.num_instructions, 9593) - self.assertEqual(stats.num_api_calls, 0) - self.assertEqual(stats.num_function_calls, 0) - self.assertEqual(stats.num_failed_functions, 0) - self.assertEqual(stats.num_failed_instructions, 0) - - def testMarshallingRoundTrip(self): - """toDict() -> fromDict() produces equivalent report""" + self.assertTrue(any("->" in function.function_name for function in functions)) + self.assertTrue(any(function.architecture_metadata for function in functions)) + self.assertTrue(any(function.num_outrefs > 0 for function in functions)) + + any_invoke = False + any_string_ref = False + for function in functions[:250]: + for instruction in function.getInstructions(): + if instruction.mnemonic.startswith("invoke-"): + any_invoke = "->" in instruction.operands or "call_site@" in instruction.operands + if any_invoke: + break + if function.stringrefs: + any_string_ref = True + if any_invoke and any_string_ref: + break + self.assertTrue(any_invoke) + self.assertTrue(any_string_ref) + + def testReportRoundTrip(self): report_dict = self.file_disassembly.toDict() - # Validate key fields in dict self.assertEqual(report_dict["status"], "ok") self.assertEqual(report_dict["architecture"], "dalvik") self.assertEqual(report_dict["base_addr"], 0) self.assertEqual(report_dict["binary_size"], 247668) self.assertEqual(report_dict["bitness"], 32) - self.assertEqual( - report_dict["sha256"], - "70f65a5dc2d9eea731effe48acbbfdd2f1a7efe151b647f30e4a124691fcdc30", - ) - self.assertEqual(report_dict["statistics"]["num_instructions"], 9593) - self.assertEqual(report_dict["statistics"]["num_functions"], 2219) - self.assertEqual(len(report_dict["xcfg"]), 2219) - # Reconstruct from dict + self.assertTrue(report_dict["data_refs_from"] is not None) + self.assertGreater(len(report_dict["xcfg"]), 2000) + reconstructed = SmdaReport.fromDict(report_dict) self.assertEqual(reconstructed.status, "ok") self.assertEqual(reconstructed.architecture, "dalvik") - self.assertEqual(reconstructed.num_functions, 2219) - self.assertEqual(reconstructed.num_instructions, 9593) - self.assertEqual(reconstructed.num_blocks, 2219) - self.assertEqual( - reconstructed.sha256, - "70f65a5dc2d9eea731effe48acbbfdd2f1a7efe151b647f30e4a124691fcdc30", - ) + self.assertEqual(reconstructed.base_addr, 0) + self.assertEqual(reconstructed.binary_size, 247668) + self.assertEqual(reconstructed.sha256, self.file_disassembly.sha256) + self.assertEqual(len(reconstructed.xcfg), len(self.file_disassembly.xcfg)) def testBufferDisassembly(self): - """Buffer-based disassembly via disassembleUnmappedBuffer produces correct results""" self.assertEqual(self.buffer_disassembly.status, "ok") self.assertEqual(self.buffer_disassembly.architecture, "dalvik") self.assertEqual(self.buffer_disassembly.bitness, 32) self.assertEqual(self.buffer_disassembly.base_addr, 0) - self.assertEqual(self.buffer_disassembly.num_functions, 2219) - self.assertEqual(self.buffer_disassembly.num_instructions, 9593) - self.assertEqual(self.buffer_disassembly.num_blocks, 2219) - - def testFunctionProperties(self): - """Individual function properties are well-formed""" - functions = list(self.file_disassembly.getFunctions()) - for fn in functions[:10]: - self.assertIsInstance(fn.offset, int) - self.assertGreater(fn.offset, 0) - self.assertGreaterEqual(fn.num_blocks, 1) - self.assertGreaterEqual(fn.num_instructions, 1) - self.assertIsInstance(fn.apirefs, dict) - self.assertIsInstance(fn.blockrefs, dict) - self.assertIsInstance(fn.inrefs, list) - self.assertIsInstance(fn.outrefs, dict) - - def testBinarySize(self): - """binary_size matches the actual DEX file size""" - self.assertEqual(self.file_disassembly.binary_size, 247668) - self.assertIsNone(self.file_disassembly.oep) + self.assertEqual(self.buffer_disassembly.num_functions, self.file_disassembly.num_functions) + self.assertEqual(self.buffer_disassembly.num_instructions, self.file_disassembly.num_instructions) + + def testCodeItemHeaderParser(self): + header = struct.pack(" Date: Mon, 13 Apr 2026 01:10:10 +0530 Subject: [PATCH 13/25] fix(dalvik): stabilize CFG recovery and add verbose diagnostics --- smda/Disassembler.py | 12 +- smda/DisassemblyResult.py | 75 +++++++++--- smda/common/SmdaFunction.py | 76 +++++++++++- smda/dalvik/DalvikDisassembler.py | 184 +++++++++++++++++++++++++++--- tests/testDalvikDisassembler.py | 131 +++++++++++++++++++++ 5 files changed, 444 insertions(+), 34 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 647a43ef..c75ee7f4 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -34,6 +34,7 @@ def __init__(self, config=None, backend=None): self.disassembler = IdaExporter(self.config) self._start_time = None self._timeout = 0 + self._last_timeout_log_second = -1 # cache the last DisassemblyResult self.disassembly = None @@ -54,8 +55,14 @@ def _callbackAnalysisTimeout(self): if not self._timeout: return False time_diff = datetime.datetime.now(datetime.timezone.utc) - self._start_time - LOGGER.debug("Current analysis callback time %s", (time_diff)) - return time_diff.seconds >= self._timeout + elapsed_seconds = int(time_diff.total_seconds()) + if elapsed_seconds >= self._timeout: + LOGGER.debug("Current analysis callback time %s", time_diff) + return True + if elapsed_seconds >= 30 and elapsed_seconds % 30 == 0 and elapsed_seconds != self._last_timeout_log_second: + self._last_timeout_log_second = elapsed_seconds + LOGGER.debug("Current analysis callback time %s", time_diff) + return False def _addStringsToReport(self, smda_report, buffer, mode=None): smda_report.buffer = buffer @@ -193,6 +200,7 @@ def disassembleBuffer( def _disassemble(self, binary_info, timeout=0): self._start_time = datetime.datetime.now(datetime.timezone.utc) self._timeout = timeout + self._last_timeout_log_second = -1 self._ensureHashes(binary_info) if self.disassembler: self.disassembly = self.disassembler.analyzeBuffer(binary_info, self._callbackAnalysisTimeout) diff --git a/smda/DisassemblyResult.py b/smda/DisassemblyResult.py index 1b39dd95..545e3fb1 100644 --- a/smda/DisassemblyResult.py +++ b/smda/DisassemblyResult.py @@ -128,6 +128,49 @@ def getMnemonic(self, instruction_addr): return self.instructions[instruction_addr][0] return "" + def _getContainingBlockStart(self, blocks, instruction_addr): + for block in blocks: + if not block: + continue + block_start = block[0][0] + block_end = block[-1][0] + block[-1][1] + if block_start <= instruction_addr < block_end: + return block_start + return None + + def _getExceptionSuccessors(self, func_addr, blocks): + metadata = self.function_metadata.get(func_addr, {}) + try_ranges = metadata.get("try_ranges", []) + if not try_ranges: + return {} + block_successors = {} + for try_range in try_ranges: + raw_targets = [] + for handler in try_range.get("handlers", []): + target_addr = handler.get("target_addr") if isinstance(handler, dict) else None + if target_addr is not None: + raw_targets.append(target_addr) + if try_range.get("catch_all_addr") is not None: + raw_targets.append(try_range["catch_all_addr"]) + if not raw_targets: + continue + normalized_targets = set() + for target_addr in raw_targets: + block_start = self._getContainingBlockStart(blocks, target_addr) + if block_start is None: + block_start = target_addr + normalized_targets.add(block_start) + for block in blocks: + if not block: + continue + block_start = block[0][0] + block_end = block[-1][0] + block[-1][1] + if try_range["start_addr"] < block_end and block_start < try_range["end_addr"]: + successors = block_successors.get(block_start, set()) + successors.update(normalized_targets) + block_successors[block_start] = successors + return block_successors + def isCode(self, addr): return addr in self.code_map @@ -191,20 +234,26 @@ def removeDataRefs(self, addr_from, addr_to): self.data_refs_to[addr_to] = refs_to def getBlockRefs(self, func_addr): - """blocks refs should stay within function context, thus kill all references outside function""" - block_refs = {} - ins_addrs = set() - for block in self.functions[func_addr]: - for ins in block: - ins_addr = ins[0] - ins_addrs.add(ins_addr) - for block in self.functions[func_addr]: + """Return a normalized intra-function CFG keyed by block start.""" + if func_addr not in self.functions: + return {} + blocks = [block for block in self.functions[func_addr] if block] + block_starts = {block[0][0] for block in blocks} + block_refs = {block_start: [] for block_start in sorted(block_starts)} + for block in blocks: last_ins_addr = block[-1][0] - if last_ins_addr in self.code_refs_from: - verified_refs = sorted(ins_addrs.intersection(self.code_refs_from[last_ins_addr])) - if verified_refs: - block_refs[block[0][0]] = verified_refs - return block_refs + if last_ins_addr not in self.code_refs_from: + continue + verified_refs = sorted(block_starts.intersection(self.code_refs_from[last_ins_addr])) + if verified_refs: + block_refs[block[0][0]] = verified_refs + for block_start, successors in self._getExceptionSuccessors(func_addr, blocks).items(): + merged_successors = set(block_refs.get(block_start, [])) + merged_successors.update(successors) + block_refs[block_start] = sorted(merged_successors) + for successor in successors: + block_refs.setdefault(successor, []) + return {block_start: block_refs[block_start] for block_start in sorted(block_refs)} def getInRefs(self, func_addr): in_refs = [] diff --git a/smda/common/SmdaFunction.py b/smda/common/SmdaFunction.py index 163ff1d3..98e950ea 100644 --- a/smda/common/SmdaFunction.py +++ b/smda/common/SmdaFunction.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import hashlib +import logging import re import struct from typing import Iterator @@ -11,6 +12,8 @@ from .SmdaInstruction import SmdaInstruction +LOGGER = logging.getLogger(__name__) + class SmdaFunction: smda_report = None @@ -47,6 +50,7 @@ def __init__(self, disassembly=None, function_offset=None, config=None, smda_rep self.outrefs = disassembly.getOutRefs(function_offset) self.is_exported = self.offset in disassembly.exported_functions self.architecture_metadata = disassembly.function_metadata.get(function_offset, {}) + self.blockrefs = self.getNormalizedBlockRefs() # metadata self.function_name = disassembly.function_symbols.get(function_offset, "") self.characteristics = ( @@ -157,17 +161,19 @@ def getCodeOutrefs(self): yield from self.code_outrefs def _calculateSccs(self): - tarjan = Tarjan(self.blockrefs) + tarjan = Tarjan(self.getNormalizedBlockRefs()) tarjan.calculateScc() return tarjan.getResult() def _calculateNestingDepth(self): nesting_depth = 0 try: - if self.blockrefs: - tree = build_dominator_tree(self.blockrefs, self.offset) + normalized_blockrefs = self.getNormalizedBlockRefs() + root = self._getCfgRoot(normalized_blockrefs) + if normalized_blockrefs and root is not None: + tree = build_dominator_tree(normalized_blockrefs, root) if tree: - nesting_depth = get_nesting_depth(self.blockrefs, tree, self.offset) + nesting_depth = get_nesting_depth(normalized_blockrefs, tree, root) except Exception: pass return nesting_depth @@ -206,6 +212,67 @@ def _parseBlocks(self, block_dict): self.blocks[int(offset)] = instructions self.binweight += sum([len(ins.bytes) / 2 for ins in instructions]) + def _getContainingBlockStart(self, instruction_addr): + for block_start, block in self.blocks.items(): + if not block: + continue + block_end = block[-1].offset + (len(block[-1].bytes) // 2) + if block_start <= instruction_addr < block_end: + return block_start + return None + + def _getCfgRoot(self, normalized_blockrefs): + if self.offset in normalized_blockrefs: + return self.offset + block_start = self._getContainingBlockStart(self.offset) + if block_start is not None: + return block_start + if normalized_blockrefs: + fallback_root = min(normalized_blockrefs) + LOGGER.warning( + "Normalized CFG for %s (0x%x) has no entry block 0x%x, falling back to 0x%x.", + self.function_name or "", + self.offset, + self.offset, + fallback_root, + ) + return fallback_root + LOGGER.warning("Normalized CFG for %s (0x%x) is empty.", self.function_name or "", self.offset) + return None + + def getNormalizedBlockRefs(self): + current_blockrefs = self.blockrefs or {} + normalized_blockrefs = { + block_start: sorted(current_blockrefs.get(block_start, [])) for block_start in self.blocks + } + try_ranges = self.architecture_metadata.get("try_ranges", []) if self.architecture_metadata else [] + for try_range in try_ranges: + raw_targets = [] + for handler in try_range.get("handlers", []): + target_addr = handler.get("target_addr") if isinstance(handler, dict) else None + if target_addr is not None: + raw_targets.append(target_addr) + if try_range.get("catch_all_addr") is not None: + raw_targets.append(try_range["catch_all_addr"]) + if not raw_targets: + continue + normalized_targets = set() + for target_addr in raw_targets: + block_start = self._getContainingBlockStart(target_addr) + if block_start is None: + block_start = target_addr + normalized_targets.add(block_start) + normalized_blockrefs.setdefault(block_start, []) + for block_start, block in self.blocks.items(): + if not block: + continue + block_end = block[-1].offset + (len(block[-1].bytes) // 2) + if try_range["start_addr"] < block_end and block_start < try_range["end_addr"]: + successors = set(normalized_blockrefs.get(block_start, [])) + successors.update(normalized_targets) + normalized_blockrefs[block_start] = sorted(successors) + return {block_start: normalized_blockrefs[block_start] for block_start in sorted(normalized_blockrefs)} + def toDotGraph(self, with_api=False): dot_graph = f'digraph "CFG for 0x{self.offset:x}" {{\n' dot_graph += f' label="CFG for 0x{self.offset:x}";\n' @@ -245,6 +312,7 @@ def fromDict(cls, function_dict, binary_info=None, version=None, smda_report=Non # provide some legacy support by assuming functions are not exported for SMDA reports < 1.7.0 smda_function.is_exported = function_dict.get("is_exported", False) smda_function.architecture_metadata = function_dict.get("architecture_metadata", {}) + smda_function.blockrefs = smda_function.getNormalizedBlockRefs() smda_function.binweight = function_dict["metadata"]["binweight"] smda_function.characteristics = function_dict["metadata"]["characteristics"] smda_function.confidence = function_dict["metadata"]["confidence"] diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 957a6bd1..8027c5bb 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -21,6 +21,7 @@ class DexReferenceResolver: PRIMITIVE_TYPES = { "VOID_T": "V", + "VOID": "V", "BOOLEAN": "Z", "BYTE": "B", "SHORT": "S", @@ -30,6 +31,17 @@ class DexReferenceResolver: "FLOAT": "F", "DOUBLE": "D", } + STRING_PRIMITIVE_TYPES = { + "void": "V", + "boolean": "Z", + "byte": "B", + "short": "S", + "char": "C", + "int": "I", + "long": "J", + "float": "F", + "double": "D", + } ACCESS_FLAG_NAMES = [ (0x0001, "public"), (0x0002, "private"), @@ -77,34 +89,50 @@ def _safe_attr(self, obj, attr, default=None): except Exception: return default + def _normalize_type_string(self, type_name): + if not type_name: + return None + if type_name in {"V", "Z", "B", "S", "C", "I", "J", "F", "D"}: + return type_name + if type_name.startswith("[") or (type_name.startswith("L") and type_name.endswith(";")): + return type_name + lowered = type_name.lower() + if lowered in self.STRING_PRIMITIVE_TYPES: + return self.STRING_PRIMITIVE_TYPES[lowered] + if "." in type_name and "/" not in type_name: + return f"L{type_name.replace('.', '/')};" + return type_name + def _format_type(self, type_obj): if type_obj is None: return "" if isinstance(type_obj, str): - return type_obj - with contextlib.suppress(Exception): - type_as_string = str(type_obj) - if type_as_string and not type_as_string.startswith("" @@ -213,6 +241,63 @@ def __init__(self, config): def addPdbFile(self, binary_info, pdb_path): return + def _formatReferenceCounts(self, reference_counts): + active_counts = [f"{ref_kind}={count}" for ref_kind, count in reference_counts.items() if count] + return ", ".join(active_counts) if active_counts else "none" + + def _summarizeHeuristics(self): + heuristic_counts = {} + for metadata in self.disassembly.function_metadata.values(): + for heuristic in metadata.get("heuristics", []): + heuristic_counts[heuristic] = heuristic_counts.get(heuristic, 0) + 1 + if not heuristic_counts: + return "none" + return ", ".join(f"{name}={heuristic_counts[name]}" for name in sorted(heuristic_counts)) + + def _logMethodDiagnostics(self, state): + metadata = state.metadata + outref_count = len(self.disassembly.getOutRefs(state.start_addr)) + string_ref_count = len(self.disassembly.getStringRefsForFunction(state.start_addr)) + heuristics = ", ".join(metadata["heuristics"]) if metadata["heuristics"] else "none" + LOGGER.debug( + "Analyzed Dalvik method 0x%08x %s: ins=%d blocks=%d outrefs=%d handlers=%d strings=%d refs=[%s] heuristics=[%s]", + state.start_addr, + state.label, + len(state.instructions), + len(self.disassembly.functions.get(state.start_addr, [])), + outref_count, + metadata.get("exception_handler_count", 0), + string_ref_count, + self._formatReferenceCounts(metadata["reference_counts"]), + heuristics, + ) + + def _logAnalysisSummary(self, version, method_counts, analyzed_count): + total_blocks = sum(len(blocks) for blocks in self.disassembly.functions.values()) + total_strings = sum(len(stringrefs) for stringrefs in self.disassembly.stringrefs.values()) + try_functions = sum( + 1 for metadata in self.disassembly.function_metadata.values() if metadata.get("exception_handler_count", 0) + ) + LOGGER.debug( + "Dalvik analysis summary: version=%s analyzed=%d/%d functions=%d blocks=%d instructions=%d " + "api_refs=%d string_refs=%d try_functions=%d failed=%d skipped(no_class=%d,no_code=%d,invalid_offset=%d) " + "heuristics=[%s]", + version, + analyzed_count, + method_counts["total"], + len(self.disassembly.functions), + total_blocks, + len(self.disassembly.instructions), + len(self.disassembly.addr_to_api), + total_strings, + try_functions, + len(self.disassembly.failed_analysis_addr), + method_counts["skipped_no_class"], + method_counts["skipped_no_code"], + method_counts["skipped_invalid_offset"], + self._summarizeHeuristics(), + ) + def _getPayloadSize(self, bytecode, idx): if idx < 0 or idx + 2 > len(bytecode): return 0 @@ -345,6 +430,42 @@ def _applyExceptionEdges(self, state, instruction_addr, decoded, try_blocks): def _buildFunctionMetadata(self, resolver, method, code_item_header, try_blocks): metadata = resolver.get_method_metadata(method) + exception_handlers = [] + try_ranges = [] + for try_block in try_blocks: + handlers = [dict(handler) for handler in try_block["handlers"]] + handler_targets = [handler["target_addr"] for handler in handlers] + for handler in handlers: + exception_handlers.append( + { + "type_idx": handler["type_idx"], + "type_name": handler["type_name"], + "target_addr": handler["target_addr"], + "protected_range_start": try_block["start_addr"], + "protected_range_end": try_block["end_addr"], + } + ) + catch_all_addr = try_block["catch_all_addr"] + if catch_all_addr is not None: + handler_targets.append(catch_all_addr) + exception_handlers.append( + { + "type_idx": None, + "type_name": "", + "target_addr": catch_all_addr, + "protected_range_start": try_block["start_addr"], + "protected_range_end": try_block["end_addr"], + } + ) + try_ranges.append( + { + "start_addr": try_block["start_addr"], + "end_addr": try_block["end_addr"], + "handlers": handlers, + "handler_targets": handler_targets, + "catch_all_addr": catch_all_addr, + } + ) metadata.update( { "registers_size": code_item_header["registers_size"], @@ -353,7 +474,9 @@ def _buildFunctionMetadata(self, resolver, method, code_item_header, try_blocks) "tries_size": code_item_header["tries_size"], "debug_info_off": code_item_header["debug_info_off"], "insns_size_units": code_item_header["insns_size"], - "exception_handlers": len(try_blocks), + "exception_handler_count": len(exception_handlers), + "exception_handlers": exception_handlers, + "try_ranges": try_ranges, "heuristics": [], "reference_counts": { "string": 0, @@ -540,6 +663,8 @@ def analyzeFunction(self, dex_file, resolver, method): state.label = resolver.format_method(method) state.finalizeAnalysis() + if LOGGER.isEnabledFor(logging.DEBUG): + self._logMethodDiagnostics(state) return state def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): @@ -566,20 +691,47 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.binary_info.version = getattr(dex_file, "version", "") resolver = DexReferenceResolver(dex_file) + methods = list(dex_file.methods) + method_counts = { + "total": len(methods), + "skipped_no_class": 0, + "skipped_no_code": 0, + "skipped_invalid_offset": 0, + } + LOGGER.debug( + "DEX summary: version=%s classes=%d methods=%d strings=%d types=%d fields=%d protos=%d", + self.disassembly.binary_info.version, + len(list(getattr(dex_file, "classes", []))), + len(methods), + len(resolver.strings), + len(resolver.types), + len(resolver.fields), + len(resolver.prototypes), + ) - for method in dex_file.methods: + analyzed_count = 0 + for method in methods: if cbAnalysisTimeout and cbAnalysisTimeout(): break if not getattr(method, "has_class", False): + method_counts["skipped_no_class"] += 1 continue if not getattr(method, "code_info", None): + method_counts["skipped_no_code"] += 1 continue if getattr(method, "code_offset", 0) < 16: + method_counts["skipped_invalid_offset"] += 1 continue try: self.analyzeFunction(dex_file, resolver, method) + analyzed_count += 1 except Exception as exc: - LOGGER.warning("Failed to analyze Dalvik method %s: %s", getattr(method, "name", ""), exc) + LOGGER.warning( + "Failed to analyze Dalvik method %s @0x%x: %s", + resolver.format_method(method), + getattr(method, "code_offset", 0), + exc, + ) method_offset = getattr(method, "code_offset", 0) self.disassembly.failed_analysis_addr.append(method_offset) self.disassembly.errors[method_offset] = { @@ -591,4 +743,6 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc) if cbAnalysisTimeout and cbAnalysisTimeout(): self.disassembly.analysis_timeout = True + if LOGGER.isEnabledFor(logging.DEBUG): + self._logAnalysisSummary(self.disassembly.binary_info.version, method_counts, analyzed_count) return self.disassembly diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index 6f97e66a..0a8930c5 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -3,12 +3,16 @@ import logging import os import struct +import subprocess +import sys import tempfile import unittest +from smda.common.SmdaFunction import SmdaFunction from smda.common.SmdaReport import SmdaReport from smda.dalvik.DalvikOpcodeDecoder import decode_instruction, parse_code_item_header, read_sleb128, read_uleb128 from smda.Disassembler import Disassembler +from smda.DisassemblyResult import DisassemblyResult from smda.utility.DexFileLoader import DexFileLoader from .context import config @@ -137,6 +141,116 @@ def testSemanticRecovery(self): break self.assertTrue(any_invoke) self.assertTrue(any_string_ref) + normalized_invokes = [ + instruction.operands + for function in functions[:250] + for instruction in function.getInstructions() + if instruction.mnemonic.startswith("invoke-") and "->" in instruction.operands + ] + self.assertTrue(normalized_invokes) + self.assertTrue(all(" - " not in operand for operand in normalized_invokes[:50])) + + def testNormalizedBlockRefsPreserveLeafBlocksAndExceptionEdges(self): + func_addr = 0x1000 + disassembly = DisassemblyResult() + disassembly.functions[func_addr] = [ + [ + (0x1000, 2, "invoke-static", "", b"\x6e\x00"), + (0x1002, 2, "move-result-object", "", b"\x0c\x00"), + (0x1004, 2, "return-object", "", b"\x11\x00"), + ], + [ + (0x1010, 2, "move-exception", "", b"\x0d\x00"), + (0x1012, 2, "return-object", "", b"\x11\x00"), + ], + [ + (0x1020, 2, "move-exception", "", b"\x0d\x00"), + (0x1022, 2, "return-object", "", b"\x11\x00"), + ], + [(0x1030, 2, "return-void", "", b"\x0e\x00")], + ] + disassembly.function_metadata[func_addr] = { + "try_ranges": [ + { + "start_addr": 0x1000, + "end_addr": 0x1004, + "handlers": [{"type_idx": 1, "type_name": "Ljava/lang/Exception;", "target_addr": 0x1010}], + "catch_all_addr": 0x1020, + } + ] + } + blockrefs = disassembly.getBlockRefs(func_addr) + self.assertEqual(blockrefs[0x1000], [0x1010, 0x1020]) + self.assertEqual(blockrefs[0x1010], []) + self.assertEqual(blockrefs[0x1020], []) + self.assertEqual(blockrefs[0x1030], []) + + def testSmdaFunctionNormalizesSerializedDalvikCfg(self): + function_dict = { + "offset": 0x1000, + "blocks": { + 0x1000: [ + [0x1000, "6e00", "invoke-static", ""], + [0x1002, "0c00", "move-result-object", ""], + [0x1004, "1100", "return-object", ""], + ], + 0x1010: [[0x1010, "0d00", "move-exception", ""], [0x1012, "1100", "return-object", ""]], + 0x1020: [[0x1020, "0d00", "move-exception", ""], [0x1022, "1100", "return-object", ""]], + }, + "apirefs": {}, + "stringrefs": {}, + "blockrefs": {}, + "inrefs": [], + "outrefs": {}, + "is_exported": False, + "architecture_metadata": { + "debug_info_off": 0, + "try_ranges": [ + { + "start_addr": 0x1000, + "end_addr": 0x1004, + "handlers": [{"type_idx": 1, "type_name": "Ljava/lang/Exception;", "target_addr": 0x1010}], + "catch_all_addr": 0x1020, + } + ], + "exception_handlers": [ + { + "type_idx": 1, + "type_name": "Ljava/lang/Exception;", + "target_addr": 0x1010, + "protected_range_start": 0x1000, + "protected_range_end": 0x1004, + } + ], + }, + "metadata": { + "binweight": 0, + "characteristics": "", + "confidence": 0.0, + "function_name": "LFoo;->bar()Ljava/lang/Object;", + "pic_hash": None, + "nesting_depth": 0, + "strongly_connected_components": [], + "tfidf": None, + }, + } + smda_function = SmdaFunction.fromDict(function_dict) + self.assertIn(0x1000, smda_function.blockrefs) + self.assertEqual(smda_function.blockrefs[0x1000], [0x1010, 0x1020]) + self.assertGreater(smda_function.nesting_depth, 0) + + def testDalvikExceptionMetadataAndNormalizedCfg(self): + functions_with_tries = [ + function + for function in self.file_disassembly.getFunctions() + if function.architecture_metadata.get("try_ranges") + ] + self.assertTrue(functions_with_tries) + function = functions_with_tries[0] + self.assertIn("debug_info_off", function.architecture_metadata) + self.assertIsInstance(function.architecture_metadata["exception_handlers"], list) + self.assertGreaterEqual(function.architecture_metadata["exception_handler_count"], 1) + self.assertIn(function.offset, function.blockrefs) def testReportRoundTrip(self): report_dict = self.file_disassembly.toDict() @@ -164,6 +278,23 @@ def testBufferDisassembly(self): self.assertEqual(self.buffer_disassembly.num_functions, self.file_disassembly.num_functions) self.assertEqual(self.buffer_disassembly.num_instructions, self.file_disassembly.num_instructions) + def testAnalyzeScriptVerboseOutputAvoidsCfgNoise(self): + result = subprocess.run( + [sys.executable, os.path.join(config.PROJECT_ROOT, "analyze.py"), "-p", "-v", self._temp_file_name], + cwd=config.PROJECT_ROOT, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + combined_output = result.stdout + result.stderr + self.assertIn("architecture: dalvik.32bit", combined_output) + self.assertIn("DEX summary:", combined_output) + self.assertIn("Analyzed Dalvik method", combined_output) + self.assertIn("Dalvik analysis summary:", combined_output) + self.assertNotIn("Current analysis callback time", combined_output) + self.assertNotIn("r not in G", combined_output) + def testCodeItemHeaderParser(self): header = struct.pack(" Date: Mon, 13 Apr 2026 18:55:05 +0530 Subject: [PATCH 14/25] feat(dalvik): correctness, hardening, and format coverage improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P1-A: auto-detect DEX in disassembleBuffer() via magic-byte check so callers no longer need to pass architecture="dalvik" explicitly - P1-B: guard SmdaInstruction.getDetailed() against non-Intel architectures to prevent a silent Capstone x86 decode of Dalvik bytecode - P2-A: bound _getPayloadSize() to len(bytecode)-idx for all three payload types; guard element_width==0 in fill-array-data to prevent DoS on forged DEX headers - P2-B: two-pass target validation — linear sweep builds valid_instruction_starts; switch-table and exception-handler targets are rejected if they don't land on a decoded instruction boundary; violations recorded in function_metadata - P2-C: track decode_error_count / is_partial in DalvikFunctionAnalysisState and propagate both fields to function_metadata so partial functions are clearly signalled rather than silently appearing fully analysed - P3-A: fix const/high16 and const-wide/high16 (format 21h) to use signed=True for the 16-bit immediate, matching baksmali's sign-extended output - P3-B: add baksmali-style escape map in DexReferenceResolver to sanitise NUL bytes, newlines, tabs, and other control chars in DEX string literals - P3-C: extend DexFileLoader._parseHeader() to accept ODEX (dey\n) and CDEX (cdex) magic bytes; apply ruff PIE810 fix (tuple startswith) - Add 7 new tests covering all the above (23 total in testDalvikDisassembler.py) Co-Authored-By: Claude Sonnet 4.6 --- smda/Disassembler.py | 7 ++ smda/common/SmdaInstruction.py | 6 ++ smda/dalvik/DalvikDisassembler.py | 102 +++++++++++++++++-- smda/dalvik/DalvikFunctionAnalysisState.py | 6 ++ smda/dalvik/DalvikOpcodeDecoder.py | 8 +- smda/utility/DexFileLoader.py | 13 ++- tests/testDalvikDisassembler.py | 113 +++++++++++++++++++++ 7 files changed, 243 insertions(+), 12 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index c75ee7f4..3bc48bcb 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -174,6 +174,13 @@ def disassembleBuffer( Disassemble a given buffer (file_content), with given base_addr. Optionally specify bitness, the areas to which disassembly should be limited to (code_areas) and an entry point (oep) """ + # Auto-detect DEX when the caller did not explicitly override architecture. + # disassembleUnmappedBuffer / disassembleFile already use FileLoader for detection; + # this path bypasses it, so we check the magic bytes manually here. + if architecture == "intel": + from smda.utility.DexFileLoader import DexFileLoader + if DexFileLoader.isCompatible(file_content): + architecture = "dalvik" binary_info = BinaryInfo(file_content) binary_info.base_addr = base_addr binary_info.bitness = bitness diff --git a/smda/common/SmdaInstruction.py b/smda/common/SmdaInstruction.py index bb305ff7..00ae2838 100644 --- a/smda/common/SmdaInstruction.py +++ b/smda/common/SmdaInstruction.py @@ -50,6 +50,12 @@ def getDataRefs(self): yield value def getDetailed(self): + arch = self.smda_function.smda_report.architecture + if arch is not None and arch != "intel": + raise NotImplementedError( + f"getDetailed() is only available for Intel architecture, " + f"not '{arch}'" + ) if self.detailed is None: capstone = self.smda_function.smda_report.getCapstone() with_details = list(capstone.disasm(bytes.fromhex(self.bytes), self.offset)) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 8027c5bb..6a5889ae 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -174,10 +174,33 @@ def format_type_by_index(self, index): return f"type@{index}" return self._format_type(type_obj) + # Baksmali-style escape map for DEX string literals. + _STRING_ESCAPE_MAP = { + '"': '\\"', + '\\': '\\\\', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + } + + @classmethod + def _escapeDexString(cls, s): + parts = [] + for ch in s: + escaped = cls._STRING_ESCAPE_MAP.get(ch) + if escaped: + parts.append(escaped) + elif ch.isprintable(): + parts.append(ch) + else: + parts.append(f"\\u{ord(ch):04x}") + return "".join(parts) + def format_ref(self, ref_kind, index): if ref_kind == "string": if 0 <= index < len(self.strings): - return '"' + self.strings[index].replace('"', '\\"') + '"' + return '"' + self._escapeDexString(self.strings[index]) + '"' return f"string@{index}" if ref_kind == "type": return self.format_type_by_index(index) @@ -301,26 +324,38 @@ def _logAnalysisSummary(self, version, method_counts, analyzed_count): def _getPayloadSize(self, bytecode, idx): if idx < 0 or idx + 2 > len(bytecode): return 0 + max_possible = len(bytecode) - idx ident = struct.unpack_from(" len(bytecode): return 0 size = struct.unpack_from(" len(bytecode): return 0 size = struct.unpack_from(" len(bytecode): return 0 element_width = struct.unpack_from("\0 — reduced validation; LIEF handles details + elif data[:4] == b"cdex": + return {"version": "cdex", "file_size": len(data), "data_off": 0, "data_size": len(data)} + else: return None file_size = struct.unpack_from(" Date: Mon, 13 Apr 2026 19:03:02 +0530 Subject: [PATCH 15/25] style: apply ruff format to fix CI formatting check ruff format --check was failing on Linux (LF line endings) due to: - missing trailing blank line in Disassembler.py - multiline raise collapsed to single line in SmdaInstruction.py - dict literal key alignment and quote style in DalvikDisassembler.py - quote style, trailing blank lines, inline comment spacing in tests Co-Authored-By: Claude Sonnet 4.6 --- smda/Disassembler.py | 1 + smda/common/SmdaInstruction.py | 5 +---- smda/dalvik/DalvikDisassembler.py | 18 +++++++++++------- tests/testDalvikDisassembler.py | 30 +++++++++++++++++------------- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 3bc48bcb..5129228d 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -179,6 +179,7 @@ def disassembleBuffer( # this path bypasses it, so we check the magic bytes manually here. if architecture == "intel": from smda.utility.DexFileLoader import DexFileLoader + if DexFileLoader.isCompatible(file_content): architecture = "dalvik" binary_info = BinaryInfo(file_content) diff --git a/smda/common/SmdaInstruction.py b/smda/common/SmdaInstruction.py index 00ae2838..f67b38a8 100644 --- a/smda/common/SmdaInstruction.py +++ b/smda/common/SmdaInstruction.py @@ -52,10 +52,7 @@ def getDataRefs(self): def getDetailed(self): arch = self.smda_function.smda_report.architecture if arch is not None and arch != "intel": - raise NotImplementedError( - f"getDetailed() is only available for Intel architecture, " - f"not '{arch}'" - ) + raise NotImplementedError(f"getDetailed() is only available for Intel architecture, not '{arch}'") if self.detailed is None: capstone = self.smda_function.smda_report.getCapstone() with_details = list(capstone.disasm(bytes.fromhex(self.bytes), self.offset)) diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 6a5889ae..b73783bc 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -176,12 +176,12 @@ def format_type_by_index(self, index): # Baksmali-style escape map for DEX string literals. _STRING_ESCAPE_MAP = { - '"': '\\"', - '\\': '\\\\', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', - '\0': '\\0', + '"': '\\"', + "\\": "\\\\", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\0": "\\0", } @classmethod @@ -701,7 +701,11 @@ def analyzeFunction(self, dex_file, resolver, method): target_addr = bytecode_offset + target_idx if target_idx not in valid_instruction_starts: metadata.setdefault("structural_violations", []).append( - {"type": "invalid_switch_target", "from": hex(i_address), "target": hex(target_addr)} + { + "type": "invalid_switch_target", + "from": hex(i_address), + "target": hex(target_addr), + } ) continue state.addCodeRef(i_address, target_addr, by_jump=True) diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index e86a4890..e407593e 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -329,7 +329,6 @@ def testDecoderHandlesSwitchAndFillArrayPayloadRefs(self): self.assertEqual(decoded_array.mnemonic, "fill-array-data") self.assertEqual(decoded_array.payload_idx, 4) - # ── New tests for consolidated enhancement plan ────────────────────────── def testDisassembleBufferDexAutodetect(self): @@ -376,25 +375,29 @@ def testConstHigh16SignedDisplay(self): def testStringEscaping(self): """P3-B: DexReferenceResolver._escapeDexString escapes control characters.""" from smda.dalvik.DalvikDisassembler import DexReferenceResolver - self.assertEqual(DexReferenceResolver._escapeDexString('hello\nworld'), 'hello\\nworld') - self.assertEqual(DexReferenceResolver._escapeDexString('\0'), '\\0') - self.assertEqual(DexReferenceResolver._escapeDexString('\t'), '\\t') - self.assertEqual(DexReferenceResolver._escapeDexString('\r'), '\\r') + + self.assertEqual(DexReferenceResolver._escapeDexString("hello\nworld"), "hello\\nworld") + self.assertEqual(DexReferenceResolver._escapeDexString("\0"), "\\0") + self.assertEqual(DexReferenceResolver._escapeDexString("\t"), "\\t") + self.assertEqual(DexReferenceResolver._escapeDexString("\r"), "\\r") self.assertEqual(DexReferenceResolver._escapeDexString('"quoted"'), '\\"quoted\\"') - self.assertEqual(DexReferenceResolver._escapeDexString('\\back'), '\\\\back') + self.assertEqual(DexReferenceResolver._escapeDexString("\\back"), "\\\\back") # Non-printable non-mapped character uses \uXXXX - self.assertIn('\\u0001', DexReferenceResolver._escapeDexString('\x01')) + self.assertIn("\\u0001", DexReferenceResolver._escapeDexString("\x01")) def testPartialDisassemblyFlagPropagation(self): """P2-C: decode_error_count and partial_disassembly are recorded in function_metadata.""" from smda.dalvik.DalvikFunctionAnalysisState import DalvikFunctionAnalysisState + disassembly = DisassemblyResult() + # Populate BinaryInfo stub so setBinaryInfo won't be called but addCodeRefs works class FakeBinaryInfo: base_addr = 0 raw_data = b"" binary = b"" binary_size = 0 + disassembly.binary_info = FakeBinaryInfo() start_addr = 0x1000 state = DalvikFunctionAnalysisState(start_addr, disassembly) @@ -414,13 +417,14 @@ class FakeBinaryInfo: def testForgedPayloadBoundsCheck(self): """P2-A: _getPayloadSize() never returns more bytes than remain in the bytecode buffer.""" from smda.dalvik.DalvikDisassembler import DalvikDisassembler + disasm = DalvikDisassembler(config) bytecode = bytearray(100) # Packed-switch payload with huge size field - struct.pack_into(" Date: Mon, 13 Apr 2026 21:37:03 +0530 Subject: [PATCH 16/25] fix(dalvik): harden CFG validation and docs --- README.md | 6 +- analyze.py | 2 +- smda/Disassembler.py | 2 +- smda/common/SmdaFunction.py | 46 ++++++++- smda/dalvik/DalvikDisassembler.py | 149 +++++++++++++++++++++++------- tests/testDalvikDisassembler.py | 127 +++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index eb85940d..7ae1bf49 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # SMDA SMDA is a minimalist recursive disassembler library that is optimized for accurate Control Flow Graph (CFG) recovery from memory dumps. -It is based on [Capstone](http://www.capstone-engine.org/) and currently supports x86/x64 Intel machine code. -As input, arbitrary memory dumps (ideally with known base address) can be processed. +It is based on [Capstone](http://www.capstone-engine.org/) and currently supports x86/x64 Intel machine code, experimental CIL (.NET) disassembly, and Dalvik bytecode from raw DEX files. +As input, arbitrary memory dumps (ideally with known base address) can be processed, and raw DEX files can be analyzed directly. The output is a collection of functions, basic blocks, and instructions with their respective edges between blocks and functions (in/out). Optionally, references to the Windows API can be inferred by using the ApiScout method. @@ -48,6 +48,8 @@ There is also a demo script: * analyze.py -- example usage: perform disassembly on a file or memory dump and optionally store results in JSON to a given output path. +For Dalvik, the current scope is raw single-DEX inputs. APK, multi-dex container handling, and ODEX/VDEX/CDEX runtime-artifact analysis are not yet first-class workflows in SMDA. + The code should be fully compatible with Python 3.8+. Further explanation on the innerworkings follow in separate publications but will be referenced here. diff --git a/analyze.py b/analyze.py index a05cfb3b..aa410522 100644 --- a/analyze.py +++ b/analyze.py @@ -66,7 +66,7 @@ def readFileContent(file_path): "--architecture", type=str, default="", - help="Use the disassembler for the following architecture if available (default:auto, options: [intel, cil]).", + help="Use the disassembler for the following architecture if available (default:auto, options: [intel, cil, dalvik]).", ) PARSER.add_argument( "-a", diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 5129228d..a1944b62 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -73,7 +73,7 @@ def _addStringsToReport(self, smda_report, buffer, mode=None): { "string": string_value, "ins_addr": referencing_addr, - "data_addr": referencing_addr, + "data_addr": None, "type": "dex", } for referencing_addr, string_value in sorted(smda_function.stringrefs.items()) diff --git a/smda/common/SmdaFunction.py b/smda/common/SmdaFunction.py index 98e950ea..1156f979 100644 --- a/smda/common/SmdaFunction.py +++ b/smda/common/SmdaFunction.py @@ -76,7 +76,11 @@ def __init__(self, disassembly=None, function_offset=None, config=None, smda_rep and disassembly.getStringRefsForFunction(function_offset) ) ): - self.stringrefs = disassembly.getStringRefsForFunction(function_offset) + self.stringrefs = ( + self._normalizeDalvikStringRefs(disassembly.getStringRefsForFunction(function_offset)) + if disassembly.binary_info.architecture == "dalvik" + else disassembly.getStringRefsForFunction(function_offset) + ) if config and config.CALCULATE_HASHING: self.pic_hash = self.getPicHash(disassembly.binary_info) if config and config.CALCULATE_SCC: @@ -212,6 +216,35 @@ def _parseBlocks(self, block_dict): self.blocks[int(offset)] = instructions self.binweight += sum([len(ins.bytes) / 2 for ins in instructions]) + @staticmethod + def _normalizeDalvikStringRefs(stringrefs): + if not stringrefs: + return [] + if isinstance(stringrefs, list): + normalized = [] + for entry in stringrefs: + if isinstance(entry, dict): + normalized.append( + { + "string": entry.get("string", ""), + "ins_addr": int(entry.get("ins_addr", 0)), + "data_addr": entry.get("data_addr", None), + "type": entry.get("type", "dex"), + } + ) + return normalized + if isinstance(stringrefs, dict): + return [ + { + "string": string_value, + "ins_addr": int(referencing_addr), + "data_addr": None, + "type": "dex", + } + for referencing_addr, string_value in sorted(stringrefs.items()) + ] + return stringrefs + def _getContainingBlockStart(self, instruction_addr): for block_start, block in self.blocks.items(): if not block: @@ -320,7 +353,16 @@ def fromDict(cls, function_dict, binary_info=None, version=None, smda_report=Non smda_function.pic_hash = function_dict["metadata"].get("pic_hash", None) smda_function.strongly_connected_components = function_dict["metadata"]["strongly_connected_components"] smda_function.tfidf = function_dict["metadata"]["tfidf"] - smda_function.stringrefs = function_dict.get("stringrefs", {}) + stringrefs = function_dict.get("stringrefs", {}) + function_architecture = None + if smda_report is not None: + function_architecture = smda_report.architecture + elif binary_info is not None: + function_architecture = binary_info.architecture + if function_architecture == "dalvik": + smda_function.stringrefs = smda_function._normalizeDalvikStringRefs(stringrefs) + else: + smda_function.stringrefs = stringrefs if binary_info and binary_info.architecture: smda_function._escaper = IntelInstructionEscaper if binary_info.architecture in ["intel"] else None else: diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index b73783bc..fda123d6 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -387,7 +387,7 @@ def _resolveReference(self, resolver, ref_kind, ref_index): def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, tries_size): if tries_size == 0: - return [] + return [], [] tries_offset = bytecode_offset + insns_size_units * 2 if insns_size_units % 2: tries_offset += 2 @@ -396,6 +396,7 @@ def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, handlers_offset = tries_offset + tries_size * 8 encoded_handler_count, cursor = read_uleb128(raw_data, handlers_offset) handlers_by_offset = {} + structural_violations = [] for _ in range(encoded_handler_count): handler_relative = cursor - handlers_offset encoded_size, cursor = read_sleb128(raw_data, cursor) @@ -419,7 +420,16 @@ def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, raw_data, tries_offset + index * 8, ) - resolved = handlers_by_offset.get(handler_off, {"handlers": [], "catch_all_addr": None}) + resolved = handlers_by_offset.get(handler_off) + if resolved is None: + structural_violations.append( + { + "type": "invalid_handler_offset", + "try_start": hex(bytecode_offset + start_addr_units * 2), + "handler_offset": handler_off, + } + ) + resolved = {"handlers": [], "catch_all_addr": None} try_items.append( { "start_addr": bytecode_offset + start_addr_units * 2, @@ -439,7 +449,7 @@ def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, ), } ) - return try_items + return try_items, structural_violations def _instructionCanThrow(self, decoded): if decoded.can_throw: @@ -450,6 +460,7 @@ def _updateApiInformation(self, from_addr, api_name): self.disassembly.addr_to_api[from_addr] = api_name def _applyExceptionEdges(self, state, instruction_addr, decoded, try_blocks): + applied = False for try_block in try_blocks: if not (try_block["start_addr"] <= instruction_addr < try_block["end_addr"]): continue @@ -458,10 +469,13 @@ def _applyExceptionEdges(self, state, instruction_addr, decoded, try_blocks): state.addCodeRef(instruction_addr, target_addr) state.addBlockStart(target_addr) state.addBlockToQueue(target_addr) + applied = True if try_block["catch_all_addr"] is not None: state.addCodeRef(instruction_addr, try_block["catch_all_addr"]) state.addBlockStart(try_block["catch_all_addr"]) state.addBlockToQueue(try_block["catch_all_addr"]) + applied = True + return applied def _buildFunctionMetadata(self, resolver, method, code_item_header, try_blocks): metadata = resolver.get_method_metadata(method) @@ -522,6 +536,7 @@ def _buildFunctionMetadata(self, resolver, method, code_item_header, try_blocks) "call_site": 0, "method_handle": 0, }, + "structural_violations": [], } ) return metadata @@ -598,6 +613,69 @@ def _buildValidInstructionStarts(self, bytecode): idx += decoded.size_bytes return valid + def _addStructuralViolation(self, metadata, violation_type, **fields): + violation = {"type": violation_type} + violation.update(fields) + metadata.setdefault("structural_violations", []).append(violation) + + def _sanitizeTryBlocks(self, try_blocks, bytecode_offset, valid_instruction_starts): + sanitized = [] + structural_violations = [] + for try_block in try_blocks: + sanitized_handlers = [] + for handler in try_block["handlers"]: + target_addr = handler["target_addr"] + target_idx = target_addr - bytecode_offset + if target_idx in valid_instruction_starts: + sanitized_handlers.append(handler) + else: + structural_violations.append( + { + "type": "invalid_handler_target", + "protected_range_start": hex(try_block["start_addr"]), + "target": hex(target_addr), + } + ) + catch_all_addr = try_block["catch_all_addr"] + if catch_all_addr is not None and (catch_all_addr - bytecode_offset) not in valid_instruction_starts: + structural_violations.append( + { + "type": "invalid_handler_target", + "protected_range_start": hex(try_block["start_addr"]), + "target": hex(catch_all_addr), + } + ) + catch_all_addr = None + sanitized.append( + { + "start_addr": try_block["start_addr"], + "end_addr": try_block["end_addr"], + "handlers": sanitized_handlers, + "catch_all_addr": catch_all_addr, + } + ) + return sanitized, structural_violations + + def _validateBranchTarget(self, metadata, source_addr, source_idx, target_idx, valid_instruction_starts): + target_addr = source_addr + (target_idx - source_idx) + if target_idx == source_idx: + self._addStructuralViolation( + metadata, + "zero_branch_offset", + from_addr=hex(source_addr), + target=hex(target_addr), + ) + return None + if target_idx not in valid_instruction_starts: + self._addStructuralViolation( + metadata, + "invalid_branch_target", + from_addr=hex(source_addr), + target=hex(target_addr), + ) + return None + return target_addr + def analyzeFunction(self, dex_file, resolver, method): start_addr = getattr(method, "code_offset", 0) raw_data = self.disassembly.binary_info.raw_data @@ -609,7 +687,7 @@ def analyzeFunction(self, dex_file, resolver, method): raise ValueError("Invalid Dalvik bytecode range") bytecode = raw_data[bytecode_offset : bytecode_offset + insns_size_bytes] - try_blocks = self._parseTryBlocks( + try_blocks, try_violations = self._parseTryBlocks( raw_data, resolver, bytecode_offset, @@ -620,30 +698,24 @@ def analyzeFunction(self, dex_file, resolver, method): # Pass 1: build a set of legal instruction-start byte offsets for target validation. valid_instruction_starts = self._buildValidInstructionStarts(bytecode) + try_blocks, target_violations = self._sanitizeTryBlocks(try_blocks, bytecode_offset, valid_instruction_starts) + state = DalvikFunctionAnalysisState(bytecode_offset, self.disassembly) metadata = self._buildFunctionMetadata(resolver, method, code_item_header, try_blocks) state.metadata = metadata + metadata["structural_violations"].extend(try_violations) + metadata["structural_violations"].extend(target_violations) # Queue exception-handler entry points, validating against instruction boundaries. for try_block in try_blocks: for handler in try_block["handlers"]: target_addr = handler["target_addr"] - target_idx = target_addr - bytecode_offset - if target_idx in valid_instruction_starts: - state.addBlockToQueue(target_addr) - else: - metadata.setdefault("structural_violations", []).append( - {"type": "invalid_handler_target", "target": hex(target_addr)} - ) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) if try_block["catch_all_addr"] is not None: ca_addr = try_block["catch_all_addr"] - ca_idx = ca_addr - bytecode_offset - if ca_idx in valid_instruction_starts: - state.addBlockToQueue(ca_addr) - else: - metadata.setdefault("structural_violations", []).append( - {"type": "invalid_handler_target", "target": hex(ca_addr)} - ) + state.addBlockStart(ca_addr) + state.addBlockToQueue(ca_addr) visited_offsets = set() payload_ranges = [] @@ -700,12 +772,11 @@ def analyzeFunction(self, dex_file, resolver, method): for target_idx in switch_targets: target_addr = bytecode_offset + target_idx if target_idx not in valid_instruction_starts: - metadata.setdefault("structural_violations", []).append( - { - "type": "invalid_switch_target", - "from": hex(i_address), - "target": hex(target_addr), - } + self._addStructuralViolation( + metadata, + "invalid_switch_target", + from_addr=hex(i_address), + target=hex(target_addr), ) continue state.addCodeRef(i_address, target_addr, by_jump=True) @@ -719,15 +790,21 @@ def analyzeFunction(self, dex_file, resolver, method): self._updateHeuristics(metadata, decoded, 0) if i_mnemonic.startswith("goto"): - target_addr = bytecode_offset + decoded.branch_target_idx - state.addCodeRef(i_address, target_addr, by_jump=True) - state.addBlockStart(target_addr) - state.addBlockToQueue(target_addr) + target_addr = self._validateBranchTarget( + metadata, i_address, idx, decoded.branch_target_idx, valid_instruction_starts + ) + if target_addr is not None: + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) elif decoded.is_conditional and decoded.branch_target_idx is not None: - target_addr = bytecode_offset + decoded.branch_target_idx - state.addCodeRef(i_address, target_addr, by_jump=True) - state.addBlockStart(target_addr) - state.addBlockToQueue(target_addr) + target_addr = self._validateBranchTarget( + metadata, i_address, idx, decoded.branch_target_idx, valid_instruction_starts + ) + if target_addr is not None: + state.addCodeRef(i_address, target_addr, by_jump=True) + state.addBlockStart(target_addr) + state.addBlockToQueue(target_addr) fallthrough = i_address + i_size state.addBlockStart(fallthrough) state.addBlockToQueue(fallthrough) @@ -747,7 +824,13 @@ def analyzeFunction(self, dex_file, resolver, method): self._updateApiInformation(i_address, call_name) if self._instructionCanThrow(decoded) and try_blocks: - self._applyExceptionEdges(state, i_address, decoded, try_blocks) + has_exception_edges = self._applyExceptionEdges(state, i_address, decoded, try_blocks) + if has_exception_edges and state.is_next_instruction_reachable: + fallthrough = i_address + i_size + fallthrough_idx = idx + i_size + if fallthrough_idx in valid_instruction_starts: + state.addBlockStart(fallthrough) + state.addBlockToQueue(fallthrough) state.addInstruction(i_address, i_size, i_mnemonic, i_op_str, decoded.bytes_) idx += i_size diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index e407593e..377851a6 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -8,6 +8,7 @@ import tempfile import unittest +from smda.common.BinaryInfo import BinaryInfo from smda.common.SmdaFunction import SmdaFunction from smda.common.SmdaReport import SmdaReport from smda.dalvik.DalvikOpcodeDecoder import decode_instruction, parse_code_item_header, read_sleb128, read_uleb128 @@ -39,6 +40,49 @@ def __call__(self, ref_kind, ref_index): return f"{ref_kind}@{ref_index}" +class SyntheticDalvikMethod: + def __init__(self, code_offset=0x10): + self.code_offset = code_offset + self.code_info = object() + + +class SyntheticDalvikResolver: + def format_ref(self, ref_kind, ref_index): + return f"{ref_kind}@{ref_index}" + + def format_type_by_index(self, index): + return f"type@{index}" + + def get_string_value(self, string_index): + return None + + def get_method_target(self, method_index): + return None, f"method@{method_index}" + + def format_method(self, method): + return "LSynthetic;->method()V" + + def get_method_metadata(self, method): + return { + "method_name": self.format_method(method), + "class_name": "LSynthetic;", + "prototype": "()V", + "access_flags": 0, + "access_flags_decoded": [], + } + + +def build_code_item(insns, tries=None, handlers_blob=b"", registers_size=1, ins_size=0, outs_size=0, debug_info_off=0): + tries = tries or [] + insns_size_units = len(insns) // 2 + header = struct.pack(" Date: Mon, 13 Apr 2026 22:03:52 +0530 Subject: [PATCH 17/25] fix(dalvik): clean up raw dex cli output --- analyze.py | 10 ++++++++-- smda/Disassembler.py | 2 ++ tests/testDalvikDisassembler.py | 30 +++++++++++++++++++++--------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/analyze.py b/analyze.py index aa410522..d05f77ab 100644 --- a/analyze.py +++ b/analyze.py @@ -7,6 +7,7 @@ from smda.Disassembler import Disassembler from smda.SmdaConfig import SmdaConfig +from smda.utility.DexFileLoader import DexFileLoader def parseBaseAddrFromArgs(args): @@ -136,8 +137,13 @@ def readFileContent(file_path): SMDA_REPORT = DISASSEMBLER.disassembleFile(ARGS.input_path, pdb_path=ARGS.pdb_path) else: BUFFER = readFileContent(ARGS.input_path) - BASE_ADDR = parseBaseAddrFromArgs(ARGS) - OEP = parseOepFromArgs(ARGS) + treat_as_dalvik = ARGS.architecture in {"", "dalvik"} and DexFileLoader.isCompatible(BUFFER) + if treat_as_dalvik: + BASE_ADDR = DexFileLoader.getBaseAddress(BUFFER) + OEP = None + else: + BASE_ADDR = parseBaseAddrFromArgs(ARGS) + OEP = parseOepFromArgs(ARGS) config.API_COLLECTION_FILES = { "win_7": os.sep.join([config.PROJECT_ROOT, "data", "apiscout_win7_prof-n_sp1.json"]) } diff --git a/smda/Disassembler.py b/smda/Disassembler.py index a1944b62..19523788 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -182,6 +182,8 @@ def disassembleBuffer( if DexFileLoader.isCompatible(file_content): architecture = "dalvik" + if bitness is None: + bitness = DexFileLoader.getBitness(file_content) binary_info = BinaryInfo(file_content) binary_info.base_addr = base_addr binary_info.bitness = bitness diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index 377851a6..aeaba997 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -390,23 +390,39 @@ def testDecoderHandlesSwitchAndFillArrayPayloadRefs(self): # ── New tests for consolidated enhancement plan ────────────────────────── def testDisassembleBufferDexAutodetect(self): - """P1-A: disassembleBuffer() without explicit architecture auto-detects DEX.""" - # Create a generic Disassembler with no backend override — the default is "intel". - # With P1-A the DEX magic bytes should flip the architecture to "dalvik". generic_disasm = Disassembler(config) report = generic_disasm.disassembleBuffer(self.dex_binary, base_addr=0) self.assertEqual(report.architecture, "dalvik") + self.assertEqual(report.bitness, 32) self.assertGreater(report.num_functions, 2000) + def testAnalyzeScriptRawDexAvoidsBaseAddrAndOepWarnings(self): + result = subprocess.run( + [ + sys.executable, + os.path.join(config.PROJECT_ROOT, "analyze.py"), + self._temp_file_name, + "-o", + tempfile.gettempdir(), + ], + cwd=config.PROJECT_ROOT, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + combined_output = result.stdout + result.stderr + self.assertIn("architecture: dalvik.32bit", combined_output) + self.assertNotIn("No base address recognized, using 0.", combined_output) + self.assertNotIn("No OEP recognized, skipping.", combined_output) + def testGetDetailedRaisesOnDalvik(self): - """P1-B: SmdaInstruction.getDetailed() raises NotImplementedError on Dalvik reports.""" functions = list(self.file_disassembly.getFunctions()) instruction = next(iter(functions[0].getInstructions())) with self.assertRaises(NotImplementedError): instruction.getDetailed() def testOdexCdexFormatDetection(self): - """P3-C: DexFileLoader.isCompatible() recognises ODEX (dey) and CDEX (cdex) magic.""" # Build a minimal valid ODEX header (same structure as DEX, different magic) odex_header = bytearray(build_dex_header(version=b"039")) odex_header[:4] = b"dey\n" @@ -421,7 +437,6 @@ def testOdexCdexFormatDetection(self): self.assertFalse(DexFileLoader.isCompatible(b"\x7fELF" + b"\x00" * 0x70)) def testConstHigh16SignedDisplay(self): - """P3-A: const/high16 with 0xFFFF immediate produces a negative literal.""" # 15 00 FF FF → const/high16 v0, #0xFFFF0000 (= -65536 as int32) raw = bytes([0x15, 0x00, 0xFF, 0xFF]) decoded = decode_instruction(raw, 0, DummyResolver()) @@ -431,7 +446,6 @@ def testConstHigh16SignedDisplay(self): self.assertIn("-", decoded.operands) def testStringEscaping(self): - """P3-B: DexReferenceResolver._escapeDexString escapes control characters.""" from smda.dalvik.DalvikDisassembler import DexReferenceResolver self.assertEqual(DexReferenceResolver._escapeDexString("hello\nworld"), "hello\\nworld") @@ -444,7 +458,6 @@ def testStringEscaping(self): self.assertIn("\\u0001", DexReferenceResolver._escapeDexString("\x01")) def testPartialDisassemblyFlagPropagation(self): - """P2-C: decode_error_count and partial_disassembly are recorded in function_metadata.""" from smda.dalvik.DalvikFunctionAnalysisState import DalvikFunctionAnalysisState disassembly = DisassemblyResult() @@ -473,7 +486,6 @@ class FakeBinaryInfo: self.assertEqual(meta.get("decode_error_count"), 2) def testForgedPayloadBoundsCheck(self): - """P2-A: _getPayloadSize() never returns more bytes than remain in the bytecode buffer.""" from smda.dalvik.DalvikDisassembler import DalvikDisassembler disasm = DalvikDisassembler(config) From dd43bf77782e950bfe5d88b1b2393ecee98de7b6 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:34:41 +0530 Subject: [PATCH 18/25] Update smda/common/SmdaInstruction.py Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- smda/common/SmdaInstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smda/common/SmdaInstruction.py b/smda/common/SmdaInstruction.py index f67b38a8..10492f26 100644 --- a/smda/common/SmdaInstruction.py +++ b/smda/common/SmdaInstruction.py @@ -27,7 +27,7 @@ def __init__(self, ins_list=None, smda_function=None): def getDataRefs(self): explicit_refs = set() smda_report = self.smda_function.smda_report - if hasattr(smda_report, "data_refs_from") and self.offset in smda_report.data_refs_from: + if smda_report.data_refs_from is not None and self.offset in smda_report.data_refs_from: explicit_refs.update(smda_report.data_refs_from[self.offset]) yield from sorted(explicit_refs) if explicit_refs: From 53e9a080947cb8a34d42fcec0872cf515ae558bf Mon Sep 17 00:00:00 2001 From: r0ny123 Date: Tue, 14 Apr 2026 09:38:16 +0530 Subject: [PATCH 19/25] fix(dalvik): improve cli diagnostics --- analyze.py | 90 ++++++++++++++++++++++++++----- smda/common/SmdaReport.py | 3 +- smda/dalvik/DalvikDisassembler.py | 78 +++++++++++++++++++-------- tests/testDalvikDisassembler.py | 12 ++--- 4 files changed, 139 insertions(+), 44 deletions(-) diff --git a/analyze.py b/analyze.py index d05f77ab..4fc1574c 100644 --- a/analyze.py +++ b/analyze.py @@ -10,7 +10,7 @@ from smda.utility.DexFileLoader import DexFileLoader -def parseBaseAddrFromArgs(args): +def parseBaseAddrFromArgs(args, silent=False): if args.base_addr: parsed_base_addr = int(args.base_addr, 16) if args.base_addr.startswith("0x") else int(args.base_addr) logging.info("using provided base address: 0x%08x", parsed_base_addr) @@ -19,24 +19,75 @@ def parseBaseAddrFromArgs(args): baddr_match = re.search(re.compile("_0x(?P[0-9a-fA-F]{8,16})"), args.input_path) if baddr_match: parsed_base_addr = int(baddr_match.group("base_addr"), 16) - logging.info( - "Parsed base address from file name: 0x%08x", - parsed_base_addr, - ) + logging.info("Parsed base address from file name: 0x%08x", parsed_base_addr) return parsed_base_addr - logging.warning("No base address recognized, using 0.") + if not silent: + logging.warning("No base address recognized, using 0.") return 0 -def parseOepFromArgs(args): +def parseOepFromArgs(args, silent=False): if args.oep and args.oep != "": parsed_oep = int(args.oep, 16) if args.oep.startswith("0x") else int(args.oep) logging.info("using provided OEP(RVA): 0x%08x", parsed_oep) return parsed_oep - logging.warning("No OEP recognized, skipping.") + if not silent: + logging.warning("No OEP recognized, skipping.") return None +def _printDalvikSummary(report, output_path, input_filename): + """Structured one-screen summary printed to stdout after Dalvik/DEX analysis.""" + size_bytes = report.binary_size or 0 + size_str = f"{size_bytes / 1024 / 1024:.1f} MB" if size_bytes >= 1024 * 1024 else f"{size_bytes / 1024:.1f} KB" + + dex_version = report.version if report.version else "?" + bitness_str = f".{report.bitness}bit" if report.bitness else "" + stats = report.statistics + + # Aggregate heuristic tags and string-ref count across all functions + heuristic_counts = {} + string_ref_total = 0 + for fn in report.getFunctions(): + for tag in (fn.architecture_metadata or {}).get("heuristics", []): + heuristic_counts[tag] = heuristic_counts.get(tag, 0) + 1 + string_ref_total += len(fn.stringrefs or {}) + + print(f"[*] File: {input_filename} ({size_str})") + print(f"[*] Architecture: {report.architecture}{bitness_str}") + print(f"[*] Format: Dalvik DEX v{dex_version}") + print(f"[*] Time: {report.execution_time:.3f}s") + print(f"[*] Functions: {stats.num_functions:,}") + print(f"[*] CFG: {stats.num_basic_blocks:,} blocks / {stats.num_instructions:,} instructions") + print(f"[*] Refs: api={stats.num_api_calls:,} strings={string_ref_total:,}") + + if heuristic_counts: + # Sort by count descending, wrap at ~52 chars per continuation line + tags = sorted(heuristic_counts.items(), key=lambda kv: -kv[1]) + tag_strs = [f"{k}={v}" for k, v in tags] + lines, current = [], "" + for ts in tag_strs: + if current and len(current) + 2 + len(ts) > 52: + lines.append(current) + current = ts + else: + current = (current + " " + ts).lstrip() + if current: + lines.append(current) + print(f"[!] Heuristics: {lines[0]}") + for line in lines[1:]: + print(f" {line}") + + if output_path and os.path.isdir(output_path): + print(f"[+] Saved: {os.path.join(output_path, input_filename + '.smda')}") + + +def _getInteractiveStream(stream): + if hasattr(stream, "reconfigure"): + stream.reconfigure(errors="backslashreplace") + return stream + + def readFileContent(file_path): file_content = b"" with open(file_path, "rb") as fin: @@ -121,11 +172,18 @@ def readFileContent(file_path): # optionally create and set up a config, e.g. when using ApiScout profiles for WinAPI import usage discovery config = SmdaConfig() - if ARGS.verbose: - config.LOG_LEVEL = logging.DEBUG if ARGS.strings: config.WITH_STRINGS = True - logging.basicConfig(level=config.LOG_LEVEL, format=config.LOG_FORMAT) + if ARGS.verbose: + config.LOG_LEVEL = logging.DEBUG + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s.%(msecs)03d %(levelname).1s %(name)s: %(message)s", + datefmt="%H:%M:%S", + stream=_getInteractiveStream(sys.stdout), + ) + else: + logging.basicConfig(level=config.LOG_LEVEL, format=config.LOG_FORMAT) SMDA_REPORT = None INPUT_FILENAME = "" BITNESS = ARGS.bitness if (ARGS.bitness in [32, 64]) else None @@ -150,7 +208,11 @@ def readFileContent(file_path): DISASSEMBLER = Disassembler(config, backend=ARGS.architecture) SMDA_REPORT = DISASSEMBLER.disassembleBuffer(BUFFER, BASE_ADDR, BITNESS, oep=OEP) SMDA_REPORT.filename = os.path.basename(ARGS.input_path) - print(SMDA_REPORT) - if SMDA_REPORT and os.path.isdir(ARGS.output_path): - with open(ARGS.output_path + os.sep + INPUT_FILENAME + ".smda", "w") as fout: + if SMDA_REPORT.architecture == "dalvik": + _printDalvikSummary(SMDA_REPORT, ARGS.output_path, INPUT_FILENAME) + else: + print(SMDA_REPORT) + if SMDA_REPORT and ARGS.output_path and os.path.isdir(ARGS.output_path): + output_file = os.path.join(ARGS.output_path, INPUT_FILENAME + ".smda") + with open(output_file, "w") as fout: json.dump(SMDA_REPORT.toDict(), fout, indent=1, sort_keys=True) diff --git a/smda/common/SmdaReport.py b/smda/common/SmdaReport.py index 5f03c79b..07a33674 100644 --- a/smda/common/SmdaReport.py +++ b/smda/common/SmdaReport.py @@ -340,4 +340,5 @@ def toFile(self, output_filepath) -> None: def __str__(self): if self.status == "error": return f"{self.execution_time:>6.3f}s -> {self.message}" - return f"{self.execution_time:>6.3f}s -> (architecture: {self.architecture}.{self.bitness}bit, base_addr: 0x{self.base_addr:08x}): {len(self.xcfg)} functions" + arch_str = f"{self.architecture}.{self.bitness}bit" if self.bitness else self.architecture + return f"{self.execution_time:>6.3f}s -> (architecture: {arch_str}, base_addr: 0x{self.base_addr:08x}): {len(self.xcfg)} functions" diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index fda123d6..49a207b1 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -260,6 +260,7 @@ def __init__(self, config): self.config = config self.disassembly = DisassemblyResult() self.disassembly.smda_version = config.VERSION + self._diag_stubs_suppressed = 0 def addPdbFile(self, binary_info, pdb_path): return @@ -279,21 +280,36 @@ def _summarizeHeuristics(self): def _logMethodDiagnostics(self, state): metadata = state.metadata + heuristics = metadata["heuristics"] + n_ins = len(state.instructions) + n_blocks = len(self.disassembly.functions.get(state.start_addr, [])) + n_handlers = metadata.get("exception_handler_count", 0) + + # Suppress trivial single-block stubs with no heuristics — typically generated + # string-obfuscation shims. Count them for the summary instead. + if n_ins <= 8 and n_blocks <= 1 and n_handlers == 0 and not heuristics: + self._diag_stubs_suppressed += 1 + return + + heuristic_str = ", ".join(heuristics) if heuristics else "none" outref_count = len(self.disassembly.getOutRefs(state.start_addr)) string_ref_count = len(self.disassembly.getStringRefsForFunction(state.start_addr)) - heuristics = ", ".join(metadata["heuristics"]) if metadata["heuristics"] else "none" - LOGGER.debug( - "Analyzed Dalvik method 0x%08x %s: ins=%d blocks=%d outrefs=%d handlers=%d strings=%d refs=[%s] heuristics=[%s]", + fmt = "0x%08x %s: ins=%d blocks=%d outrefs=%d handlers=%d strings=%d refs=[%s] heuristics=[%s]" + args = ( state.start_addr, state.label, - len(state.instructions), - len(self.disassembly.functions.get(state.start_addr, [])), + n_ins, + n_blocks, outref_count, - metadata.get("exception_handler_count", 0), + n_handlers, string_ref_count, self._formatReferenceCounts(metadata["reference_counts"]), - heuristics, + heuristic_str, ) + # Both tiers logged at DEBUG — per-method detail belongs in verbose mode only. + # Heuristic summary is surfaced via the INFO-level analysis banner instead. + prefix = "[!] " if heuristics else "" + LOGGER.debug(prefix + fmt, *args) def _logAnalysisSummary(self, version, method_counts, analyzed_count): total_blocks = sum(len(blocks) for blocks in self.disassembly.functions.values()) @@ -301,25 +317,40 @@ def _logAnalysisSummary(self, version, method_counts, analyzed_count): try_functions = sum( 1 for metadata in self.disassembly.function_metadata.values() if metadata.get("exception_handler_count", 0) ) - LOGGER.debug( - "Dalvik analysis summary: version=%s analyzed=%d/%d functions=%d blocks=%d instructions=%d " - "api_refs=%d string_refs=%d try_functions=%d failed=%d skipped(no_class=%d,no_code=%d,invalid_offset=%d) " - "heuristics=[%s]", + heuristics_str = self._summarizeHeuristics() + sep = "-" * 68 + LOGGER.info(sep) + LOGGER.info( + "DEX v%s | analyzed %d/%d | functions=%d blocks=%d instructions=%d", version, analyzed_count, method_counts["total"], len(self.disassembly.functions), total_blocks, len(self.disassembly.instructions), + ) + LOGGER.info( + " api_refs=%d string_refs=%d try_blocks=%d failed=%d", len(self.disassembly.addr_to_api), total_strings, try_functions, len(self.disassembly.failed_analysis_addr), - method_counts["skipped_no_class"], - method_counts["skipped_no_code"], - method_counts["skipped_invalid_offset"], - self._summarizeHeuristics(), ) + skip_nc = method_counts["skipped_no_class"] + skip_nc2 = method_counts["skipped_no_code"] + skip_inv = method_counts["skipped_invalid_offset"] + if skip_nc + skip_nc2 + skip_inv: + LOGGER.info( + " skipped: no_class=%d no_code=%d invalid_offset=%d", + skip_nc, + skip_nc2, + skip_inv, + ) + if self._diag_stubs_suppressed: + LOGGER.info(" (%d trivial stubs suppressed from per-method log)", self._diag_stubs_suppressed) + if heuristics_str: + LOGGER.info("[!] heuristics: %s", heuristics_str) + LOGGER.info(sep) def _getPayloadSize(self, bytecode, idx): if idx < 0 or idx + 2 > len(bytecode): @@ -840,16 +871,17 @@ def analyzeFunction(self, dex_file, resolver, method): state.label = resolver.format_method(method) state.finalizeAnalysis() - if LOGGER.isEnabledFor(logging.DEBUG): - self._logMethodDiagnostics(state) + self._logMethodDiagnostics(state) return state def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): - LOGGER.debug("Analyzing buffer with %d bytes @0x%08x", binary_info.binary_size, binary_info.base_addr) + self._diag_stubs_suppressed = 0 + LOGGER.info("Analyzing buffer with %d bytes @0x%08x", binary_info.binary_size, binary_info.base_addr) self.disassembly = DisassemblyResult() self.disassembly.smda_version = self.config.VERSION self.disassembly.setBinaryInfo(binary_info) self.disassembly.binary_info.architecture = "dalvik" + self.disassembly.binary_info.bitness = 32 # Dalvik VM is always 32-bit self.disassembly.binary_info.version = "" self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc) self.disassembly.language = "dalvik" @@ -875,8 +907,10 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): "skipped_no_code": 0, "skipped_invalid_offset": 0, } - LOGGER.debug( - "DEX summary: version=%s classes=%d methods=%d strings=%d types=%d fields=%d protos=%d", + sep = "-" * 68 + LOGGER.info(sep) + LOGGER.info( + "DEX v%s | classes=%d methods=%d strings=%d types=%d fields=%d protos=%d", self.disassembly.binary_info.version, len(list(getattr(dex_file, "classes", []))), len(methods), @@ -885,6 +919,7 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): len(resolver.fields), len(resolver.prototypes), ) + LOGGER.info(sep) analyzed_count = 0 for method in methods: @@ -920,6 +955,5 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc) if cbAnalysisTimeout and cbAnalysisTimeout(): self.disassembly.analysis_timeout = True - if LOGGER.isEnabledFor(logging.DEBUG): - self._logAnalysisSummary(self.disassembly.binary_info.version, method_counts, analyzed_count) + self._logAnalysisSummary(self.disassembly.binary_info.version, method_counts, analyzed_count) return self.disassembly diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index aeaba997..a6f3176e 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -346,10 +346,10 @@ def testAnalyzeScriptVerboseOutputAvoidsCfgNoise(self): ) self.assertEqual(result.returncode, 0, msg=result.stderr) combined_output = result.stdout + result.stderr - self.assertIn("architecture: dalvik.32bit", combined_output) - self.assertIn("DEX summary:", combined_output) - self.assertIn("Analyzed Dalvik method", combined_output) - self.assertIn("Dalvik analysis summary:", combined_output) + self.assertIn("dalvik.32bit", combined_output) + self.assertIn("DEX v", combined_output) + self.assertIn("heuristics=[", combined_output) + self.assertIn("api_refs=", combined_output) self.assertNotIn("Current analysis callback time", combined_output) self.assertNotIn("r not in G", combined_output) @@ -387,8 +387,6 @@ def testDecoderHandlesSwitchAndFillArrayPayloadRefs(self): self.assertEqual(decoded_array.mnemonic, "fill-array-data") self.assertEqual(decoded_array.payload_idx, 4) - # ── New tests for consolidated enhancement plan ────────────────────────── - def testDisassembleBufferDexAutodetect(self): generic_disasm = Disassembler(config) report = generic_disasm.disassembleBuffer(self.dex_binary, base_addr=0) @@ -412,7 +410,7 @@ def testAnalyzeScriptRawDexAvoidsBaseAddrAndOepWarnings(self): ) self.assertEqual(result.returncode, 0, msg=result.stderr) combined_output = result.stdout + result.stderr - self.assertIn("architecture: dalvik.32bit", combined_output) + self.assertIn("dalvik.32bit", combined_output) self.assertNotIn("No base address recognized, using 0.", combined_output) self.assertNotIn("No OEP recognized, skipping.", combined_output) From e73fa8feb551ead9f88db95667a59f03546582ab Mon Sep 17 00:00:00 2001 From: r0ny123 Date: Thu, 16 Apr 2026 01:23:51 +0530 Subject: [PATCH 20/25] Refactor: Address PR review comments regarding imports - Moved DexFileLoader import to the top of smda/Disassembler.py for consistency. - Removed redundant 'as' aliases in smda/dalvik/__init__.py. --- smda/Disassembler.py | 3 +-- smda/dalvik/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 19523788..f01432c8 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -11,6 +11,7 @@ from smda.ida.IdaExporter import IdaExporter from smda.intel.IntelDisassembler import IntelDisassembler from smda.SmdaConfig import SmdaConfig +from smda.utility.DexFileLoader import DexFileLoader from smda.utility.FileLoader import FileLoader from smda.utility.MemoryFileLoader import MemoryFileLoader from smda.utility.StringExtractor import extract_strings @@ -178,8 +179,6 @@ def disassembleBuffer( # disassembleUnmappedBuffer / disassembleFile already use FileLoader for detection; # this path bypasses it, so we check the magic bytes manually here. if architecture == "intel": - from smda.utility.DexFileLoader import DexFileLoader - if DexFileLoader.isCompatible(file_content): architecture = "dalvik" if bitness is None: diff --git a/smda/dalvik/__init__.py b/smda/dalvik/__init__.py index 33b8395b..20273edd 100644 --- a/smda/dalvik/__init__.py +++ b/smda/dalvik/__init__.py @@ -1,2 +1,2 @@ -from .DalvikDisassembler import DalvikDisassembler as DalvikDisassembler -from .DalvikFunctionAnalysisState import DalvikFunctionAnalysisState as DalvikFunctionAnalysisState +from .DalvikDisassembler import DalvikDisassembler +from .DalvikFunctionAnalysisState import DalvikFunctionAnalysisState From fdba45daacd19ef3084b43edd882322052757cb2 Mon Sep 17 00:00:00 2001 From: r0ny123 Date: Thu, 16 Apr 2026 01:27:31 +0530 Subject: [PATCH 21/25] Fix: Lint issues identified by ruff - Combined nested if statements in Disassembler.py (SIM102). - Restored explicit re-exports in dalvik/__init__.py (F401). --- smda/Disassembler.py | 7 +++---- smda/dalvik/__init__.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index f01432c8..84be2f3b 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -178,10 +178,9 @@ def disassembleBuffer( # Auto-detect DEX when the caller did not explicitly override architecture. # disassembleUnmappedBuffer / disassembleFile already use FileLoader for detection; # this path bypasses it, so we check the magic bytes manually here. - if architecture == "intel": - if DexFileLoader.isCompatible(file_content): - architecture = "dalvik" - if bitness is None: + if architecture == "intel" and DexFileLoader.isCompatible(file_content): + architecture = "dalvik" + if bitness is None: bitness = DexFileLoader.getBitness(file_content) binary_info = BinaryInfo(file_content) binary_info.base_addr = base_addr diff --git a/smda/dalvik/__init__.py b/smda/dalvik/__init__.py index 20273edd..33b8395b 100644 --- a/smda/dalvik/__init__.py +++ b/smda/dalvik/__init__.py @@ -1,2 +1,2 @@ -from .DalvikDisassembler import DalvikDisassembler -from .DalvikFunctionAnalysisState import DalvikFunctionAnalysisState +from .DalvikDisassembler import DalvikDisassembler as DalvikDisassembler +from .DalvikFunctionAnalysisState import DalvikFunctionAnalysisState as DalvikFunctionAnalysisState From fdc1de22652d8cebbdda91efa14b961b36b55019 Mon Sep 17 00:00:00 2001 From: r0ny123 Date: Thu, 16 Apr 2026 01:30:19 +0530 Subject: [PATCH 22/25] Style: Reformat smda/Disassembler.py with ruff --- smda/Disassembler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 84be2f3b..ac589c70 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -181,7 +181,7 @@ def disassembleBuffer( if architecture == "intel" and DexFileLoader.isCompatible(file_content): architecture = "dalvik" if bitness is None: - bitness = DexFileLoader.getBitness(file_content) + bitness = DexFileLoader.getBitness(file_content) binary_info = BinaryInfo(file_content) binary_info.base_addr = base_addr binary_info.bitness = bitness From ee66effbe15e240363be1d18c062e5383361fcd1 Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:43:46 +0530 Subject: [PATCH 23/25] Update smda/Disassembler.py Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- smda/Disassembler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index ac589c70..2f54d652 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -68,8 +68,8 @@ def _callbackAnalysisTimeout(self): def _addStringsToReport(self, smda_report, buffer, mode=None): smda_report.buffer = buffer for smda_function in smda_report.getFunctions(): - if smda_report.architecture == "dalvik" and smda_function.stringrefs: - if isinstance(smda_function.stringrefs, dict): + if smda_report.architecture == "dalvik": + if smda_function.stringrefs and isinstance(smda_function.stringrefs, dict): smda_function.stringrefs = [ { "string": string_value, From 7f65c5b0ede6fba0a3b7333e34b31402ea915c2f Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Thu, 7 May 2026 20:16:02 +0530 Subject: [PATCH 24/25] Refactor: Address review feedback on Dalvik PR - SmdaInstruction.getDataRefs: yield both explicit data_refs_from entries and Intel operand-derived refs (deduped) instead of returning early after explicit refs. - SmdaFunction._getCfgRoot: return None when offset has no entry block, so dominator/nesting are skipped rather than computed from a fabricated root. - Disassembler._callbackAnalysisTimeout: log on 30s bucket transitions to remain reliable when callback timing skips past exact boundaries. - DalvikDisassembler: prefer bytes for lief.DEX.parse (avoids ~30x memory blowup on large DEX), with fallback to list for older LIEF. - DalvikDisassembler._buildValidInstructionStarts: advance by 2 bytes on resync (Dalvik instructions are 16-bit aligned). - analyze.py: replace hand-rolled wrap with textwrap.wrap. Co-Authored-By: Claude Opus 4.7 (1M context) --- analyze.py | 14 +++----------- smda/Disassembler.py | 7 +++++-- smda/common/SmdaFunction.py | 12 +++++++----- smda/common/SmdaInstruction.py | 16 ++++++++++------ smda/dalvik/DalvikDisassembler.py | 20 +++++++++++++++++--- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/analyze.py b/analyze.py index 4fc1574c..14e0b508 100644 --- a/analyze.py +++ b/analyze.py @@ -4,6 +4,7 @@ import os import re import sys +import textwrap from smda.Disassembler import Disassembler from smda.SmdaConfig import SmdaConfig @@ -62,18 +63,9 @@ def _printDalvikSummary(report, output_path, input_filename): print(f"[*] Refs: api={stats.num_api_calls:,} strings={string_ref_total:,}") if heuristic_counts: - # Sort by count descending, wrap at ~52 chars per continuation line tags = sorted(heuristic_counts.items(), key=lambda kv: -kv[1]) - tag_strs = [f"{k}={v}" for k, v in tags] - lines, current = [], "" - for ts in tag_strs: - if current and len(current) + 2 + len(ts) > 52: - lines.append(current) - current = ts - else: - current = (current + " " + ts).lstrip() - if current: - lines.append(current) + joined = " ".join(f"{k}={v}" for k, v in tags) + lines = textwrap.wrap(joined, width=52, break_long_words=False, break_on_hyphens=False) or [""] print(f"[!] Heuristics: {lines[0]}") for line in lines[1:]: print(f" {line}") diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 2f54d652..0945594d 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -60,8 +60,11 @@ def _callbackAnalysisTimeout(self): if elapsed_seconds >= self._timeout: LOGGER.debug("Current analysis callback time %s", time_diff) return True - if elapsed_seconds >= 30 and elapsed_seconds % 30 == 0 and elapsed_seconds != self._last_timeout_log_second: - self._last_timeout_log_second = elapsed_seconds + # Log on 30s bucket transitions (not exact-second boundaries) so the message + # still fires when callback timing skips past 30/60/... whole seconds. + current_bucket = elapsed_seconds // 30 + if current_bucket >= 1 and current_bucket != self._last_timeout_log_second: + self._last_timeout_log_second = current_bucket LOGGER.debug("Current analysis callback time %s", time_diff) return False diff --git a/smda/common/SmdaFunction.py b/smda/common/SmdaFunction.py index 1156f979..537fee73 100644 --- a/smda/common/SmdaFunction.py +++ b/smda/common/SmdaFunction.py @@ -68,6 +68,9 @@ def __init__(self, disassembly=None, function_offset=None, config=None, smda_rep if function_offset in disassembly.candidates else None ) + # DEX strings are part of the parsed file structure, so they're always + # populated for Dalvik regardless of WITH_STRINGS — no extra extraction + # cost. For other architectures, honor WITH_STRINGS as usual. if ( config and config.WITH_STRINGS @@ -261,15 +264,14 @@ def _getCfgRoot(self, normalized_blockrefs): if block_start is not None: return block_start if normalized_blockrefs: - fallback_root = min(normalized_blockrefs) + # No entry block found for self.offset — refuse to fabricate a root, + # since dominator/nesting derived from a wrong root is silently misleading. LOGGER.warning( - "Normalized CFG for %s (0x%x) has no entry block 0x%x, falling back to 0x%x.", + "Normalized CFG for %s (0x%x) has no entry block; skipping root-dependent analysis.", self.function_name or "", self.offset, - self.offset, - fallback_root, ) - return fallback_root + return None LOGGER.warning("Normalized CFG for %s (0x%x) is empty.", self.function_name or "", self.offset) return None diff --git a/smda/common/SmdaInstruction.py b/smda/common/SmdaInstruction.py index 10492f26..6bb08092 100644 --- a/smda/common/SmdaInstruction.py +++ b/smda/common/SmdaInstruction.py @@ -25,13 +25,12 @@ def __init__(self, ins_list=None, smda_function=None): self.operands = ins_list[3] def getDataRefs(self): - explicit_refs = set() + emitted = set() smda_report = self.smda_function.smda_report if smda_report.data_refs_from is not None and self.offset in smda_report.data_refs_from: - explicit_refs.update(smda_report.data_refs_from[self.offset]) - yield from sorted(explicit_refs) - if explicit_refs: - return + for value in sorted(smda_report.data_refs_from[self.offset]): + emitted.add(value) + yield value if smda_report.architecture != "intel": return if self.getMnemonicGroup(IntelInstructionEscaper) != "C": @@ -46,7 +45,12 @@ def getDataRefs(self): if detailed.reg_name(i.mem.base) == "rip": # add RIP value value += detailed.address + detailed.size - if value is not None and self.smda_function.smda_report.isAddrWithinMemoryImage(value): + if ( + value is not None + and value not in emitted + and self.smda_function.smda_report.isAddrWithinMemoryImage(value) + ): + emitted.add(value) yield value def getDetailed(self): diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index 49a207b1..aa8c2220 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -627,14 +627,17 @@ def _buildValidInstructionStarts(self, bytecode): payload_ranges = [] idx = 0 null_resolve = lambda ref_kind, ref_index: "" # noqa: E731 + # Dalvik instructions are 16-bit aligned (one "code unit"), so always + # advance by 2 on resync — stepping by 1 wastes a decode attempt at every + # odd offset on adversarial input. while idx < len(bytecode): if any(start <= idx < end for start, end in payload_ranges): - idx += 1 + idx += 2 continue try: decoded = decode_instruction(bytecode, idx, null_resolve) except ValueError: - idx += 1 + idx += 2 continue valid.add(idx) if decoded.payload_idx is not None: @@ -894,7 +897,18 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): with contextlib.suppress(Exception): dex_file = lief.DEX.parse(binary_info.file_path) if dex_file is None: - dex_file = lief.DEX.parse(list(binary_info.raw_data)) + # Prefer bytes/memoryview: list(raw_data) allocates a PyLong per byte + # (~30x memory blowup on large DEX). Older LIEF builds only accept + # List[int] and either raise TypeError or return None — fall back then. + raw_data = binary_info.raw_data + if not isinstance(raw_data, (bytes, bytearray)): + raw_data = bytes(raw_data) + try: + dex_file = lief.DEX.parse(raw_data) + except TypeError: + dex_file = None + if dex_file is None: + dex_file = lief.DEX.parse(list(raw_data)) if dex_file is None: raise ValueError("Failed to parse DEX file") From 47d70a5a23fa48d08a4a4bcad62a667befbf8ffd Mon Sep 17 00:00:00 2001 From: Rony <49360849+r0ny123@users.noreply.github.com> Date: Thu, 7 May 2026 20:31:19 +0530 Subject: [PATCH 25/25] Refactor: Rename DexReferenceResolver methods, fix autodetect backend - DexReferenceResolver: rename snake_case methods to camelCase to match the rest of the smda codebase (formatMethod, formatField, formatProto, formatTypeByIndex, formatRef, getMethod, getMethodTarget, getStringValue, getMethodMetadata, plus private helpers _indexItems, _safeGet, _safeAttr, _normalizeTypeString, _formatType, _formatProto). Module-level functions in DalvikOpcodeDecoder are left snake_case to match the existing style of StringExtractor/DominatorTree/CilDisassembler. - Disassembler.disassembleBuffer: when DEX magic is autodetected on a Disassembler that was constructed with an explicit Intel backend, reset self.disassembler so initDisassembler creates a DalvikDisassembler instead of running Intel on DEX bytes. - Add testDisassembleBufferIntelOnDexAutodetectsDalvik to pin the contract that magic-byte autodetect overrides explicit-Intel. Co-Authored-By: Claude Opus 4.7 (1M context) --- smda/Disassembler.py | 3 + smda/dalvik/DalvikDisassembler.py | 124 +++++++++++++++--------------- tests/testDalvikDisassembler.py | 25 ++++-- 3 files changed, 83 insertions(+), 69 deletions(-) diff --git a/smda/Disassembler.py b/smda/Disassembler.py index 0945594d..6ce12532 100644 --- a/smda/Disassembler.py +++ b/smda/Disassembler.py @@ -185,6 +185,9 @@ def disassembleBuffer( architecture = "dalvik" if bitness is None: bitness = DexFileLoader.getBitness(file_content) + # initDisassembler caches by self.disassembler-is-None, so a backend + # picked at construction time would otherwise win against autodetect. + self.disassembler = None binary_info = BinaryInfo(file_content) binary_info.base_addr = base_addr binary_info.bitness = bitness diff --git a/smda/dalvik/DalvikDisassembler.py b/smda/dalvik/DalvikDisassembler.py index aa8c2220..057f70d4 100644 --- a/smda/dalvik/DalvikDisassembler.py +++ b/smda/dalvik/DalvikDisassembler.py @@ -66,30 +66,30 @@ class DexReferenceResolver: def __init__(self, dex_file): self.dex_file = dex_file self.strings = list(getattr(dex_file, "strings", [])) - self.methods = self._index_items(getattr(dex_file, "methods", [])) - self.fields = self._index_items(getattr(dex_file, "fields", [])) - self.types = self._index_items(getattr(dex_file, "types", [])) - self.prototypes = self._index_items(getattr(dex_file, "prototypes", [])) - self.classes = self._index_items(getattr(dex_file, "classes", [])) + self.methods = self._indexItems(getattr(dex_file, "methods", [])) + self.fields = self._indexItems(getattr(dex_file, "fields", [])) + self.types = self._indexItems(getattr(dex_file, "types", [])) + self.prototypes = self._indexItems(getattr(dex_file, "prototypes", [])) + self.classes = self._indexItems(getattr(dex_file, "classes", [])) - def _index_items(self, items): + def _indexItems(self, items): indexed = {} for index, item in enumerate(items): indexed[getattr(item, "index", index)] = item return indexed - def _safe_get(self, collection, index): + def _safeGet(self, collection, index): if index in collection: return collection[index] return None - def _safe_attr(self, obj, attr, default=None): + def _safeAttr(self, obj, attr, default=None): try: return getattr(obj, attr) except Exception: return default - def _normalize_type_string(self, type_name): + def _normalizeTypeString(self, type_name): if not type_name: return None if type_name in {"V", "Z", "B", "S", "C", "I", "J", "F", "D"}: @@ -103,76 +103,76 @@ def _normalize_type_string(self, type_name): return f"L{type_name.replace('.', '/')};" return type_name - def _format_type(self, type_obj): + def _formatType(self, type_obj): if type_obj is None: return "" if isinstance(type_obj, str): - return self._normalize_type_string(type_obj) - fullname = self._safe_attr(type_obj, "fullname", None) + return self._normalizeTypeString(type_obj) + fullname = self._safeAttr(type_obj, "fullname", None) if fullname: - return self._normalize_type_string(fullname) - value = self._safe_attr(type_obj, "value", None) + return self._normalizeTypeString(fullname) + value = self._safeAttr(type_obj, "value", None) if value is not None: - fullname = self._safe_attr(value, "fullname", None) + fullname = self._safeAttr(value, "fullname", None) if fullname: - return self._normalize_type_string(fullname) - primitive_name = self.PRIMITIVE_TYPES.get(self._safe_attr(value, "name", ""), None) + return self._normalizeTypeString(fullname) + primitive_name = self.PRIMITIVE_TYPES.get(self._safeAttr(value, "name", ""), None) if primitive_name: return primitive_name - name = self._safe_attr(value, "name", None) + name = self._safeAttr(value, "name", None) if name: - return self._normalize_type_string(name) + return self._normalizeTypeString(name) with contextlib.suppress(Exception): - normalized = self._normalize_type_string(str(value)) + normalized = self._normalizeTypeString(str(value)) if normalized and not normalized.startswith("" - def _format_proto(self, prototype): + def _formatProto(self, prototype): if prototype is None: return "()" params = [] for param in getattr(prototype, "parameters_type", []): - params.append(self._format_type(param)) - return_type = self._format_type(getattr(prototype, "return_type", None)) + params.append(self._formatType(param)) + return_type = self._formatType(getattr(prototype, "return_type", None)) return f"({''.join(params)}){return_type}" - def format_method(self, method): + def formatMethod(self, method): if method is None: return "method@" - class_name = self._format_type(getattr(method, "cls", None)) + class_name = self._formatType(getattr(method, "cls", None)) method_name = getattr(method, "name", "") - prototype = self._format_proto(getattr(method, "prototype", None)) + prototype = self._formatProto(getattr(method, "prototype", None)) return f"{class_name}->{method_name}{prototype}" - def format_field(self, field): + def formatField(self, field): if field is None: return "field@" - class_name = self._format_type(getattr(field, "cls", None)) + class_name = self._formatType(getattr(field, "cls", None)) field_name = getattr(field, "name", "") - field_type = self._format_type(getattr(field, "type", None)) + field_type = self._formatType(getattr(field, "type", None)) return f"{class_name}->{field_name}:{field_type}" - def format_proto(self, index): - prototype = self._safe_get(self.prototypes, index) + def formatProto(self, index): + prototype = self._safeGet(self.prototypes, index) if prototype is None: return f"proto@{index}" - return self._format_proto(prototype) + return self._formatProto(prototype) - def format_type_by_index(self, index): - type_obj = self._safe_get(self.types, index) + def formatTypeByIndex(self, index): + type_obj = self._safeGet(self.types, index) if type_obj is None: return f"type@{index}" - return self._format_type(type_obj) + return self._formatType(type_obj) # Baksmali-style escape map for DEX string literals. _STRING_ESCAPE_MAP = { @@ -197,44 +197,44 @@ def _escapeDexString(cls, s): parts.append(f"\\u{ord(ch):04x}") return "".join(parts) - def format_ref(self, ref_kind, index): + def formatRef(self, ref_kind, index): if ref_kind == "string": if 0 <= index < len(self.strings): return '"' + self._escapeDexString(self.strings[index]) + '"' return f"string@{index}" if ref_kind == "type": - return self.format_type_by_index(index) + return self.formatTypeByIndex(index) if ref_kind == "field": - return self.format_field(self._safe_get(self.fields, index)) + return self.formatField(self._safeGet(self.fields, index)) if ref_kind == "method": - return self.format_method(self._safe_get(self.methods, index)) + return self.formatMethod(self._safeGet(self.methods, index)) if ref_kind == "proto": - return self.format_proto(index) + return self.formatProto(index) if ref_kind == "method_handle": return f"method_handle@{index}" if ref_kind == "call_site": return f"call_site@{index}" return f"{ref_kind}@{index}" if ref_kind else f"item@{index}" - def get_method(self, method_index): - return self._safe_get(self.methods, method_index) + def getMethod(self, method_index): + return self._safeGet(self.methods, method_index) - def get_method_target(self, method_index): - method = self.get_method(method_index) + def getMethodTarget(self, method_index): + method = self.getMethod(method_index) if method is None: return None, None code_offset = getattr(method, "code_offset", 0) code_info = getattr(method, "code_info", None) if code_offset and code_info: - return code_offset, self.format_method(method) - return None, self.format_method(method) + return code_offset, self.formatMethod(method) + return None, self.formatMethod(method) - def get_string_value(self, string_index): + def getStringValue(self, string_index): if 0 <= string_index < len(self.strings): return self.strings[string_index] return None - def get_method_metadata(self, method): + def getMethodMetadata(self, method): access_flags = getattr(method, "access_flags", 0) access_flags = getattr(access_flags, "value", access_flags) if isinstance(access_flags, (list, tuple, set)): @@ -243,11 +243,11 @@ def get_method_metadata(self, method): normalized_flags |= getattr(flag, "value", 0) access_flags = normalized_flags access_flag_names = [name for mask, name in self.ACCESS_FLAG_NAMES if access_flags & mask] - method_name = self.format_method(method) + method_name = self.formatMethod(method) return { "method_name": method_name, - "class_name": self._format_type(getattr(method, "cls", None)), - "prototype": self._format_proto(getattr(method, "prototype", None)), + "class_name": self._formatType(getattr(method, "cls", None)), + "prototype": self._formatProto(getattr(method, "prototype", None)), "access_flags": access_flags, "access_flags_decoded": access_flag_names, } @@ -414,7 +414,7 @@ def _resolveSwitchTargets(self, bytecode, switch_insn_idx, payload_idx): return targets def _resolveReference(self, resolver, ref_kind, ref_index): - return resolver.format_ref(ref_kind, ref_index) + return resolver.formatRef(ref_kind, ref_index) def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, tries_size): if tries_size == 0: @@ -468,7 +468,7 @@ def _parseTryBlocks(self, raw_data, resolver, bytecode_offset, insns_size_units, "handlers": [ { "type_idx": handler["type_idx"], - "type_name": resolver.format_type_by_index(handler["type_idx"]), + "type_name": resolver.formatTypeByIndex(handler["type_idx"]), "target_addr": bytecode_offset + handler["addr_units"] * 2, } for handler in resolved["handlers"] @@ -509,7 +509,7 @@ def _applyExceptionEdges(self, state, instruction_addr, decoded, try_blocks): return applied def _buildFunctionMetadata(self, resolver, method, code_item_header, try_blocks): - metadata = resolver.get_method_metadata(method) + metadata = resolver.getMethodMetadata(method) exception_handlers = [] try_ranges = [] for try_block in try_blocks: @@ -792,7 +792,7 @@ def analyzeFunction(self, dex_file, resolver, method): state.setNextInstructionReachable(not decoded.is_terminator) if decoded.ref_kind == "string" and decoded.ref_index is not None: - string_value = resolver.get_string_value(decoded.ref_index) + string_value = resolver.getStringValue(decoded.ref_index) if string_value is not None: self.disassembly.addStringRef(state.start_addr, i_address, string_value) if decoded.payload_idx is not None: @@ -847,9 +847,9 @@ def analyzeFunction(self, dex_file, resolver, method): call_target = None call_name = None if decoded.ref_kind == "method" and decoded.ref_index is not None: - call_target, call_name = resolver.get_method_target(decoded.ref_index) + call_target, call_name = resolver.getMethodTarget(decoded.ref_index) elif decoded.ref_index is not None: - call_name = resolver.format_ref(decoded.ref_kind, decoded.ref_index) + call_name = resolver.formatRef(decoded.ref_kind, decoded.ref_index) if call_target is not None: state.addCodeRef(i_address, call_target) if call_target == state.start_addr: @@ -872,7 +872,7 @@ def analyzeFunction(self, dex_file, resolver, method): break state.endBlock() - state.label = resolver.format_method(method) + state.label = resolver.formatMethod(method) state.finalizeAnalysis() self._logMethodDiagnostics(state) return state @@ -954,7 +954,7 @@ def analyzeBuffer(self, binary_info, cbAnalysisTimeout=None): except Exception as exc: LOGGER.warning( "Failed to analyze Dalvik method %s @0x%x: %s", - resolver.format_method(method), + resolver.formatMethod(method), getattr(method, "code_offset", 0), exc, ) diff --git a/tests/testDalvikDisassembler.py b/tests/testDalvikDisassembler.py index a6f3176e..eea86d37 100644 --- a/tests/testDalvikDisassembler.py +++ b/tests/testDalvikDisassembler.py @@ -47,24 +47,24 @@ def __init__(self, code_offset=0x10): class SyntheticDalvikResolver: - def format_ref(self, ref_kind, ref_index): + def formatRef(self, ref_kind, ref_index): return f"{ref_kind}@{ref_index}" - def format_type_by_index(self, index): + def formatTypeByIndex(self, index): return f"type@{index}" - def get_string_value(self, string_index): + def getStringValue(self, string_index): return None - def get_method_target(self, method_index): + def getMethodTarget(self, method_index): return None, f"method@{method_index}" - def format_method(self, method): + def formatMethod(self, method): return "LSynthetic;->method()V" - def get_method_metadata(self, method): + def getMethodMetadata(self, method): return { - "method_name": self.format_method(method), + "method_name": self.formatMethod(method), "class_name": "LSynthetic;", "prototype": "()V", "access_flags": 0, @@ -394,6 +394,17 @@ def testDisassembleBufferDexAutodetect(self): self.assertEqual(report.bitness, 32) self.assertGreater(report.num_functions, 2000) + def testDisassembleBufferIntelOnDexAutodetectsDalvik(self): + # Pinning the contract: an explicit architecture="intel" on DEX bytes + # is overridden by magic-byte autodetection rather than producing + # garbage Intel disassembly. If the autodetect ever becomes opt-in, + # this test must be updated to assert error/empty Intel output instead. + intel_disasm = Disassembler(config, backend="intel") + report = intel_disasm.disassembleBuffer(self.dex_binary, base_addr=0, architecture="intel") + self.assertEqual(report.architecture, "dalvik") + self.assertEqual(report.bitness, 32) + self.assertGreater(report.num_functions, 2000) + def testAnalyzeScriptRawDexAvoidsBaseAddrAndOepWarnings(self): result = subprocess.run( [