diff --git a/.github/workflows/deploy_libhc.yml b/.github/workflows/deploy_libhc.yml new file mode 100644 index 0000000..76e9764 --- /dev/null +++ b/.github/workflows/deploy_libhc.yml @@ -0,0 +1,56 @@ +name: Publish libhc debian package + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + create_release: + runs-on: ubuntu-latest + outputs: + v-version: ${{ steps.version.outputs.v-version }} + + steps: + - name: Get next version + uses: reecetech/version-increment@2024.10.1 + id: version + with: + release_branch: main + use_api: true + increment: patch + + build_and_publish: + needs: create_release + runs-on: ubuntu-latest + + steps: + - name: Install dependencies + run: | + sudo apt-get -qq update -y + sudo apt-get -qq install cmake + + - name: Check out + uses: actions/checkout@v5 + + - name: Build package + run: | + cmake -B"./build" \ + -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_BUILD_TYPE=Release \ + -DLIBHC_VERSION=${{ needs.create_release.outputs.v-version }} + cd build + cpack -G DEB + + - name: Upload debian package to release + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.create_release.outputs.v-version }} + draft: false + generate_release_notes: true + prerelease: false + files: | + build/libhc-dev*.deb diff --git a/.github/workflows/test_libhc.yml b/.github/workflows/test_libhc.yml new file mode 100644 index 0000000..02fe201 --- /dev/null +++ b/.github/workflows/test_libhc.yml @@ -0,0 +1,57 @@ +name: Test Hypercalls +on: + workflow_dispatch: + pull_request: + branches: + - main + +jobs: + test_hypercalls: + runs-on: ubuntu-latest + strategy: + matrix: + arch: + - i386 + - x86_64 + - arm + - aarch64 + + steps: + - name: Install dependencies + run: | + sudo apt-get -qq update -y + sudo apt-get -qq install cmake + + - name: Check out + uses: actions/checkout@v5 + + - name: Download for cross-compiling + run: | + # i386 compile + sudo apt-get -y install gcc-multilib + # arm compile + sudo apt-get -y install gcc-arm-linux-gnueabihf + # aarch64 compile + sudo apt-get -y install gcc-aarch64-linux-gnu + + - name: Install PANDA + run: | + ubuntu_version=$(lsb_release -rs) + curl -LJ -o /tmp/pandare_${ubuntu_version}.deb https://github.com/panda-re/panda/releases/latest/download/pandare_${ubuntu_version}.deb + sudo apt-get -y install /tmp/pandare_${ubuntu_version}.deb + pip install pandare + + # Adding the rm -rf to make sure the build directory is clean before building on each architecture + - name: Build binaries for ${{ matrix.arch }} + run: | + rm -rf build + cmake -B"./build" \ + -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_BUILD_TYPE=Release \ + -DARCH=${{ matrix.arch }} + cmake --build "./build" --parallel "$(nproc)" --config Release + + - name: Run tests + run: | + cd tests + python3 test_libhc.py --arch ${{ matrix.arch }} --ci diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8670390 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# No panda recordings +recording-rr-* +*.plog +*.json + +# No ISO +*.iso + +# Binaries +test_hc_i386 +test_hc_x86_64 +test_hc_arm +test_hc_aarch64 + +# No IDE +.idea + +# ignore build folder and any debian packages +build +*.deb + +# Got this from here, https://github.com/github/gitignore/blob/main/CMake.gitignore +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..de713fa --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.15) + +set (CMAKE_CONFIGURATION_TYPES "Release" CACHE STRING "Configs" FORCE) +set(CMAKE_VERBOSE_MAKEFILE OFF) + +# Read version number from command line argument +if(DEFINED LIBHC_VERSION) + string(REGEX MATCH "v?([0-9]+)\\.([0-9]+)\\.([0-9]+)" _ ${LIBHC_VERSION}) + set(CPACK_PACKAGE_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(CPACK_PACKAGE_VERSION_MINOR ${CMAKE_MATCH_2}) + set(CPACK_PACKAGE_VERSION_PATCH ${CMAKE_MATCH_3}) + set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_2}) + set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_3}) +else() + set(CPACK_PACKAGE_VERSION_MAJOR "0") + set(CPACK_PACKAGE_VERSION_MINOR "0") + set(CPACK_PACKAGE_VERSION_PATCH "0") + set(PROJECT_VERSION_MAJOR "0") + set(PROJECT_VERSION_MINOR "0") + set(PROJECT_VERSION_PATCH "0") +endif() + +# --- Options --- +# Architecture can be: x86_64 (default), i386, arm, aarch64 +set(ARCH "x86_64" CACHE STRING "Target architecture for compilation") + +# --- Architecture Specific Flags --- +# We use a list for flags to ensure CMake passes them as separate arguments to the compiler +set(ARCH_FLAGS "") + +if(ARCH STREQUAL "aarch64") + set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) + list(APPEND ARCH_FLAGS "-static" "-O0" "-g" "-gdwarf-2" "-fno-stack-protector") + message(STATUS "Configuring for AArch64 (Cross-compile)") +elseif(ARCH STREQUAL "arm") + set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) + list(APPEND ARCH_FLAGS "-marm" "-static" "-O0" "-g" "-gdwarf-2" "-fno-stack-protector") + message(STATUS "Configuring for ARM 32-bit (Cross-compile)") +elseif(ARCH STREQUAL "i386") + # Separate flags are required: -m32 for 32-bit, -static for PANDA portability + list(APPEND ARCH_FLAGS "-m32" "-static" "-O0" "-g" "-gdwarf-2" "-fno-stack-protector") + message(STATUS "Configuring for i386 32-bit (Requires gcc-multilib)") +else() + list(APPEND ARCH_FLAGS "-static" "-O0" "-g" "-gdwarf-2" "-fno-stack-protector") + message(STATUS "Configuring for Native x86_64") +endif() + +project(libhc VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} LANGUAGES C CXX) + +# --- Build Target --- +add_executable(test_hc tests/test_hc.c) + +# Include the headers from the local directory +target_include_directories(test_hc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# Apply static flags, you need this for PANDA +target_compile_options(test_hc PRIVATE ${ARCH_FLAGS} -Wall -Wextra) +if(ARCH_FLAGS MATCHES "-static") + target_link_options(test_hc PRIVATE ${ARCH_FLAGS}) +endif() + +# Set output name based on architecture and place the binary in the tests/ directory +set_target_properties(test_hc PROPERTIES + OUTPUT_NAME "test_hc_${ARCH}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" +) + +# --- Installation --- +include(GNUInstallDirs) + +# Install the header file, we want this /usr/include/panda/hypercall.h +install(FILES include/hypercall.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" + DESTINATION "${CMAKE_INSTALL_DOCDIR}" + RENAME "copyright") + +include(CPackConfig.txt) diff --git a/CPackConfig.txt b/CPackConfig.txt new file mode 100644 index 0000000..221e4e6 --- /dev/null +++ b/CPackConfig.txt @@ -0,0 +1,30 @@ +set(CPACK_PACKAGE_NAME "libhc-dev") + +set(CPACK_PACKAGE_VENDOR "pandare") + +set(CPACK_PACKAGE_DESCRIPTION "Cross-platform assembly hypercall wrappers for PANDA") + +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Cross-platform assembly hypercall wrappers for PANDA") + +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) + +set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "all") + +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Luke Craig ") + +set(CPACK_DEBIAN_PACKAGE_SECTION "devel") + +set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") + +set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}_${PROJECT_VERSION}_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}") + +# Homepage +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/panda-re/libhc") + +# Source ignore files +set(CPACK_SOURCE_IGNORE_FILES "^${CMAKE_BINARY_DIR};/\\\\.gitignore;/\\\\.svn;\\\\.swp$;\\\\.#;/#;.*~") + +# Licensing information +set(CPACK_DEBIAN_PACKAGE_LICENSE "MIT") + +include(CPack) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac6e89f --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Libhc +Use this header inconjuction with [PIRATE](https://github.com/panda-re/panda/blob/dev/panda/plugins/taint2/PIRATE.md) for making your hypercalls. For an example, see how [LAVA](https://github.com/panda-re/lava/blob/master/tools/include/pirate_mark_lava.h) and [PANDA](https://github.com/panda-re/panda/blob/dev/panda/plugins/pri_taint/pri_taint.cpp) use this library for an architecture neutral hypercall system to taint bytes. + +This has the advantage of once you get this working in one platform, it is much easier to make your plug-in work in other archtictures. + +Please see [this](https://github.com/panda-re/libhc/issues/5) for a better understanding how the mapping between arguments and registers is decided. + +## Creating the package locally +To create the debian package use the following steps, the package would be under the `build` folder. + +```bash +cmake -B"./build" \ + -DCMAKE_INSTALL_PREFIX="/usr" \ + -DCMAKE_BUILD_TYPE=Release +pushd build +cpack -G DEB +popd +``` diff --git a/hypercall.h b/include/hypercall.h similarity index 96% rename from hypercall.h rename to include/hypercall.h index fd4ea15..c74f425 100644 --- a/hypercall.h +++ b/include/hypercall.h @@ -1,3 +1,5 @@ +#ifndef __HYPERCALL_H__ +#define __HYPERCALL_H__ #define DECLARE_REGISTER(x,y,z) register unsigned long reg##x asm(#y) = z; #define COMMA , @@ -115,23 +117,23 @@ #else #error "not supported" #endif -static inline unsigned long igloo_hypercall(unsigned long num, unsigned long arg1){ +static inline unsigned long igloo_hypercall(unsigned long num, unsigned long arg1) { REGISTER2 ASM() RETURN } -static inline unsigned long igloo_hypercall2(unsigned long num, unsigned long arg1, unsigned long arg2){ +static inline unsigned long igloo_hypercall2(unsigned long num, unsigned long arg1, unsigned long arg2) { REGISTER3 ASM(COMMA"r"(reg2)) RETURN } -static inline unsigned long igloo_hypercall3(unsigned long num, unsigned long arg1, unsigned long arg2, unsigned long arg3){ +static inline unsigned long igloo_hypercall3(unsigned long num, unsigned long arg1, unsigned long arg2, unsigned long arg3) { REGISTER4 ASM(COMMA "r"(reg2)COMMA "r"(reg3)) RETURN } -static inline unsigned long igloo_hypercall4(unsigned long num, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4){ +static inline unsigned long igloo_hypercall4(unsigned long num, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4) { REGISTER5 ASM(COMMA "r"(reg2)COMMA "r"(reg3)COMMA "r"(reg4)) RETURN @@ -155,3 +157,4 @@ static inline int hc(int hc_type, void **s,int len) { return ret; } #endif +#endif // __HYPERCALL_H__ \ No newline at end of file diff --git a/tests/test_hc.c b/tests/test_hc.c new file mode 100644 index 0000000..7d5651d --- /dev/null +++ b/tests/test_hc.c @@ -0,0 +1,22 @@ +#include +#include "hypercall.h" + + +int main() { + int magic = 6767; + printf("[*] Attempting hypercall %d...\n", magic); + printf("[!] NOTE: This will FAIL if run outside of PANDA/QEMU!\n"); + + // In PANDA, the handler will set the return value to 6767 if successful + int result = (int) igloo_hypercall4(magic, 42, 43, 44, 45); + + printf("[+] Hypercall finished. Result: %d\n", result); + + if (result == magic) { + printf("[+] Success!\n"); + return 0; + } else { + printf("[-] Failure: Expected %d, got %d\n", magic, result); + return 1; + } +} \ No newline at end of file diff --git a/tests/test_libhc.py b/tests/test_libhc.py new file mode 100644 index 0000000..a310052 --- /dev/null +++ b/tests/test_libhc.py @@ -0,0 +1,70 @@ +from pandare import Panda +import argparse + + +def run_hypercall_test(architecture: str, ci: bool = False): + """ + Initializes the project the PANDA project and + test the hypercalls + """ + panda = Panda(generic=architecture) + class State: + command_args = [f"./test_hc_{architecture}"] + magic = 6767 + type_number = 42 + data = 43 + length = 44 + misc = 45 + state = State() + + @panda.queue_blocking + def create_recording_wrapper(): + """ + Run the recording with the test binary. + This binary run should a hypercall and confirm that the values match. + """ + # Pass in None for snap_name since I already did the revert_sync already + print(f"[PyPANDA TEST] Starting recording for hypercall test, copying over test_hc_{architecture} binary") + panda.revert_sync('root') + panda.copy_to_guest(f"test_hc_{architecture}") + print(panda.run_serial_cmd(f"./test_hc_{architecture}/test_hc_{architecture}")) + panda.stop_run() + + @panda.hypercall(state.magic) + def before_hc(cpu): + print("[PyPANDA TEST] Hypercall intercepted!") + magic = panda.arch.get_arg(cpu, 0, convention='syscall') + type_number = panda.arch.get_arg(cpu, 1, convention='syscall') + data = panda.arch.get_arg(cpu, 2, convention='syscall') + length = panda.arch.get_arg(cpu, 3, convention='syscall') + misc = panda.arch.get_arg(cpu, 4, convention='syscall') + + # Confirm the values indeed match + assert magic == state.magic, f"Expected magic {state.magic}, got {magic}" + assert type_number == state.type_number, f"Expected type_number {state.type_number}, got {type_number}" + assert data == state.data, f"Expected data {state.data}, got {data}" + assert length == state.length, f"Expected length {state.length}, got {length}" + assert misc == state.misc, f"Expected misc {state.misc}, got {misc}" + + def record(): + """ + Start the recording + """ + if ci: + panda.set_complete_rr_snapshot() + panda.run() + + record() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Test running hypercalls in PANDA") + parser.add_argument("-a", "--arch", "--architecture", type=str, choices=["i386", "x86_64", "arm", "aarch64"], + default="x86_64", + dest="architecture", + help="The architecture to test (default: x86_64)") + parser.add_argument("--ci", action="store_true", default=False, + dest="ci", + help="Run in CI mode with complete RR snapshot (default: False)") + args = parser.parse_args() + run_hypercall_test(architecture=args.architecture, ci=args.ci)