diff --git a/.github/workflows/dockcross.yml b/.github/workflows/dockcross.yml index a314e6b..9a4dd03 100644 --- a/.github/workflows/dockcross.yml +++ b/.github/workflows/dockcross.yml @@ -58,10 +58,17 @@ jobs: - uses: actions/checkout@v4 - name: Install Dependencies - run: brew install libffi readline llvm + run: | + brew install libffi readline llvm + python3 -m pip install --upgrade pip conan + + - name: Resolve Conan dependencies + run: | + conan profile detect --force + conan install . --output-folder=build --build=missing -g CMakeDeps - name: Configure CMake - run: brew sh -c "mkdir build && cd build && cmake .." + run: brew sh -c "mkdir build && cd build && cmake .. -DUSE_FIND_PACKAGE=ON" - name: Build run: brew sh -c "cd build && make -j 4" diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 0f77add..3b43954 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -18,11 +18,19 @@ jobs: - name: Install Dependencies (macOS) if: runner.os == 'macOS' - run: brew install libffi cmake + run: | + brew install libffi cmake + python3 -m pip install --upgrade pip conan + + - name: Resolve Conan dependencies (macOS) + if: runner.os == 'macOS' + run: | + conan profile detect --force + conan install . --output-folder=build --build=missing -g CMakeDeps - name: Configure CMake (macOS) if: runner.os == 'macOS' - run: cmake -B build -DCMAKE_BUILD_TYPE=Release + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DUSE_FIND_PACKAGE=ON - name: Build run: cmake --build build --config Release diff --git a/CMakeLists.txt b/CMakeLists.txt index ed26856..04d250f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(cliffi VERSION 1.0 LANGUAGES C) +project(cliffi VERSION 1.0 LANGUAGES C CXX) enable_testing() @@ -82,6 +82,7 @@ src/tokenize.c src/parse_address.c src/shims.c src/exception_handling.c +src/export_symbols.cpp ) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) @@ -89,6 +90,9 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) add_library(cliffi_common_deps INTERFACE) +# LIEF is optional for local/pkg-config builds and enabled via Conan/CMakeDeps in cross-platform CI. +set(CLIFFI_HAS_LIEF OFF) + # Target executable add_executable(cliffi ${CLIFFI_COMMON_SOURCES}) @@ -98,6 +102,7 @@ find_package(PkgConfig REQUIRED) pkg_search_module(LIBFFI libffi REQUIRED) pkg_search_module(READLINE readline REQUIRED) +pkg_search_module(LIEF_CPP QUIET LIEF lief) target_link_libraries(cliffi_common_deps INTERFACE @@ -105,6 +110,12 @@ ${LIBFFI_LIBRARIES} ${READLINE_LIBRARIES} ) +if(LIEF_CPP_FOUND) + target_include_directories(cliffi_common_deps INTERFACE ${LIEF_CPP_INCLUDE_DIRS}) + target_link_libraries(cliffi_common_deps INTERFACE ${LIEF_CPP_LIBRARIES}) + set(CLIFFI_HAS_LIEF ON) +endif() + endif() # Append the project root directory to CMAKE_PREFIX_PATH list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/build") @@ -126,6 +137,12 @@ target_link_libraries(cliffi_common_deps INTERFACE readline::readline) message(STATUS "readline include dirs: ${readline_INCLUDE_DIRS}") message("attempting link readline::readline") endif() + +set(LIEF_DIR ${CMAKE_SOURCE_DIR}/build) +set(ENV{LIEF_DIR} ${CMAKE_SOURCE_DIR}/build) +find_package(LIEF REQUIRED) +target_link_libraries(cliffi_common_deps INTERFACE LIEF::LIEF) +set(CLIFFI_HAS_LIEF ON) endif() if(NOT ANDROID) # on android this breaks things @@ -142,6 +159,7 @@ target_include_directories(cliffi_common_deps INTERFACE ) target_link_libraries(cliffi PRIVATE cliffi_common_deps) +target_compile_definitions(cliffi PRIVATE $<$:CLIFFI_HAS_LIEF>) # Compile cliffi_testlib.c into a shared library add_library(cliffitest SHARED test/lib/cliffi_testlib.c) @@ -181,6 +199,7 @@ target_link_libraries(cliffi_unit_tests PRIVATE unity # The Unity testing framework library ) target_compile_definitions(cliffi_unit_tests PRIVATE CLIFFI_UNIT_TESTING) # this prevents main() from main.c from being compiled +target_compile_definitions(cliffi_unit_tests PRIVATE $<$:CLIFFI_HAS_LIEF>) add_test(NAME cliffi_unit_tests COMMAND cliffi_unit_tests) @@ -1340,4 +1359,3 @@ add_test(NAME test_nonzero_exit_for_nonexistent_function set_tests_properties(test_nonzero_exit_for_nonexistent_function PROPERTIES WILL_FAIL TRUE) # this test is expected to fail - diff --git a/README.md b/README.md index 8fd36c0..dde44e3 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,12 @@ addr_pointer = -P intpointer In REPL mode, there are `load`, `dump`, and `store` to respectively load a value from a memory address, print a value from a memory address, or store a value to a memory address. -There are also commands to do a `hexdump` from a memory address, and `calculate_offset` to calculate a memory offset (and store the offset to a variable) for a particular library loaded into memory, relative to some reference layout, given a known symbol (ie function) name and reference address. This is helpful for calculating the actual locations of various stripped global and static variables from their addresses in the binary. +There are also commands to do a `hexdump` from a memory address, `exports ` to list exported symbols/functions from a shared library, and `calculate_offset` to calculate a memory offset (and store the offset to a variable) for a particular library loaded into memory, relative to some reference layout, given a known symbol (ie function) name and reference address. This is helpful for calculating the actual locations of various stripped global and static variables from their addresses in the binary. For example if in your decompiler, there is a static struct `{ int; short; char*; }` of interest at `0x10beef`, you can find the address of an exported function doFoo() at `0x10dead`, calculate the offset and store it in a variable, and then reference it. ``` calculate_offset myoffset mysharedlib.so doFoo 0x10dead // calculate the offset +exports mysharedlib.so // list exports/symbols (LIEF-backed) hexdump myoffset+0x10beef 16 // to just see 16 bytes at that address dump S: i h s :S myoffset+0x10beef // to dump the struct presently at that address store myoffset+0x10beef -S: 20 -h 0x6008 hello :S // to store that value into that address diff --git a/conanfile.txt b/conanfile.txt index f833b5a..21be91f 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,3 +1,4 @@ [requires] libffi/3.4.6 readline/8.2 +lief/0.16.2 diff --git a/profile_script.sh b/profile_script.sh index 83e9872..66cf109 100755 --- a/profile_script.sh +++ b/profile_script.sh @@ -82,6 +82,15 @@ sed -i "s|^compiler\s*=.*|compiler=$COMPILER|" "$PROFILE_PATH" sed -i "s|^os\s*=.*|os=$OS|" "$PROFILE_PATH" # This ensures the 'os=' line is present and set. echo "Updated arch, compiler, os in profile." +# Conan Android profiles must use libc++ (not libstdc++11) with clang/NDK. +if [ "$OS" == "Android" ]; then + if grep -q '^compiler\.libcxx\s*=' "$PROFILE_PATH"; then + sed -i 's|^compiler\.libcxx\s*=.*|compiler.libcxx=c++_shared|' "$PROFILE_PATH" + else + printf '\ncompiler.libcxx=c++_shared\n' >> "$PROFILE_PATH" + fi +fi + # --- Correctly add os.api_level AFTER the 'os=' line --- if [ "$OS" == "Android" ]; then TARGET_API_LEVEL="${ANDROID_API}" diff --git a/src/export_symbols.cpp b/src/export_symbols.cpp new file mode 100644 index 0000000..6fa4c56 --- /dev/null +++ b/src/export_symbols.cpp @@ -0,0 +1,37 @@ +#include "export_symbols.h" + +#include + +#if defined(CLIFFI_HAS_LIEF) +#include +#include +#include + +bool list_exported_symbols_with_lief(const char* library_path) { + std::unique_ptr binary = LIEF::Parser::parse(library_path); + if (!binary) { + fprintf(stderr, "Error: Failed to parse '%s' using LIEF\n", library_path); + return false; + } + + size_t exported_count = 0; + const auto exported = binary->exported_functions(); + for (const std::string& symbol_name : exported) { + printf("%s\n", symbol_name.c_str()); + exported_count++; + } + + printf("Total exported symbols: %zu\n", exported_count); + return true; +} + +#else + +bool list_exported_symbols_with_lief(const char* library_path) { + (void)library_path; + fprintf(stderr, + "Error: This build does not include LIEF support. Rebuild with a LIEF dependency to use 'exports'.\n"); + return false; +} + +#endif diff --git a/src/export_symbols.h b/src/export_symbols.h new file mode 100644 index 0000000..d6a4cc1 --- /dev/null +++ b/src/export_symbols.h @@ -0,0 +1,16 @@ +#ifndef EXPORT_SYMBOLS_H +#define EXPORT_SYMBOLS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool list_exported_symbols_with_lief(const char* library_path); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/main.c b/src/main.c index 833b2b8..cfea1b2 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include "return_formatter.h" #include "types_and_utils.h" #include "var_map.h" +#include "export_symbols.h" #include #include @@ -540,6 +541,26 @@ void parseHexdump(char* hexdumpCommand) { +void parseListExports(char* exportsCommand) { + int argc; + char** argv; + tokenize(exportsCommand, &argc, &argv); + if (argc < 1) { + raiseException(1, "Error: Invalid number of arguments for exports\n"); + return; + } + + char* resolvedPath = resolve_library_path(argv[0]); + if (resolvedPath == NULL) { + raiseException(1, "Error: Could not resolve library path for '%s'\n", argv[0]); + return; + } + + if (!list_exported_symbols_with_lief(resolvedPath)) { + raiseException(1, "Error: Failed to list exported symbols for '%s'\n", resolvedPath); + } +} + int parseREPLCommand(char* command){ command = trim_whitespace(command); if (strlen(command) > 0) { @@ -564,6 +585,7 @@ int parseREPLCommand(char* command){ " hexdump
: Print a hexdump of memory\n" "Shared Library Management:\n" " list: List all opened libraries\n" + " exports : List exported symbols/functions for a library (LIEF-backed)\n" " close : Close the specified library\n" " closeall: Close all opened libraries\n" "Shell commands:\n" @@ -575,6 +597,8 @@ int parseREPLCommand(char* command){ print_usage(">"); } else if (strcmp(command, "list") == 0) { listOpenedLibraries(); + } else if (strncmp(command, "exports ", 8) == 0) { + parseListExports(command + 8); } else if (strncmp(command, "close", 5) == 0) { char* libraryName = command + 5; while (*libraryName == ' ') {