Cross architecture sanity check tests (#2745)

* Add a test that steps through each instruction in a program in different arches to detect crashes in annotations code

* lint

* Add symlinks so qemu can find libraries, simplify selecting correct qemu version

* lint

* compile cross-arch binaries with -static

* Add map of cross-arch library paths to avoid creating symlink

* lint

* clean up cross-arch makefile, skip the slow tests unless an environment variable is set

* correct compiler versions

* remove empty lines

* Makefile comment

* Add syscall to basic.c

* Increase performance

* Always run the tests

* lint
pull/2915/head
OBarronCS 8 months ago committed by GitHub
parent 491800e5a3
commit 232f5a1b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,6 +7,7 @@ from __future__ import annotations
import os
import subprocess
import sys
from typing import Literal
import gdb
import pytest
@ -37,13 +38,13 @@ def qemu_assembly_run():
def _start_binary(asm: str, arch: str, *args):
nonlocal qemu
# This is defaulting to 32-bit in this case, breaks the compilation
if arch == "riscv64":
context.bits = 64
# Clear the context so setting the .arch will also set .bits
# https://github.com/Gallopsled/pwntools/issues/2498
context.clear()
context.arch = arch
binary_tmp_path = make_elf_from_assembly(asm)
qemu_suffix = pwnlib.qemu.archname(arch=arch)
qemu_suffix = pwnlib.qemu.archname()
qemu = subprocess.Popen(
[
@ -72,6 +73,20 @@ def qemu_assembly_run():
qemu.kill()
# Map of qemu_suffix to location of library files in default Ubuntu installs of cross-compilers
CROSS_ARCH_LIBC = {
"aarch64": "/usr/aarch64-linux-gnu",
"arm": "/usr/arm-linux-gnueabihf",
"mips": "/usr/mips-linux-gnu",
"mips64": "/usr/mips64-linux-gnuabi64/",
"riscv64": "/usr/riscv64-linux-gnu/",
"loongarch64": "/usr/loongarch64-linux-gnu/",
"ppc": "/usr/powerpc-linux-gnu/",
"ppc64": "/usr/powerpc64-linux-gnu/",
"sparc64": "/usr/sparc64-linux-gnu/",
}
@pytest.fixture
def qemu_start_binary():
"""
@ -87,14 +102,21 @@ def qemu_start_binary():
sys.stdout.flush()
os._exit(1)
def _start_binary(path: str, arch: str, *args):
def _start_binary(path: str, arch: str, endian: Literal["big", "little"] | None = None):
nonlocal qemu
if endian is not None:
context.endian = endian
qemu_suffix = pwnlib.qemu.archname(arch=arch)
# qemu_libs = pwnlib.qemu.ld_prefix(arch=arch)
qemu_libs = CROSS_ARCH_LIBC.get(qemu_suffix, f"/usr/gnemul/qemu-{qemu_suffix}")
qemu = subprocess.Popen(
[
f"qemu-{arch}",
f"qemu-{qemu_suffix}",
"-L",
f"/usr/{arch}-linux-gnu/",
qemu_libs,
"-g",
f"{QEMU_PORT}",
f"{path}",

@ -1,19 +1,60 @@
.PHONY: all
all: reference-binary.aarch64.out reference-binary.riscv64.out
%.aarch64.out : %.aarch64.c
@echo "[+] Building '$@'"
@aarch64-linux-gnu-gcc $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $< $(LDFLAGS)
ARCHS = aarch64 arm riscv64 mips64 mips32
CC.aarch64 = aarch64-linux-gnu-gcc
CC.arm = arm-linux-gnueabihf-gcc
CC.riscv64 = riscv64-linux-gnu-gcc
# Ubuntu riscv64 cross compiler package doesn't have "--enable-multilib" enabled to allow compiling to 32-bit
# CC.riscv32 = riscv64-linux-gnu-gcc
CC.mips64 = mips64-linux-gnuabi64-gcc
CC.mips32 = mips-linux-gnu-gcc
# CC.loongarch64 = loongarch64-linux-gnu-gcc
ALL_FLAGS = -g
CFLAGS.aarch64 = $(ALL_FLAGS)
CFLAGS.arm = $(ALL_FLAGS)
CFLAGS.riscv64 = -march=rv64gc -mabi=lp64d $(ALL_FLAGS)
# CFLAGS.riscv32 = -march=rv32gc $(ALL_FLAGS)
CFLAGS.mips64 = $(ALL_FLAGS)
CFLAGS.mips32 = $(ALL_FLAGS)
# CFLAGS.loongarch64 = $(ALL_FLAGS)
%.riscv64.out : %.riscv64.c
@echo "[+] Building '$@'"
@riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -g $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $? $(LDFLAGS)
AARCH64_SOURCES := $(wildcard *.aarch64.c)
AARCH64_TARGETS := $(AARCH64_SOURCES:.aarch64.c=.aarch64.out)
ARM_SOURCES := $(wildcard *.arm.c)
ARM_TARGETS := $(ARM_SOURCES:.arm.c=.arm.out)
RISCV64_SOURCES := $(wildcard *.riscv64.c)
RISCV64_TARGETS := $(RISCV64_SOURCES:.riscv64.c=.riscv64.out)
MIPS64_SOURCES := $(wildcard *.mips64.c)
MIPS64_TARGETS := $(MIPS64_SOURCES:.mips64.c=.mips64.out)
MIPS32_SOURCES := $(wildcard *.mips32.c)
MIPS32_TARGETS := $(MIPS32_SOURCES:.mips32.c=.mips32.out)
# Build basic.c for all architectures
BASIC_C_TARGETS = $(ARCHS:%=basic.%.out)
basic.%.out: basic.c
@echo "[+] Building '$@'"
$(CC.$*) $(CFLAGS.$*) -o $@ $<
%.aarch64.out : %.aarch64.c
@echo "[+] Building '$@'"
$(CC.aarch64) $(CFLAGS.aarch64) -o $@ $<
%.riscv64.out : %.riscv64.c
@echo "[+] Building '$@'"
$(CC.riscv64) $(CFLAGS.riscv64) -o $@ $?
all: $(BASIC_C_TARGETS) $(AARCH64_TARGETS) $(ARM_TARGETS) $(RISCV64_TARGETS) $(MIPS64_TARGETS) $(MIPS32_TARGETS)
clean:
rm -f *.aarch64.out *.x86_64.out *.arm.out
rm -f $(BASIC_C_TARGETS) $(AARCH64_TARGETS) $(ARM_TARGETS) $(RISCV64_TARGETS) $(MIPS64_TARGETS) $(MIPS32_TARGETS)

@ -0,0 +1,12 @@
#include <unistd.h>
#include <sys/syscall.h>
char* str = "hello";
int main(int argc, char const* argv[])
{
// Starting from main, this program has about ~1500 instructions until exit in most arches
syscall(SYS_write,1,str,5);
return 0;
}

@ -1,5 +1,6 @@
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#define PORT 80

@ -0,0 +1,58 @@
from __future__ import annotations
from typing import Literal
import gdb
import user
import pwndbg.aglib.proc
import pwndbg.commands.context
# The tests in this file execute for a long time - they can take 5-15 minutes to run, depending on the machine
# They check for any crashes in the instruction enhancement code that may arise through
# when displaying the context.
# These are worth running after large changes in the instruction enhancement code and updates to Unicorn/Capstone.
NUMBER_OF_STEPS = 1500
# Step through a binary, running "ctx" each time the program stops
# This is meant to detect crashes originating from the annotations/emulation code
def helper(
qemu_start_binary, filename: str, qemu_arch: str, endian: Literal["big", "little"] | None = None
):
FILE = user.binaries.get(filename)
qemu_start_binary(FILE, qemu_arch, endian)
gdb.execute("b main")
gdb.execute("c")
pwndbg.commands.context.context_disasm()
for i in range(NUMBER_OF_STEPS):
if not pwndbg.aglib.proc.alive:
break
gdb.execute("stepi")
pwndbg.commands.context.context_disasm()
def test_basic_aarch64(qemu_start_binary):
helper(qemu_start_binary, "basic.aarch64.out", "aarch64")
def test_basic_arm(qemu_start_binary):
helper(qemu_start_binary, "basic.arm.out", "arm")
def test_basic_riscv64(qemu_start_binary):
helper(qemu_start_binary, "basic.riscv64.out", "riscv64")
def test_basic_mips64(qemu_start_binary):
# pwnlib.context.endian defaults to "little", but these MIPS binaries are compiled to big endian.
helper(qemu_start_binary, "basic.mips64.out", "mips64", endian="big")
def test_basic_mips32(qemu_start_binary):
helper(qemu_start_binary, "basic.mips32.out", "mips", endian="big")
Loading…
Cancel
Save