Skip to content

Commit

Permalink
Migrate tests to RPyC (#97)
Browse files Browse the repository at this point in the history
## Description/Motivation/Screenshots

This PR is the GEF-Extras counter-part of hugsy/gef#1040 

Also since `kallsyms` was removed from GEF (in preparation for the
kernel module), it was added here with its tests

## How Has This Been Tested ?

"Tested" indicates that the PR works *and* the unit test (i.e. `make
test`) run passes without issue.

*  [x] x86-32
*  [x] x86-64
*  [ ] ARM
*  [x] AARCH64
*  [ ] MIPS
*  [ ] POWERPC
*  [ ] SPARC
*  [ ] RISC-V

## Checklist

<!-- N.B.: Your patch won't be reviewed unless fulfilling the following
base requirements: -->
<!--- Put an `x` in all the boxes that are complete, or that don't apply
-->
*  [x] My code follows the code style of this project.
*  [x] My change includes a change to the documentation, if required.
*  [x] If my change adds new code,
[adequate tests](https://hugsy.github.io/gef/testing) have been added.
*  [x] I have read and agree to the

[CONTRIBUTING](https://github.com/hugsy/gef/blob/main/.github/CONTRIBUTING.md)
document.

---------

Co-authored-by: Grazfather <[email protected]>
  • Loading branch information
hugsy and Grazfather authored Jan 10, 2024
1 parent 4b98e62 commit a669c30
Show file tree
Hide file tree
Showing 18 changed files with 524 additions and 363 deletions.
19 changes: 4 additions & 15 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: CI Test for GEF-EXTRAS

env:
BRANCH: main
NB_CPU: 1

on:
push:
Expand Down Expand Up @@ -66,30 +67,18 @@ jobs:
- name: Checkout GEF
run: |
mkdir -p ${{ env.GEF_PATH_DIR }}
wget -O ${{ env.GEF_PATH }} https://raw.githubusercontent.com/hugsy/gef/${{ env.BRANCH }}/gef.py
git clone -b ${{ env.BRANCH }} https://github.com/hugsy/gef ${{ env.GEF_PATH_DIR }}
echo "source ${{ env.GEF_PATH }}" > ~/.gdbinit
gdb -q -ex 'gef missing' -ex 'gef help' -ex 'gef config' -ex start -ex continue -ex quit /bin/pwd
- name: Build config file
- name: Setup Tests
run: |
gdb -q \
-ex "gef config pcustom.struct_path '$(pwd)/structs'" \
-ex "gef config syscall-args.path '$(pwd)/syscall-tables'" \
-ex "gef config context.libc_args True" \
-ex "gef config context.libc_args_path '$(pwd)/glibc-function-args'" \
-ex 'gef save' \
-ex quit
make -C tests/binaries -j ${{ env.NB_CPU }}
- name: Run Tests
run: |
make -C tests/binaries -j ${{ env.NB_CPU }}
python${{ env.PY_VER }} -m pytest --forked -n ${{ env.NB_CPU }} -v -k "not benchmark" tests/
- name: Run linter
run: |
python${{ env.PY_VER }} -m pylint --rcfile=$(pwd)/.pylintrc gef.py tests/*/*.py
standalone:
runs-on: ubuntu-latest
name: "Verify GEF-Extras install from gef/scripts"
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ name: Validation

on:
pull_request:

branches:
- main

jobs:
pre_commit:
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: actions/[email protected]
with:
python-version: "3.8"
- uses: pre-commit/[email protected]

docs_link_check:
Expand All @@ -20,9 +23,9 @@ jobs:
contents: read
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check links
uses: lycheeverse/lychee-action@v1.4.1
uses: lycheeverse/lychee-action@v1.9.1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
Expand Down
2 changes: 1 addition & 1 deletion scripts/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ def register_external_context_pane(pane_name: str, display_pane_function: Callab
def pane_title() -> str: ...


def register(cls: Type["GenericCommand"]) -> Type["GenericCommand"]: ...
def register(cls: Union[Type["GenericCommand"], Type["GenericFunction"]]) -> Union[Type["GenericCommand"], Type["GenericFunction"]]: ...


class GenericCommandBase:
Expand Down
1 change: 0 additions & 1 deletion scripts/assemble.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

__AUTHOR__ = "hugsy"
__VERSION__ = 0.2
__LICENSE__ = "MIT"
Expand Down
Empty file added scripts/kernel/__init__.py
Empty file.
57 changes: 57 additions & 0 deletions scripts/kernel/symbols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Collection of functions and commands to manipulate kernel symbols
"""

__AUTHOR__ = "hugsy"
__VERSION__ = 0.1
__LICENSE__ = "MIT"

import argparse
from typing import TYPE_CHECKING, Any, List

if TYPE_CHECKING:
from .. import * # this will allow linting for GEF and GDB


@register
class SolveKernelSymbolCommand(GenericCommand):
"""Solve kernel symbols from kallsyms table."""

_cmdline_ = "ksymaddr"
_syntax_ = f"{_cmdline_} SymbolToSearch"
_example_ = f"{_cmdline_} prepare_creds"

@parse_arguments({"symbol": ""}, {})
def do_invoke(self, _: List[str], **kwargs: Any) -> None:
def hex_to_int(num):
try:
return int(num, 16)
except ValueError:
return 0

args: argparse.Namespace = kwargs["arguments"]
if not args.symbol:
self.usage()
return
sym = args.symbol
with open("/proc/kallsyms", "r") as f:
syms = [line.strip().split(" ", 2) for line in f]
matches = [
(hex_to_int(addr), sym_t, " ".join(name.split()))
for addr, sym_t, name in syms
if sym in name
]
for addr, sym_t, name in matches:
if sym == name.split()[0]:
ok(f"Found matching symbol for '{name}' at {addr:#x} (type={sym_t})")
else:
warn(
f"Found partial match for '{sym}' at {addr:#x} (type={sym_t}): {name}"
)
if not matches:
err(f"No match for '{sym}'")
elif matches[0][0] == 0:
err(
"Check that you have the correct permissions to view kernel symbol addresses"
)
return
1 change: 0 additions & 1 deletion scripts/libc_function_args/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class GlibcFunctionArguments:
@staticmethod
def load_libc_args() -> bool:
"""Load the LIBC function arguments. Returns `True` on success, `False` or an Exception otherwise."""
global gef

# load libc function arguments' definitions
path = pathlib.Path(
Expand Down
88 changes: 88 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
import pathlib
import random
import subprocess
import tempfile
import time
import unittest

import rpyc

from .utils import GEF_EXTRAS_SCRIPTS_PATH, debug_target

COVERAGE_DIR = os.getenv("COVERAGE_DIR", "")
GEF_PATH = pathlib.Path(os.getenv("GEF_PATH", "../gef/gef.py")).absolute()
RPYC_GEF_PATH = GEF_PATH.parent / "scripts/remote_debug.py"
RPYC_HOST = "localhost"
RPYC_PORT = 18812
RPYC_SPAWN_TIME = 1.0


class RemoteGefUnitTestGeneric(unittest.TestCase):
"""
The base class for GEF test cases. This will create the `rpyc` environment to programmatically interact with
GDB and GEF in the test.
"""

def setUp(self) -> None:
self._coverage_file = None
if not hasattr(self, "_target"):
setattr(self, "_target", debug_target("default"))
else:
assert isinstance(self._target, pathlib.Path) # type: ignore pylint: disable=E1101
assert self._target.exists() # type: ignore pylint: disable=E1101
self._port = random.randint(1025, 65535)
self._commands = ""

if COVERAGE_DIR:
self._coverage_file = pathlib.Path(COVERAGE_DIR) / os.getenv(
"PYTEST_XDIST_WORKER", "gw0"
)
self._commands += f"""
pi import coverage
pi cov = coverage.Coverage(data_file="{self._coverage_file}", auto_data=True, branch=True)
pi cov.start()
"""

self._commands += f"""
source {GEF_PATH}
gef config gef.debug True
gef config gef.propagate_debug_exception True
gef config gef.disable_color True
gef config gef.extra_plugins_dir {GEF_EXTRAS_SCRIPTS_PATH}
source {RPYC_GEF_PATH}
pi start_rpyc_service({self._port})
"""

self._initfile = tempfile.NamedTemporaryFile(mode="w", delete=False)
self._initfile.write(self._commands)
self._initfile.flush()
self._command = [
"gdb",
"-q",
"-nx",
"-ex",
f"source {self._initfile.name}",
"--",
str(self._target.absolute()), # type: ignore pylint: disable=E1101
]
self._process = subprocess.Popen(self._command)
assert self._process.pid > 0
time.sleep(RPYC_SPAWN_TIME)
self._conn = rpyc.connect(
RPYC_HOST,
self._port,
)
self._gdb = self._conn.root.gdb
self._gef = self._conn.root.gef
return super().setUp()

def tearDown(self) -> None:
if COVERAGE_DIR:
self._gdb.execute("pi cov.stop()")
self._gdb.execute("pi cov.save()")
self._conn.close()
self._process.terminate()
return super().tearDown()
61 changes: 39 additions & 22 deletions tests/commands/capstone_disassemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,55 @@
"""

import pytest
from tests.base import RemoteGefUnitTestGeneric

from tests.utils import (ARCH, GefUnitTestGeneric, gdb_run_cmd,
gdb_start_silent_cmd, removeuntil)
from tests.utils import (
ARCH,
ERROR_INACTIVE_SESSION_MESSAGE,
removeuntil,
)


@pytest.mark.skipif(ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}")
class CapstoneDisassembleCommand(GefUnitTestGeneric):
@pytest.mark.skipif(
ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}"
)
class CapstoneDisassembleCommand(RemoteGefUnitTestGeneric):
"""`capstone-disassemble` command test module"""

def setUp(self) -> None:
try:
import capstone # pylint: disable=W0611
except ImportError:
pytest.skip("capstone-engine not available",
allow_module_level=True)
pytest.skip("capstone-engine not available", allow_module_level=True)
return super().setUp()

def test_cmd_capstone_disassemble(self):
self.assertFailIfInactiveSession(gdb_run_cmd("capstone-disassemble"))
res = gdb_start_silent_cmd("capstone-disassemble")
self.assertNoException(res)
self.assertTrue(len(res.splitlines()) > 1)

self.assertFailIfInactiveSession(
gdb_run_cmd("capstone-disassemble --show-opcodes"))
res = gdb_start_silent_cmd(
"capstone-disassemble --show-opcodes --length 5 $pc")
self.assertNoException(res)
self.assertTrue(len(res.splitlines()) >= 5)
gdb = self._gdb
cmd = "capstone-disassemble"

self.assertEqual(
ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True)
)

gdb.execute("start")
res = gdb.execute("capstone-disassemble", to_string=True) or ""
assert res

cmd = "capstone-disassemble --show-opcodes"
res = gdb.execute(cmd, to_string=True) or ""
assert res

cmd = "capstone-disassemble --show-opcodes --length 5 $pc"
res = gdb.execute(cmd, to_string=True) or ""
assert res

lines = res.splitlines()
self.assertGreaterEqual(len(lines), 5)

# jump to the output buffer
res = removeuntil("→ ", res, included=True)
addr, opcode, symbol, *_ = [x.strip()
for x in res.splitlines()[2].strip().split()]
addr, opcode, symbol, *_ = [x.strip() for x in lines[2].strip().split()]

# match the correct output format: <addr> <opcode> [<symbol>] mnemonic [operands,]
# gef➤ cs --show-opcodes --length 5 $pc
# → 0xaaaaaaaaa840 80000090 <main+20> adrp x0, #0xaaaaaaaba000
Expand All @@ -49,6 +65,7 @@ def test_cmd_capstone_disassemble(self):
self.assertTrue(int(opcode, 16))
self.assertTrue(symbol.startswith("<") and symbol.endswith(">"))

res = gdb_start_silent_cmd("cs --show-opcodes main")
self.assertNoException(res)
self.assertTrue(len(res.splitlines()) > 1)
cmd = "cs --show-opcodes main"
res = gdb.execute(cmd, to_string=True) or ""
assert res
self.assertGreater(len(res.splitlines()), 1)
17 changes: 17 additions & 0 deletions tests/commands/kernel/ksymaddr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
`ksymaddr` command test module
"""


from tests.base import RemoteGefUnitTestGeneric


class KsymaddrCommand(RemoteGefUnitTestGeneric):
"""`ksymaddr` command test module"""

cmd = "ksymaddr"

def test_cmd_ksymaddr(self):
gdb = self._gdb
res = gdb.execute(f"{self.cmd} prepare_kernel_cred", to_string=True)
self.assertIn("Found matching symbol for 'prepare_kernel_cred'", res)
26 changes: 16 additions & 10 deletions tests/commands/keystone_assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,29 @@

import pytest

from tests.utils import (ARCH, GefUnitTestGeneric, gdb_run_silent_cmd,
gdb_start_silent_cmd)
from tests.base import RemoteGefUnitTestGeneric

from tests.utils import (
ARCH,
)

@pytest.mark.skipif(ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}")
class KeystoneAssembleCommand(GefUnitTestGeneric):

@pytest.mark.skipif(
ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}"
)
class KeystoneAssembleCommand(RemoteGefUnitTestGeneric):
"""`keystone-assemble` command test module"""

def setUp(self) -> None:
try:
import keystone # pylint: disable=W0611
except ImportError:
pytest.skip("keystone-engine not available",
allow_module_level=True)
pytest.skip("keystone-engine not available", allow_module_level=True)
return super().setUp()

def test_cmd_keystone_assemble(self):
self.assertNotIn("keystone", gdb_run_silent_cmd("gef missing"))
gdb = self._gdb
self.assertNotIn("keystone", gdb.execute("gef missing", to_string=True))
cmds = [
"assemble --arch arm --mode arm add r0, r1, r2",
"assemble --arch arm --mode arm --endian big add r0, r1, r2",
Expand All @@ -43,6 +48,7 @@ def test_cmd_keystone_assemble(self):
"assemble --arch x86 --mode 64 mov rax, 0x42",
]
for cmd in cmds:
res = gdb_start_silent_cmd(cmd)
self.assertNoException(res)
self.assertGreater(len(res.splitlines()), 1)
res = gdb.execute(cmd, to_string=True) or ""
assert res
lines = res.splitlines()
self.assertGreater(len(lines), 1)
Loading

0 comments on commit a669c30

Please sign in to comment.