diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index 69b4d3fe8..8c18d1930 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -321,6 +321,11 @@ class ArgparsedCommand: _ArgparsedCommand(self.parser, function, alias) return _ArgparsedCommand(self.parser, function) +# We use a 64-bit max value literal here instead of pwndbg.arch.current +# as realistically its ok to pull off the biggest possible type here +# We cache its GDB value type which is 'unsigned long long' +_mask = 0xffffffffFFFFFFFF +_mask_val_type = gdb.Value(_mask).type def sloppy_gdb_parse(s): """ @@ -333,6 +338,34 @@ def sloppy_gdb_parse(s): :return: Whatever gdb.parse_and_eval returns or string. """ try: - return gdb.parse_and_eval(s) + val = gdb.parse_and_eval(s) + # We can't just return int(val) because GDB may return: + # "Python Exception Cannot convert value to long." + # e.g. for: + # pwndbg> pi int(gdb.parse_and_eval('__libc_start_main')) + # + # Here, the _mask_val.type should be `unsigned long long` + return int(val.cast(_mask_val_type)) except (TypeError, gdb.error): return s + +def AddressExpr(s): + """ + Parses an address expression. Returns an int. + """ + val = sloppy_gdb_parse(s) + + if not isinstance(val, int): + raise argparse.ArgumentError('Incorrect address (or GDB expression): %s' % s) + + return val + +def HexOrAddressExpr(s): + """ + Parses string as hexidecimal int or an address expression. Returns an int. + (e.g. '1234' will return 0x1234) + """ + try: + return int(s, 16) + except ValueError: + return AddressExpr(s) diff --git a/pwndbg/commands/hexdump.py b/pwndbg/commands/hexdump.py index 23e25c161..03a8beba3 100644 --- a/pwndbg/commands/hexdump.py +++ b/pwndbg/commands/hexdump.py @@ -41,7 +41,7 @@ def address_or_module_name(s): raise argparse.ArgumentTypeError('Unknown hexdump argument type.') parser = argparse.ArgumentParser(description='Hexdumps data at the specified address or module name (or at $sp)') -parser.add_argument('address_or_module', type=address_or_module_name, nargs='?', default='$sp', +parser.add_argument('address', type=address_or_module_name, nargs='?', default='$sp', help='Address or module name to dump') parser.add_argument('count', nargs='?', default=pwndbg.config.hexdump_bytes, help='Number of bytes to dump') @@ -49,8 +49,7 @@ parser.add_argument('count', nargs='?', default=pwndbg.config.hexdump_bytes, @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning -def hexdump(address_or_module=None, count=pwndbg.config.hexdump_bytes): - address = address_or_module +def hexdump(address=None, count=pwndbg.config.hexdump_bytes): if hexdump.repeat: address = hexdump.last_address hexdump.offset += 1 diff --git a/pwndbg/commands/windbg.py b/pwndbg/commands/windbg.py index 1d779289c..2df0b8971 100644 --- a/pwndbg/commands/windbg.py +++ b/pwndbg/commands/windbg.py @@ -29,8 +29,8 @@ def get_type(size): }[size] parser = argparse.ArgumentParser(description="Starting at the specified address, dump N bytes.") -parser.add_argument("address", type=int, help="The address to dump from.") -parser.add_argument("count", type=int, default=64, nargs="?", help="The number of bytes to dump.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") +parser.add_argument("count", type=pwndbg.commands.AddressExpr, default=64, nargs="?", help="The number of bytes to dump.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def db(address, count=64): @@ -42,8 +42,8 @@ def db(address, count=64): parser = argparse.ArgumentParser(description="Starting at the specified address, dump N words.") -parser.add_argument("address", type=int, help="The address to dump from.") -parser.add_argument("count", type=int, default=32, nargs="?", help="The number of words to dump.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") +parser.add_argument("count", type=pwndbg.commands.AddressExpr, default=32, nargs="?", help="The number of words to dump.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def dw(address, count=32): @@ -55,8 +55,8 @@ def dw(address, count=32): parser = argparse.ArgumentParser(description="Starting at the specified address, dump N dwords.") -parser.add_argument("address", type=int, help="The address to dump from.") -parser.add_argument("count", type=int, default=16, nargs="?", help="The number of dwords to dump.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") +parser.add_argument("count", type=pwndbg.commands.AddressExpr, default=16, nargs="?", help="The number of dwords to dump.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def dd(address, count=16): @@ -67,8 +67,8 @@ def dd(address, count=16): return dX(4, address, count, repeat=dd.repeat) parser = argparse.ArgumentParser(description="Starting at the specified address, dump N qwords.") -parser.add_argument("address", type=int, help="The address to dump from.") -parser.add_argument("count", type=int, default=8, nargs="?", help="The number of qwords to dump.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") +parser.add_argument("count", type=pwndbg.commands.AddressExpr, default=8, nargs="?", help="The number of qwords to dump.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def dq(address, count=8): @@ -79,12 +79,12 @@ def dq(address, count=8): return dX(8, address, count, repeat=dq.repeat) parser = argparse.ArgumentParser(description="Starting at the specified address, hexdump.") -parser.add_argument("address", type=int, help="The address to dump from.") -parser.add_argument("count", type=int, default=8, nargs="?", help="The number of bytes to hexdump.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") +parser.add_argument("count", type=pwndbg.commands.AddressExpr, default=8, nargs="?", help="The number of bytes to hexdump.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning def dc(address, count=8): - return pwndbg.commands.hexdump.hexdump(address=address, count=count, repeat=dc.repeat) + return pwndbg.commands.hexdump.hexdump(address=address, count=count) def dX(size, address, count, to_string=False, repeat=False): """ @@ -109,6 +109,10 @@ def dX(size, address, count, to_string=False, repeat=False): except gdb.MemoryError: break + if not values: + print('Could not access the provided address') + return + n_rows = int(math.ceil(count * size / float(16))) row_sz = int(16 / size) rows = [values[i*row_sz:(i+1)*row_sz] for i in range(n_rows)] @@ -140,7 +144,7 @@ def enhex(size, value): parser = argparse.ArgumentParser(description="Write hex bytes at the specified address.") -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, nargs="*", help="The bytes to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -152,7 +156,7 @@ def eb(address, data): parser = argparse.ArgumentParser(description="Write hex words at the specified address.") -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, nargs="*", help="The words to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -164,7 +168,7 @@ def ew(address, data): parser = argparse.ArgumentParser(description="Write hex dwords at the specified address.") -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, nargs="*", help="The dwords to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -176,7 +180,7 @@ def ed(address, data): parser = argparse.ArgumentParser(description="Write hex qwords at the specified address.") -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, nargs="*", help="The qwords to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -188,7 +192,7 @@ def eq(address, data): parser = argparse.ArgumentParser(description="Write a string at the specified address.") -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, help="The string to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -199,7 +203,7 @@ def ez(address, data): return eX(1, address, data, hex=False) parser = argparse.ArgumentParser(description="Write a string at the specified address.") #TODO Is eza just ez? If so just alias. I had trouble finding windbg documentation defining ez -parser.add_argument("address", type=int, help="The address to write to.") +parser.add_argument("address", type=pwndbg.commands.HexOrAddressExpr, help="The address to write to.") parser.add_argument("data", type=str, help="The string to write.") @pwndbg.commands.ArgparsedCommand(parser) @pwndbg.commands.OnlyWhenRunning @@ -213,31 +217,47 @@ def eX(size, address, data, hex=True): """ This relies on windbg's default hex encoding being enforced """ - address = pwndbg.commands.fix(address) - - if address is None: + if not data: + print('Cannot write empty data into memory.') return - for i, bytestr in enumerate(data): - if hex: - bytestr = str(bytestr) + if hex: + # Early validation if all data is hex + for string in data: + if string.startswith('0x'): + string = string[2:] + + if any(ch not in '0123456789abcdefABCDEF' for ch in string): + print('Incorrect data format: it must all be a hex value (0x1234 or 1234, both interpreted as 0x1234)') + return - if bytestr.startswith('0x'): - bytestr = bytestr[2:] + writes = 0 + for i, string in enumerate(data): + if hex: + if string.startswith('0x'): + string = string[2:] - bytestr = bytestr.rjust(size*2, '0') + string = string.rjust(size*2, '0') - data = codecs.decode(bytestr, 'hex') + data = codecs.decode(string, 'hex') else: - data = bytestr + data = string if pwndbg.arch.endian == 'little': data = data[::-1] - pwndbg.memory.write(address + (i * size), data) + try: + pwndbg.memory.write(address + (i * size), data) + writes += 1 + except gdb.error: + print('Cannot access memory at address %#x' % address) + if writes > 0: + print('(Made %d writes to memory; skipping further writes)' % writes) + return + parser = argparse.ArgumentParser(description="Dump pointers and symbols at the specified address.") -parser.add_argument("addr", type=int, help="The address to dump from.") +parser.add_argument("addr", type=pwndbg.commands.HexOrAddressExpr, help="The address to dump from.") @pwndbg.commands.ArgparsedCommand(parser,aliases=['kd','dps','dqs']) #TODO are these really all the same? They had identical implementation... @pwndbg.commands.OnlyWhenRunning def dds(addr): @@ -249,28 +269,35 @@ def dds(addr): da_parser = argparse.ArgumentParser() da_parser.description = 'Dump a string at the specified address.' -da_parser.add_argument('address', help='Address to dump') +da_parser.add_argument('address', type=pwndbg.commands.HexOrAddressExpr, help='Address to dump') da_parser.add_argument('max', type=int, nargs='?', default=256, help='Maximum string length') @pwndbg.commands.ArgparsedCommand(da_parser) @pwndbg.commands.OnlyWhenRunning def da(address, max): - address = int(address) - address &= pwndbg.arch.ptrmask print("%x" % address, repr(pwndbg.strings.get(address, max))) ds_parser = argparse.ArgumentParser() ds_parser.description = 'Dump a string at the specified address.' -ds_parser.add_argument('address', help='Address to dump') +ds_parser.add_argument('address', type=pwndbg.commands.HexOrAddressExpr, help='Address to dump') ds_parser.add_argument('max', type=int, nargs='?', default=256, help='Maximum string length') @pwndbg.commands.ArgparsedCommand(ds_parser) @pwndbg.commands.OnlyWhenRunning def ds(address, max): - address = int(address) - address &= pwndbg.arch.ptrmask - print("%x" % address, repr(pwndbg.strings.get(address, max))) - + # We do change the max length to the default if its too low + # because the truncated display is not that ideal/not the same as GDB's yet + # (ours: "truncated ...", GDBs: "truncated "...) + if max < 256: + print('Max str len of %d too low, changing to 256' % max) + max = 256 + + string = pwndbg.strings.get(address, max, maxread=4096) + if string: + print("%x %r" % (address, string)) + else: + print("Data at address can't be dereferenced or is not a printable null-terminated string or is too short.") + print("Perhaps try: db
or hexdump
") @pwndbg.commands.ArgparsedCommand("List breakpoints.") def bl(): diff --git a/pwndbg/memory.py b/pwndbg/memory.py index 9a47e9054..e00dccfc3 100644 --- a/pwndbg/memory.py +++ b/pwndbg/memory.py @@ -95,6 +95,7 @@ def write(addr, data): if isinstance(data, str): data = bytes(data, 'utf8') + # Throws an exception if can't access memory gdb.selected_inferior().write_memory(addr, data) @@ -146,8 +147,10 @@ def string(addr, max=4096): if peek(addr): data = bytearray(read(addr, max, partial=True)) - if b'\x00' in data: - return data.split(b'\x00')[0] + try: + return data[:data.index(b'\x00')] + except ValueError: + pass return bytearray() diff --git a/pwndbg/strings.py b/pwndbg/strings.py index 1bc0fdae3..3fc83faea 100644 --- a/pwndbg/strings.py +++ b/pwndbg/strings.py @@ -32,17 +32,27 @@ def update_length(): else: length = int(message) -def get(address, maxlen = None): +def get(address, maxlen=None, maxread=None): + """ + Returns a printable C-string from address. + + Returns `None` if string contains non-printable chars + or if the `maxlen` length data does not end up with a null byte. + """ if maxlen is None: maxlen = length + if maxread is None: + maxread = length + try: - sz = pwndbg.memory.string(address, maxlen) - sz = sz.decode('latin-1', 'replace') + sz = pwndbg.memory.string(address, maxread) + except gdb.error: # should not happen, but sanity check? + return None + + sz = sz.decode('latin-1', 'replace') - if not sz or not all(s in string.printable for s in sz): - return None - except Exception as e: + if not sz or not all(s in string.printable for s in sz): return None if len(sz) < maxlen or not maxlen: diff --git a/tests/binaries/memory.asm b/tests/binaries/memory.asm new file mode 100644 index 000000000..615e67f62 --- /dev/null +++ b/tests/binaries/memory.asm @@ -0,0 +1,32 @@ +global _start + +; This binary is there to test commands that access memory +; like dq, dd, dw, db, dc etc. +; So while the program does nothing, we create some data for testing those + +_start: + nop + +data: + dq 0x0 + dq 0x1 + dq 0x0000000100000002 + dq 0x0001000200030004 + dq 0x0102030405060708 + +data2: + dq 0x1122334455667788 + dq 0x0123456789abcdef + dq 0x0 + dq 0xffffffffffffffff + dq 0x0011223344556677 + dq 0x8899aabbccddeeff + +short_str: + db "some cstring here" + db 0 + +long_str: + db "long string: " + times 300 db 'A' + db 0 diff --git a/tests/conftest.py b/tests/conftest.py index 763b233e2..bbc17510d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,12 +11,11 @@ _start_binary_called = False @pytest.fixture def start_binary(): """ - Returns function that launches given binary with 'start' command + Returns function that launches given binary with 'starti' command """ def _start_binary(path, *args): gdb.execute('file ' + path) - gdb.execute('break _start') - gdb.execute('run ' + ' '.join(args)) + gdb.execute('starti ' + ' '.join(args)) global _start_binary_called # if _start_binary_called: diff --git a/tests/test_windbg.py b/tests/test_windbg.py new file mode 100644 index 000000000..2a84b2d0a --- /dev/null +++ b/tests/test_windbg.py @@ -0,0 +1,259 @@ + +import gdb +import pwndbg + +import tests + +MEMORY_BINARY = tests.binaries.get('memory.out') +data_addr = '0x400081' + + +def test_windbg_dX_commands(start_binary): + """ + Tests windbg compatibility commands that dump memory + like dq, dw, db, ds etc. + """ + start_binary(MEMORY_BINARY) + + # Try to fail commands in different way + for cmd_prefix in ('dq', 'dd', 'dw', 'db'): + + # With a non-existent symbol + cmd = cmd_prefix + ' nonexistentsymbol' + assert gdb.execute(cmd, to_string=True) == ( + 'usage: XX [-h] address [count]\n' + "XX: error: argument address: invalid HexOrAddressExpr value: 'nonexistentsymbol'\n" + ).replace('XX', cmd_prefix) + + # With an invalid/unmapped address + cmd = cmd_prefix + ' 0' + assert gdb.execute(cmd, to_string=True) == 'Could not access the provided address\n' + + ################################################# + #### dq command tests + ################################################# + # Try `dq` with symbol, &symbol, 0x
and
without 0x prefix (treated as hex!) + dq1 = gdb.execute('dq data', to_string=True) + dq2 = gdb.execute('dq &data', to_string=True) + dq3 = gdb.execute('dq %s' % data_addr, to_string=True) + dq4 = gdb.execute('dq %s' % data_addr.replace('0x', ''), to_string=True) + assert dq1 == dq2 == dq3 == dq4 == ( + '0000000000400081 0000000000000000 0000000000000001\n' + '0000000000400091 0000000100000002 0001000200030004\n' + '00000000004000a1 0102030405060708 1122334455667788\n' + '00000000004000b1 0123456789abcdef 0000000000000000\n' + ) + + # Try `dq` with different counts + dq_count1 = gdb.execute('dq data 2', to_string=True) + dq_count2 = gdb.execute('dq &data 2', to_string=True) + dq_count3 = gdb.execute('dq %s 2' % data_addr, to_string=True) + assert dq_count1 == dq_count2 == dq_count3 == '0000000000400081 0000000000000000 0000000000000001\n' + + assert gdb.execute('dq data 1', to_string=True) == '0000000000400081 0000000000000000\n' + assert gdb.execute('dq data 3', to_string=True) == ( + '0000000000400081 0000000000000000 0000000000000001\n' + '0000000000400091 0000000100000002\n' + ) + + # Try 'dq' with count equal to a register, but lets set it before ;) + # also note that we use `data2` here + assert gdb.execute('set $eax=4', to_string=True) == '' # assert as a sanity check + assert gdb.execute('dq data2 $eax', to_string=True) == ( + '00000000004000a9 1122334455667788 0123456789abcdef\n' + '00000000004000b9 0000000000000000 ffffffffffffffff\n' + ) + + # See if we can repeat dq command (use count for shorter data) + assert gdb.execute('dq data2 2', to_string=True) == ( + '00000000004000a9 1122334455667788 0123456789abcdef\n' + ) + + # TODO/FIXME: Can we test command repeating here? Neither passing `from_tty=True` + # or setting `pwndbg.commands.windbg.dq.repeat = True` works here + #assert gdb.execute('dq data2 2', to_string=True, from_tty=True) == ( + # '00000000004000b9 0000000000000000 ffffffffffffffff\n' + #) + + ################################################# + #### dd command tests + ################################################# + dd1 = gdb.execute('dd data', to_string=True) + dd2 = gdb.execute('dd &data', to_string=True) + dd3 = gdb.execute('dd %s' % data_addr, to_string=True) + dd4 = gdb.execute('dd %s' % data_addr.replace('0x', ''), to_string=True) + assert dd1 == dd2 == dd3 == dd4 == ( + '0000000000400081 00000000 00000000 00000001 00000000\n' + '0000000000400091 00000002 00000001 00030004 00010002\n' + '00000000004000a1 05060708 01020304 55667788 11223344\n' + '00000000004000b1 89abcdef 01234567 00000000 00000000\n' + ) + + # count tests + assert gdb.execute('dd data 4', to_string=True) == ( + '0000000000400081 00000000 00000000 00000001 00000000\n' + ) + assert gdb.execute('dd data 3', to_string=True) == ( + '0000000000400081 00000000 00000000 00000001\n' + ) + + ################################################# + #### dw command tests + ################################################# + dw1 = gdb.execute('dw data', to_string=True) + dw2 = gdb.execute('dw &data', to_string=True) + dw3 = gdb.execute('dw %s' % data_addr, to_string=True) + dw4 = gdb.execute('dw %s' % data_addr.replace('0x', ''), to_string=True) + assert dw1 == dw2 == dw3 == dw4 == ( + '0000000000400081 0000 0000 0000 0000 0001 0000 0000 0000\n' + '0000000000400091 0002 0000 0001 0000 0004 0003 0002 0001\n' + '00000000004000a1 0708 0506 0304 0102 7788 5566 3344 1122\n' + '00000000004000b1 cdef 89ab 4567 0123 0000 0000 0000 0000\n' + ) + + # count tests + assert gdb.execute('dw data 8', to_string=True) == ( + '0000000000400081 0000 0000 0000 0000 0001 0000 0000 0000\n' + ) + + assert gdb.execute('dw data 8/2', to_string=True) == ( + '0000000000400081 0000 0000 0000 0000\n' + ) + + assert gdb.execute('dw data $eax', to_string=True) == ( + '0000000000400081 0000 0000 0000 0000\n' + ) + + ################################################# + #### db command tests + ################################################# + db1 = gdb.execute('db data', to_string=True) + db2 = gdb.execute('db &data', to_string=True) + db3 = gdb.execute('db %s' % data_addr, to_string=True) + db4 = gdb.execute('db %s' % data_addr.replace('0x', ''), to_string=True) + assert db1 == db2 == db3 == db4 == ( + '0000000000400081 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00\n' + '0000000000400091 02 00 00 00 01 00 00 00 04 00 03 00 02 00 01 00\n' + '00000000004000a1 08 07 06 05 04 03 02 01 88 77 66 55 44 33 22 11\n' + '00000000004000b1 ef cd ab 89 67 45 23 01 00 00 00 00 00 00 00 00\n' + ) + + # count tests + assert gdb.execute('db data 31', to_string=True) == ( + '0000000000400081 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00\n' + '0000000000400091 02 00 00 00 01 00 00 00 04 00 03 00 02 00 01\n' + ) + assert gdb.execute('db data $ax', to_string=True) == ( + '0000000000400081 00 00 00 00\n' + ) + + ################################################# + #### dc command tests + ################################################# + dc1 = gdb.execute('dc data', to_string=True) + dc2 = gdb.execute('dc &data', to_string=True) + dc3 = gdb.execute('dc %s' % data_addr, to_string=True) + dc4 = gdb.execute('dc %s' % data_addr.replace('0x', ''), to_string=True) + assert dc1 == dc2 == dc3 == dc4 == ( + '+0000 0x400081 00 00 00 00 00 00 00 00 ' + '│....│....│ │ │\n' + ) + + assert gdb.execute('dc data 3', to_string=True) == ( + '+0000 0x400081 00 00 00 │... ' + '│ │ │ │\n' + ) + + ################################################# + #### ds command tests + ################################################# + ds1 = gdb.execute('ds short_str', to_string=True) + ds2 = gdb.execute('ds &short_str', to_string=True) + ds3 = gdb.execute('ds 0x4000d9', to_string=True) + ds4 = gdb.execute('ds 4000d9', to_string=True) + assert ds1 == ds2 == ds3 == ds4 == "4000d9 'some cstring here'\n" + + # Check too low maxlen + assert gdb.execute('ds short_str 5', to_string=True) == ( + 'Max str len of 5 too low, changing to 256\n' + "4000d9 'some cstring here'\n" + ) + + # Check output for a string longer than (the default) maxlen of 256 + assert gdb.execute('ds long_str', to_string=True) == ( + "4000eb 'long string: " + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...'\n" + ) + + # Check impossible address + assert gdb.execute('ds 0', to_string=True) == ( + "Data at address can't be dereferenced or is not a printable null-terminated " + 'string or is too short.\n' + 'Perhaps try: db
or hexdump
\n' + ) + + +def test_windbg_eX_commands(start_binary): + """ + Tests windbg compatibility commands that write to memory + like eq, ed, ew, eb etc. + """ + start_binary(MEMORY_BINARY) + + # Try to fail commands in different way + for cmd_prefix in ('eq', 'ed', 'ew', 'eb'): + # With a non-existent symbol + cmd = cmd_prefix + ' nonexistentsymbol' + assert gdb.execute(cmd, to_string=True) == ( + 'usage: XX [-h] address [data ...]\n' + "XX: error: argument address: invalid HexOrAddressExpr value: 'nonexistentsymbol'\n" + ).replace('XX', cmd_prefix) + + # With no data arguments provided + cmd = cmd_prefix + ' 0' + assert gdb.execute(cmd, to_string=True) == 'Cannot write empty data into memory.\n' + + # With invalid/unmapped address 0 + cmd = cmd_prefix + ' 0 1122' + assert gdb.execute(cmd, to_string=True) == ( + 'Cannot access memory at address 0x0\n' + ) + + # With invalid data which can't be parsed as hex + cmd = cmd_prefix + ' 0 x' + assert gdb.execute(cmd, to_string=True) == ( + 'Incorrect data format: it must all be a hex value (0x1234 or 1234, both ' + 'interpreted as 0x1234)\n' + ) + ######################################### + ### Test eq write + ######################################### + assert gdb.execute('eq $sp 0xcafebabe', to_string=True) == '' + assert '0x00000000cafebabe' in gdb.execute('x/xg $sp', to_string=True) + + assert gdb.execute('eq $sp 0xbabe 0xcafe', to_string=True) == '' + assert '0x000000000000babe\t0x000000000000cafe' in gdb.execute('x/2xg $sp', to_string=True) + + assert gdb.execute('eq $sp cafe000000000000 babe000000000000', to_string=True) == '' + assert '0xcafe000000000000\t0xbabe000000000000' in gdb.execute('x/2xg $sp', to_string=True) + + # TODO/FIXME: implement tests for others (ed, ew, eb etc) + + ######################################### + ### Test write & output on partial write + ######################################### + # e.g. when we make a write to the last stack address + stack_ea = pwndbg.regs[pwndbg.regs.stack] + stack_page = pwndbg.vmmap.find(stack_ea) + + # Last possible address on stack where we can perform an 8-byte write + stack_last_qword_ea = stack_page.end - 8 + + assert gdb.execute('eq %#x 0xCAFEBABEdeadbeef 0xABCD' % stack_last_qword_ea, to_string=True) == ( + 'Cannot access memory at address 0x7fffffffeff8\n' + '(Made 1 writes to memory; skipping further writes)\n' + ) + + # Check if the write actually occured + assert pwndbg.memory.read(stack_last_qword_ea, 8) == b'\xef\xbe\xad\xde\xbe\xba\xfe\xca' +