Fix nearpc following jumps when used w/o emulation (#499)

* Tests launcher: show passed and failed count

* Build nearpc, emulate, u, pdisass test binaries

* Add tests for emulate, nearpc, pdisass, u

* Refactored disasm and emulator

* Fix nearpc following jumps w/o emulation

* Prevent tests from calling start_binary twice

* Add test for emulate_disasm_loop

* Fix isort

* Add nasm to travis install

* Add --eval-command quit to tests invocation

This should prevent travis from staying in gdb/stalled build when something fails in weird way (like a file is missing)
```
[+] Building 'emulate_disasm.o'
make: nasm: Command not found
make: *** [emulate_disasm.o] Error 127
gdbinit.py: No such file or directory.
pytests_collect.py: No such file or directory.
No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself.
Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received
```

* Add test binaries
pull/500/head
Disconnect3d 8 years ago committed by GitHub
parent 87aa167599
commit f2ebe4bce0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,7 @@ cache:
- capstone
- unicorn
install:
- sudo apt-get -y install gdb
- sudo apt-get -y install gdb nasm
- lsb_release -a
- pip install -r requirements.txt
- sudo ./setup.sh

@ -186,12 +186,12 @@ emulate_command = emulate
@pwndbg.commands.ParsedCommand
@pwndbg.commands.OnlyWhenRunning
def pdisass(pc=None, lines=None):
def pdisass(pc=None, lines=None, to_string=False):
"""
Compatibility layer for PEDA's pdisass command
"""
nearpc.repeat = pdisass.repeat
return nearpc(pc, lines, False, False)
return nearpc(pc, lines, to_string, False)
nearpc.next_pc = 0

@ -298,14 +298,14 @@ def bp(where):
@pwndbg.commands.ParsedCommand
@pwndbg.commands.OnlyWhenRunning
def u(where=None, n=5):
def u(where=None, n=5, to_string=False):
"""
Starting at the specified address, disassemble
N instructions (default 5).
"""
if where is None:
where = pwndbg.regs.pc
pwndbg.commands.nearpc.nearpc(where, n)
return pwndbg.commands.nearpc.nearpc(where, n, to_string)
@pwndbg.commands.Command
@pwndbg.commands.OnlyWhenRunning

@ -144,12 +144,13 @@ DO_NOT_EMULATE = {
capstone.CS_GRP_INVALID,
capstone.CS_GRP_IRET,
# Note that we explicitly do not include the PRIVILEGE category, since
# we may be in kernel code, and privileged instructions are just fine
# in that case.
# capstone.CS_GRP_PRIVILEGE,
# Note that we explicitly do not include the PRIVILEGE category, since
# we may be in kernel code, and privileged instructions are just fine
# in that case.
#capstone.CS_GRP_PRIVILEGE,
}
def near(address, instructions=1, emulate=False, show_prev_insns=True):
"""
Disasms instructions near given `address`. Passing `emulate` makes use of
@ -199,8 +200,8 @@ def near(address, instructions=1, emulate=False, show_prev_insns=True):
#
# At this point, we've already added everything *BEFORE* the requested address,
# and the instruction at 'address'.
insn = current
total_instructions = 1+(2*instructions)
insn = current
total_instructions = 1 + (2*instructions)
while insn and len(insns) < total_instructions:
target = insn.target
@ -208,26 +209,20 @@ def near(address, instructions=1, emulate=False, show_prev_insns=True):
# Disable emulation if necessary
if emulate and set(insn.groups) & DO_NOT_EMULATE:
emulate = False
emu = None
# Continue disassembling after a RET or JUMP, but don't follow through CALL.
if capstone.CS_GRP_CALL in insn.groups:
target = insn.next
emu = None
# If we initialized the emulator and emulation is still enabled, we can use it
# to figure out the next instruction.
elif emu:
if emu:
target_candidate, size_candidate = emu.single_step()
if None not in (target_candidate, size_candidate):
target = target_candidate
size = size_candidate
# Continue disassembling at the *next* instruction unless we have emulated
# the path of execution.
elif target != pc:
target = insn.next
target = insn.address + insn.size
insn = one(target)
if insn:

@ -9,7 +9,6 @@ from __future__ import print_function
from __future__ import unicode_literals
import binascii
import inspect
import re
import capstone as C
@ -68,6 +67,7 @@ arch_to_CS = {
DEBUG = False
def debug(*a,**kw):
if DEBUG: print(*a, **kw)
@ -138,7 +138,7 @@ class Emulator(object):
# Jump tracking state
self._prev = None
self._prevsize = None
self._prev_size = None
self._curr = None
# Initialize the register state
@ -179,7 +179,6 @@ class Emulator(object):
if DEBUG:
self.hook_add(U.UC_HOOK_CODE, self.trace_hook)
def __getattr__(self, name):
reg = self.get_reg_enum(name)
@ -270,11 +269,6 @@ class Emulator(object):
Also supports general registers like 'sp' and 'pc'.
"""
if 'fsbase' in reg:
# import pdb
# pdb.set_trace()
pass
if not self.regs:
return None
@ -332,9 +326,9 @@ class Emulator(object):
def mem_read(self, *a, **kw):
debug("uc.mem_read(*%r, **%r)" % (a, kw))
return self.uc.mem_read(*a,**kw)
return self.uc.mem_read(*a, **kw)
jump_types = set([C.CS_GRP_CALL, C.CS_GRP_JUMP, C.CS_GRP_RET])
jump_types = {C.CS_GRP_CALL, C.CS_GRP_JUMP, C.CS_GRP_RET}
def until_jump(self, pc=None):
"""
@ -364,7 +358,7 @@ class Emulator(object):
# Set up the state. Resetting this each time means that we will not ever
# stop on the *current* instruction.
self._prev = None
self._prevsize = None
self._prev_size = None
self._curr = None
# Add the single-step hook, start emulating, and remove the hook.
@ -373,26 +367,26 @@ class Emulator(object):
# We're done emulating
return self._prev, self._curr
def until_jump_hook_code(self, uc, address, size, user_data):
def until_jump_hook_code(self, _uc, address, instruction_size, _user_data):
# We have not emulated any instructions yet.
if self._prev is None:
pass
# We have moved forward one linear instruction, no branch or the
# branch target was the next instruction.
elif self._prev + self._prevsize == address:
elif self._prev + self._prev_size == address:
pass
# We have branched!
# The previous instruction does not immediately precede this one.
else:
self._curr = address
debug(hex(self._prev), hex(self._prevsize), '-->', hex(self._curr))
debug(hex(self._prev), hex(self._prev_size), '-->', hex(self._curr))
self.emu_stop()
return
self._prev = address
self._prevsize = size
self._prev_size = instruction_size
def until_call(self, pc=None):
addr, target = self.until_jump(pc)
@ -425,7 +419,7 @@ class Emulator(object):
A StopIteration is raised if a fault or syscall or call instruction
is encountered.
"""
self._singlestep = (None, None)
self._single_step = (None, None)
pc = pc or self.pc
insn = pwndbg.disasm.one(pc)
@ -433,16 +427,16 @@ class Emulator(object):
# If we don't know how to disassemble, bail.
if insn is None:
debug("Can't disassemble instruction at %#x" % pc)
return self._singlestep
return self._single_step
debug("# Single-stepping at %#x: %s %s" % (pc, insn.mnemonic, insn.op_str))
try:
self.emulate_with_hook(self.single_step_hook_code, count=1)
except U.unicorn.UcError:
self._singlestep = (None, None)
except U.unicorn.UcError as e:
self._single_step = (None, None)
return self._singlestep
return self._single_step
def single_step_iter(self, pc=None):
a = self.single_step(pc)
@ -451,9 +445,9 @@ class Emulator(object):
yield a
a = self.single_step(pc)
def single_step_hook_code(self, uc, address, size, user_data):
def single_step_hook_code(self, _uc, address, instruction_size, _user_data):
debug("# single_step: %#-8x" % address)
self._singlestep = (address, size)
self._single_step = (address, instruction_size)
def dumpregs(self):
for reg in list(self.regs.misc) + list(self.regs.common) + list(self.regs.flags):
@ -467,6 +461,6 @@ class Emulator(object):
value = self.uc.reg_read(enum)
debug("uc.reg_read(%(name)s) ==> %(value)x" % locals())
def trace_hook(self, uc, address, size, user_data):
data = binascii.hexlify(self.mem_read(address, size))
def trace_hook(self, _uc, address, instruction_size, _user_data):
data = binascii.hexlify(self.mem_read(address, instruction_size))
debug("# trace_hook: %#-8x %r" % (address, data))

@ -1,10 +1,33 @@
#!/bin/bash
cd tests/binaries && make && cd ../..
# NOTE: We run tests under GDB sessions and because of some cleanup/tests dependencies problems
# we decided to run each test in a separate GDB session
TESTS_LIST=$(gdb --silent --nx --nh --command gdbinit.py --command pytests_collect.py | grep -o "tests/.*::.*")
TESTS_LIST=$(gdb --silent --nx --nh --command gdbinit.py --command pytests_collect.py --eval-command quit | grep -o "tests/.*::.*")
tests_passed_or_skipped=0
tests_failed=0
for test_case in ${TESTS_LIST}; do
PWNDBG_LAUNCH_TEST="${test_case}" PWNDBG_DISABLE_COLORS=1 gdb --silent --nx --nh --command gdbinit.py --command pytests_launcher.py
done
PWNDBG_LAUNCH_TEST="${test_case}" PWNDBG_DISABLE_COLORS=1 gdb --silent --nx --nh --command gdbinit.py --command pytests_launcher.py --eval-command quit
exit_status=$?
if [ ${exit_status} -eq 0 ]; then
(( ++tests_passed_or_skipped ))
else
(( ++tests_failed ))
fi
done
echo ""
echo "*********************************"
echo "********* TESTS SUMMARY *********"
echo "*********************************"
echo "Tests passed or skipped: ${tests_passed_or_skipped}"
echo "Tests failed: ${tests_failed}"
if [ ${tests_failed} -ne 0 ]; then
exit 1
fi

@ -9,5 +9,6 @@ from . import old_bash
path = os.path.dirname(__file__)
def get(x):
return os.path.join(path, x)

@ -0,0 +1,14 @@
global _start
; This binary is there to test
; emulate vs nearpc/u/pdisas commands
; The emulate should show just jump and one nop
; The rest should show jump and two nops
;
; Motivated by https://github.com/pwndbg/pwndbg/issues/315
_start:
jmp label
nop
label:
nop

Binary file not shown.

@ -0,0 +1,17 @@
global _start
; This binary is there to test
; emulate vs nearpc/u/pdisas commands
; The emulate should show just jump and one nop
; The rest should show jump and two nops
;
; Motivated by https://github.com/pwndbg/pwndbg/issues/315
_start:
mov rsi, string
mov rdi, rsp
mov rcx, 3
rep movsb
string db '12345', 0

@ -1,36 +1,50 @@
CC = gcc
DEBUG = 1
CFLAGS += -Wall
SOURCES = $(wildcard *.c)
COMPILED = $(SOURCES:.c=.o)
LINKED = $(SOURCES:.c=.out)
CC = gcc
DEBUG = 1
CFLAGS += -Wall
SOURCES = $(wildcard *.c)
COMPILED = $(SOURCES:.c=.o)
LINKED = $(SOURCES:.c=.out)
NASM = nasm -f elf64
LD = ld
SOURCES_ASM = $(wildcard *.asm)
COMPILED_ASM = $(SOURCES_ASM:.asm=.o)
LINKED_ASM = $(SOURCES_ASM:.asm=.out)
LDFLAGS =
EXTRA_FLAGS =
EXTRA_FLAGS =
EXTRA_FLAGS_ASM =
ifeq ($(TARGET), x86)
CFLAGS += -m32
CFLAGS += -m32
endif
ifeq ($(DEBUG), 1)
CFLAGS += -DDEBUG=1 -ggdb -O0
CFLAGS += -DDEBUG=1 -ggdb -O0
else
CFLAGS += -O1
CFLAGS += -O1
endif
.PHONY : all clean
all: $(LINKED)
all: $(LINKED) $(LINKED_ASM)
%.out : %.c
@echo "[+] Building '$@'"
@$(CC) $(CFLAGS) $(EXTRA_FLAGS) -o $@ $? $(LDFLAGS)
%.o : %.asm
@echo "[+] Building '$@'"
@$(NASM) $(EXTRA_FLAGS_ASM) -o $@ $?
%.out : %.o
@echo "[+] Linking '$@'"
@$(LD) -o $@ $?
clean :
@echo "[+] Cleaning stuff"
@rm -f $(COMPILED) $(LINKED)
reference-binary.out: EXTRA_FLAGS := -Dexample=1

@ -9,14 +9,22 @@ from __future__ import unicode_literals
import gdb
import pytest
_start_binary_called = False
@pytest.fixture
def entry_binary():
def start_binary():
"""
Returns function that launches given binary with 'entry' command
Returns function that launches given binary with 'start' command
"""
def _entry_binary(name):
gdb.execute('file ./tests/binaries/' + name)
gdb.execute('entry')
def _start_binary(path):
gdb.execute('file ' + path)
gdb.execute('start')
global _start_binary_called
if _start_binary_called:
raise Exception('Starting more than one binary is not supported in pwndbg tests.')
_start_binary_called = True
return _entry_binary
yield _start_binary

@ -0,0 +1,95 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import tests
from pwndbg.commands.nearpc import emulate
from pwndbg.commands.nearpc import nearpc
from pwndbg.commands.nearpc import pdisass
from pwndbg.commands.windbg import u
EMULATE_DISASM_BINARY = tests.binaries.get('emulate_disasm.out')
EMULATE_DISASM_LOOP_BINARY = tests.binaries.get('emulate_disasm_loop.out')
def test_emulate_disasm(start_binary):
"""
Tests emulate command and its caching behavior
"""
start_binary(EMULATE_DISASM_BINARY)
assert emulate(to_string=True) == [
' ► 0x400080 <_start> jmp label <0x400083>',
'',
' 0x400083 <label> nop ',
' 0x400084 add byte ptr [rax], al',
' 0x400086 add byte ptr [rax], al',
' 0x400088 add byte ptr [rax], al',
' 0x40008a add byte ptr [rax], al',
' 0x40008c add byte ptr [rax], al',
' 0x40008e add byte ptr [rax], al',
' 0x400090 add byte ptr [rax], al',
' 0x400092 add byte ptr [rax], al',
' 0x400094 add byte ptr [rax], al'
]
disasm_without_emu = [
' ► 0x400080 <_start> jmp label <0x400083>',
' ',
' 0x400082 <_start+2> nop ',
' 0x400083 <label> nop ',
' 0x400084 add byte ptr [rax], al',
' 0x400086 add byte ptr [rax], al',
' 0x400088 add byte ptr [rax], al',
' 0x40008a add byte ptr [rax], al',
' 0x40008c add byte ptr [rax], al',
' 0x40008e add byte ptr [rax], al',
' 0x400090 add byte ptr [rax], al',
' 0x400092 add byte ptr [rax], al'
]
assert nearpc(to_string=True) == disasm_without_emu
assert emulate(to_string=True, emulate=False) == disasm_without_emu
assert pdisass(to_string=True) == disasm_without_emu
assert u(to_string=True) == disasm_without_emu
def test_emulate_disasm_loop(start_binary):
start_binary(EMULATE_DISASM_LOOP_BINARY)
assert emulate(to_string=True) == [
' ► 0x400080 <_start> movabs rsi, string <0x400094>',
' 0x40008a <_start+10> mov rdi, rsp',
' 0x40008d <_start+13> mov ecx, 3',
' 0x400092 <_start+18> rep movsb byte ptr [rdi], byte ptr [rsi]',
'',
' 0x400092 <_start+18> rep movsb byte ptr [rdi], byte ptr [rsi]',
'',
' 0x400092 <_start+18> rep movsb byte ptr [rdi], byte ptr [rsi]',
'',
' 0x400092 <_start+18> rep movsb byte ptr [rdi], byte ptr [rsi]',
' 0x400094 <string> xor dword ptr [rdx], esi',
' 0x400096 <string+2> xor esi, dword ptr [rsi]',
' 0x40009d add byte ptr [rax], al',
' 0x40009f add byte ptr [rax], al'
]
disasm_without_emu = [
' ► 0x400080 <_start> movabs rsi, string <0x400094>',
' 0x40008a <_start+10> mov rdi, rsp',
' 0x40008d <_start+13> mov ecx, 3',
' 0x400092 <_start+18> rep movsb byte ptr [rdi], byte ptr [rsi]',
' 0x400094 <string> xor dword ptr [rdx], esi',
' 0x400096 <string+2> xor esi, dword ptr [rsi]',
' 0x40009d add byte ptr [rax], al',
' 0x40009f add byte ptr [rax], al',
' 0x4000a1 add byte ptr [rax], al',
' 0x4000a3 add byte ptr [rax], al',
' 0x4000a5 add byte ptr [rax], al'
]
assert nearpc(to_string=True) == disasm_without_emu
assert emulate(to_string=True, emulate=False) == disasm_without_emu
assert pdisass(to_string=True) == disasm_without_emu
assert u(to_string=True) == disasm_without_emu

@ -5,13 +5,16 @@ from __future__ import unicode_literals
import pwndbg.memory
import pwndbg.stack
import tests
REFERENCE_BINARY = tests.binaries.get('reference-binary.out')
def test_memory_read_write(entry_binary):
def test_memory_read_write(start_binary):
"""
Tests simple pwndbg's memory read/write operations with different argument types
"""
entry_binary('reference-binary.out')
start_binary(REFERENCE_BINARY)
stack_addr = next(iter(pwndbg.stack.stacks.values())).vaddr
# Testing write(addr, str)

Loading…
Cancel
Save