def get_tcache_bin_chunks(self, index): """Fetches the chunks' addresses in the tcache_perthread_struct.entries[] array for the specified index :return: the list of addresses in this specified bin """ ptm = self.ptm tcache = ptm.cache.tcache dbg = self.ptm.dbg if tcache.entries[index] == 0: return [] # I've seen uninitialized entries[] still holding old data i.e. non-null # even though the counts is 0 if tcache.counts[index] == 0: return [] addr = tcache.entries[index] - 2 * ptm.SIZE_SZ p = mc.malloc_chunk(ptm, addr, inuse=False, debugger=dbg, allow_invalid=True, tcache=True) if not p.initOK: # afaict should not happen in a normal scenario but better be safe return [] addresses = [] while True: addresses.append(p.address) if p.next == 0x0: break addr = p.next - 2 * ptm.SIZE_SZ p = mc.malloc_chunk(ptm, addr, inuse=False, debugger=dbg, allow_invalid=True, tcache=True) if not p.initOK: # same return addresses return addresses
def get_fast_bin_chunks(self, index): """Fetches the chunks' addresses in the malloc_state.fastbinsY[] array for the specified index :return: the list of addresses in this specified bin """ ptm = self.ptm mstate = ptm.cache.mstate dbg = self.ptm.dbg fb_base = int(mstate.address) + mstate.fastbins_offset p = mc.malloc_chunk( ptm, addr=fb_base - (2 * ptm.SIZE_SZ) + index * ptm.SIZE_SZ, fast=True, debugger=dbg, allow_invalid=True, ) addresses = [] while p.fd != 0: if p.fd is None: break addresses.append(p.fd) p = mc.malloc_chunk( ptm, p.fd, fast=True, debugger=dbg, allow_invalid=True, ) return addresses
def compact_listing(self): """Print all the chunks in a given arena using a compact flat listing """ max_count = self.args.count pu.print_title("{:>15} for arena @ {:#x}".format("compact flat heap listing", self.cache.mstate.address), end="\n") if self.ptm.SIZE_SZ == 4: # Workaround on 32-bit. Empirically it seems the first chunk starts at offset +0x8? addr = self.sbrk_base+8 else: addr = self.sbrk_base count = 0 while True: p = mc.malloc_chunk( self.ptm, addr, read_data=False, debugger=self.dbg, use_cache=True ) if p.address == self.ptm.top(self.cache.mstate): print("|T", end="") count += 1 break if p.type == pt.chunk_type.FREE_FAST: print("|f%d" % self.ptm.fast_bin_index(self.ptm.chunksize(p)), end="") elif p.type == pt.chunk_type.FREE_TCACHE: print("|t%d" % self.ptm.tcache_bin_index(self.ptm.chunksize(p)), end="") elif p.type == pt.chunk_type.INUSE: print("|M", end="") else: if ( (p.fd == self.cache.mstate.last_remainder) and (p.bk == self.cache.mstate.last_remainder) and (self.cache.mstate.last_remainder != 0) ): print("|L", end="") else: print("|F%d" % self.ptm.bin_index(self.ptm.chunksize(p)), end="") count += 1 sys.stdout.flush() if max_count != None and count == max_count: break addr = self.ptm.next_chunk(p) print("|") if max_count == None: print(f"Total of {count} chunks") else: print(f"Total of {count}+ chunks")
def get_bin_chunks(self, index): """Fetches the chunks' addresses in the malloc_state.bins[] array for the specified index :return: the list of addresses in this specified bin """ log.debug("get_bin_chunks(%d)" % index) ptm = self.ptm mstate = ptm.cache.mstate dbg = self.ptm.dbg #ptm.mutex_lock(mstate) b = ptm.bin_at(mstate, index+1) if b == 0: # Not initialized yet return [] p = mc.malloc_chunk( ptm, b, inuse=False, debugger=dbg, tcache=False, fast=False, allow_invalid=True) addresses = [] while p.fd != int(b): addresses.append(p.address) p = mc.malloc_chunk( ptm, ptm.first(p), inuse=False, debugger=dbg, tcache=False, fast=False, allow_invalid=True) #ptm.mutex_unlock(mstate) return addresses
def show_stats(self): """Show a summary of the memory statistics """ self.cache.update_arena(show_status=False) self.cache.update_param(show_status=False) self.cache.update_tcache(show_status=False) self.cache.update_tcache_bins(show_status=False) main_arena_address = self.cache.main_arena_address par = self.cache.par in_use_b = par.mmapped_mem avail_b = 0 system_b = in_use_b pu.print_title("Malloc Stats", end="\n\n") arena = 0 mstate = ms.malloc_state( self.ptm, main_arena_address, debugger=self.dbg, version=self.version ) while 1: self.cache.update_arena(address=mstate.address, show_status=False) self.cache.update_fast_bins(show_status=False) self.cache.update_bins(show_status=False) if mstate.address == self.cache.main_arena_address: sbrk_base, _ = self.dbg.get_heap_address(par) else: sbrk_base = (mstate.address + mstate.size + self.ptm.MALLOC_ALIGN_MASK) & ~self.ptm.MALLOC_ALIGN_MASK avail = 0 inuse = 0 nblocks = 1 addr = sbrk_base while True: p = mc.malloc_chunk( self.ptm, addr, read_data=False, debugger=self.dbg, use_cache=True ) if p.address == self.ptm.top(self.cache.mstate): avail += self.ptm.chunksize(p) break if p.type == pt.chunk_type.FREE_FAST: avail += self.ptm.chunksize(p) elif p.type == pt.chunk_type.FREE_TCACHE: avail += self.ptm.chunksize(p) elif p.type == pt.chunk_type.INUSE: inuse += self.ptm.chunksize(p) else: avail += self.ptm.chunksize(p) nblocks += 1 addr = self.ptm.next_chunk(p) pu.print_header("Arena {} @ {:#x}:".format(arena, mstate.address), end="\n") print("{:16} = ".format("system bytes"), end="") pu.print_value("{} ({:#x})".format(mstate.max_system_mem, mstate.max_system_mem), end="\n") print("{:16} = ".format("free bytes"), end="") pu.print_value("{} ({:#x})".format(avail, avail), end="\n") print("{:16} = ".format("in use bytes"), end="") pu.print_value("{} ({:#x})".format(inuse, inuse), end="\n") system_b += mstate.max_system_mem avail_b += avail in_use_b += inuse if mstate.next == main_arena_address: break else: next_addr = self.dbg.format_address(mstate.next) mstate = ms.malloc_state( self.ptm, next_addr, debugger=self.dbg, version=self.version ) arena += 1 pu.print_header("\nTotal (including mmap):", end="\n") print("{:16} = ".format("system bytes"), end="") pu.print_value("{} ({:#x})".format(system_b, system_b), end="\n") print("{:16} = ".format("free bytes"), end="") pu.print_value("{} ({:#x})".format(avail_b, avail_b), end="\n") print("{:16} = ".format("in use bytes"), end="") pu.print_value("{} ({:#x})".format(in_use_b, in_use_b), end="\n") if self.version <= 2.23: # catch the error before we print anything val = par.max_total_mem print("{:16} = ".format("max system bytes"), end="") pu.print_value("{}".format(val), end="\n") print("{:16} = ".format("max mmap regions"), end="") pu.print_value("{}".format(par.max_n_mmaps), end="\n") print("{:16} = ".format("max mmap bytes"), end="") pu.print_value("{}".format(par.max_mmapped_mem), end="\n")
def parse_many(address, ptm, dbg=None, count=1, count_handle=None, search_depth=0, skip_header=False, hexdump_unit=1, search_value=None, search_type=None, match_only=False, print_offset=0, verbose=0, no_newline=False, debug=False, hexdump=False, maxbytes=0, metadata=None, highlight_types=[], highlight_addresses=[], highlight_metadata=[], highlight_only=False, inuse=None, tcache=None, fast=None, allow_invalid=False, header_once=None, commands=None, use_cache=False, address_offset=False ): """Parse many chunks starting from a given address and show them based passed arguments :param address: chunk's address to start parsing from :param ptm: ptmalloc object (libptmalloc constants and helpers) :param dbg: pydbg object (debugger interface) :param count: see ptchunk's ArgumentParser definition maximum number of chunks to print, or None if unlimited :param count_handle: maximum number of chunks to handle per address, even if not printed, or None if unlimited :param search_depth: see ptchunk's ArgumentParser definition :param skip_header: see ptchunk's ArgumentParser definition :param hexdump_unit: see ptchunk's ArgumentParser definition :param search_value: see ptchunk's ArgumentParser definition :param search_type: see ptchunk's ArgumentParser definition :param match_only: see ptchunk's ArgumentParser definition :param print_offset: see ptchunk's ArgumentParser definition :param verbose: see ptchunk's ArgumentParser definition :param no_newline: see ptchunk's ArgumentParser definition :param debug: see ptchunk's ArgumentParser definition :param hexdump: see ptchunk's ArgumentParser definition :param maxbytes: see ptchunk's ArgumentParser definition :param metadata: see ptchunk's ArgumentParser definition :param highlight_types: list of types. highlight chunks with matching type with a '*' e.g. to be used by 'ptlist' :param highlight_addresses: list of addresses. highlight chunks with matching address with a '*' e.g. to be used by 'ptlist' :param highlight_metadata: list of metadata. highlight chunks with matching metadata with a '*' e.g. to be used by 'ptlist' :param highlight_only: see ptchunk's ArgumentParser definition :param inuse: True if we know all the chunks are inuse (i.e. not in any bin) False if we know they are NOT in inuse. None otherwise. Useful to specify when parsing a regular bin :param tcache: True if we know all the chunks are in the tcache bins, False if we know they are NOT in the tcache bins. None otherwise. Useful to specify when parsing a tcache bin :param fast: Same as "tcache" but for fast bins :param allow_invalid: sometimes these structures will be used for that isn't actually a complete chunk, like a freebin, in these cases we still wanted to be able to parse so that we can access the forward and backward pointers, so shouldn't complain about their being invalid size :param header_once: string to print before printing the first chunk, or None if not needed :param commands: see ptchunk's ArgumentParser definition :param use_cache: see ptchunk's ArgumentParser definition :param address_offset: see ptchunk's ArgumentParser definition :return: the list of malloc_chunk being parsed and already shown """ chunks = [] highlight_types2 = [] highlight_types = set(highlight_types) for t in highlight_types: if t == "M": highlight_types2.append(pt.chunk_type.INUSE) elif t == "F": highlight_types2.append(pt.chunk_type.FREE_SMALL) highlight_types2.append(pt.chunk_type.FREE_LARGE) elif t == "f": highlight_types2.append(pt.chunk_type.FREE_FAST) elif t == "t": highlight_types2.append(pt.chunk_type.FREE_TCACHE) else: print("ERROR: invalid chunk type provided, should not happen") return [] highlight_addresses = set(highlight_addresses) highlight_metadata = set(highlight_metadata) highlight_metadata_found = set([]) p = mc.malloc_chunk( ptm, addr=address, debugger=dbg, use_cache=use_cache, tcache=tcache, fast=fast, allow_invalid=allow_invalid ) if not p.initOK: return first_address = p.address dump_offset = 0 while True: prefix = "" # used for one-line output suffix = "" # used for one-line output epilog = "" # used for verbose output colorize_func = str # do not colorize by default if metadata is not None: opened = False list_metadata = [e.strip() for e in metadata.split(",")] L, s, e, colorize_func = ptmeta.get_metadata(p.address, list_metadata=list_metadata) suffix += s epilog += e p.metadata = L # save so we can easily export to json later if search_value is not None: if not dbg.search_chunk( ptm, p, search_value, search_type=search_type, depth=search_depth, skip=skip_header ): found_match = False suffix += " [NO MATCH]" else: suffix += pu.light_green(" [MATCH]") found_match = True # XXX - the current representation is not really generic as we print the first short # as an ID and the second 2 bytes as 2 characters. We may want to support passing the # format string as an argument but this is already useful if print_offset != 0: mem = dbg.read_memory( p.data_address + print_offset, 4 ) (id_, desc) = struct.unpack_from("<H2s", mem, 0x0) if h.is_ascii(desc): suffix += " 0x%04x %s" % (id_, str(desc, encoding="utf-8")) else: suffix += " 0x%04x hex(%s)" % ( id_, str(binascii.hexlify(desc), encoding="utf-8"), ) # Only print the chunk type for non verbose if p.address == ptm.cache.par.sbrk_base: suffix += " (sbrk_base)" elif p.address == ptm.top(ptm.cache.mstate): suffix += " (top)" printed = False if verbose == 0: found_highlight = False # Only highlight chunks for non verbose if p.address in highlight_addresses: found_highlight = True highlight_addresses.remove(p.address) if p.type in highlight_types2: found_highlight = True if len(highlight_metadata) > 0: # We retrieve all metadata since we want to highlight chunks containing any of the # metadata, even if we don't show some of the metadata _, s, _, _ = ptmeta.get_metadata(p.address, list_metadata="all") for m in highlight_metadata: # we check in the one-line output as it should have less non-useful information if m in s: found_highlight = True highlight_metadata_found.add(m) if found_highlight: prefix += "* " if (not highlight_only or found_highlight) \ and (not match_only or found_match): if header_once != None: print(header_once) header_once = None if no_newline: print(prefix + ptm.chunk_info(p, colorize_func=colorize_func, first_address=first_address, address_offset=address_offset) + suffix, end="") else: print(prefix + ptm.chunk_info(p, colorize_func=colorize_func, first_address=first_address, address_offset=address_offset) + suffix) printed = True elif verbose >= 1 and (not match_only or found_match): if header_once != None: print(header_once) header_once = None print(p) printed = True # XXX - this is old code used in Cisco ASA. Need removal or merge? if ptm.ptchunk_callback is not None: size = ptm.chunksize(p) - p.hdr_size if p.data_address is not None: # We can provide an excess of information and the # callback can choose what to use cbinfo = {} cbinfo["caller"] = "ptchunk" cbinfo["allocator"] = "ptmalloc" cbinfo["addr"] = p.data_address cbinfo["hdr_sz"] = p.hdr_size cbinfo["chunksz"] = ptm.chunksize(p) cbinfo["min_hdr_sz"] = ptm.INUSE_HDR_SZ cbinfo["data_size"] = size cbinfo["inuse"] = p.inuse cbinfo["size_sz"] = ptm.SIZE_SZ if debug: cbinfo["debug"] = True print(cbinfo) # We expect callback to tell us how much data it # 'consumed' in printing out info dump_offset = ptm.ptchunk_callback(cbinfo) # mem-based callbacks not yet supported if printed: if hexdump: dbg.print_hexdump_chunk(ptm, p, maxlen=maxbytes, off=dump_offset, unit=hexdump_unit, verbose=verbose) if verbose >= 1 and epilog: print(epilog, end="") if commands: for command in commands.split(";"): formatted_command = command.replace("@", f"{p.address:#x}") print(dbg.execute(formatted_command)) chunks.append(p) if count != None: count -= 1 if count_handle != None: count_handle -= 1 if count != 0 and count_handle != 0: if printed and (verbose >= 1 or hexdump): print("--") if p.is_top: # Only print the chunk type for non verbose if verbose == 0: if ptm.cache.mstate.address == ptm.cache.main_arena_address: start = ptm.cache.par.sbrk_base else: # XXX - seems mstate is at offset 0x20 so there is 0x10 unknown bytes and 0x10 bytes for the chunk # header holding the mstate. So aligning to page works? start = ((ptm.cache.mstate.address & ~0xfff) + ptm.MALLOC_ALIGN_MASK) & ~ptm.MALLOC_ALIGN_MASK end = int(start + ptm.cache.mstate.max_system_mem) if address_offset is True: end -= first_address if ptm.cache.mstate.address == ptm.cache.main_arena_address: print("{:#x}".format(end), end="") print(" (sbrk_end)") else: print("{:#x}".format(end)) else: print("Stopping due to end of heap") break p = mc.malloc_chunk( ptm, addr=(p.address + ptm.chunksize(p)), debugger=dbg, use_cache=use_cache, tcache=tcache, fast=fast, allow_invalid=allow_invalid ) if not p.initOK: break else: break if len(highlight_addresses) != 0: pu.print_error("WARNING: Could not find these chunk addresses: %s" % (", ".join(["0x%x" % x for x in highlight_addresses]))) if len(highlight_metadata-highlight_metadata_found) != 0: pu.print_error("WARNING: Could not find these metadata: %s" % (", ".join(list(highlight_metadata-highlight_metadata_found)))) return chunks
def prepare_args_if_negative_count(self): """This is a little bit of a hack. The idea is to handle cases where the user wants to print N chunks going backwards. We are going to list all the chunks in the arena until we find all the addresses requested and then craft new arguments as if the user requested to print from new addresses N chunks before the requested addresses before calling parse_many2() """ self.args.reverse = False # Nothing to do if the count is positive or unlimited if self.args.count == None or self.args.count >= 0: return # We are making the count positive self.args.count = self.args.count*-1 # And we print N chunks before the requested chunk + the actual chunk self.args.count += 1 addresses = self.dbg.parse_address(self.args.addresses) if len(addresses) == 0: pu.print_error("WARNING: No valid address supplied") self.parser.print_help() return [] # We will fill it with new addresses later below self.args.addresses = [] # Let's get all the chunks' addresses in the arena mstate = self.cache.mstate par = self.cache.par if mstate.address == self.cache.main_arena_address: addr, _ = self.dbg.get_heap_address(par) else: print("Using manual arena calculation for heap start") addr = (mstate.address + mstate.size + self.ptm.MALLOC_ALIGN_MASK) & ~self.ptm.MALLOC_ALIGN_MASK chunks_addresses = [] chunks_addresses.append(addr) while True: p = mc.malloc_chunk( self.ptm, addr, read_data=False, debugger=self.dbg, use_cache=True ) if not p.initOK: pu.print_error("WARNING: Stopping due to invalid chunk parsed in arena") break chunks_addresses.append(addr) if p.address == self.ptm.top(self.cache.mstate): break addr = self.ptm.next_chunk(p) # Prepare arguments for "ptchunk" format # i.e. for every address, get the new address N chunks before for addr in addresses: try: index = chunks_addresses.index(addr) except ValueError: pu.print_error(f"WARNING: Could not find {addr:#x} in arena, skipping") continue index -= self.args.count if index < 0: pu.print_error(f"WARNING: Reaching beginning of arena with {addr:#x}") index = 0 self.args.addresses.append(f"{chunks_addresses[index]:#x}")