From 984b49d6839da773375d5d47ade0caa60df4d827 Mon Sep 17 00:00:00 2001 From: Adam Macdonald <72780006+twokilohertz@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:28:43 +0000 Subject: [PATCH] Public release 0.1 --- .clang-format | 91 ++++++ .clang-tidy | 2 + .gitignore | 29 ++ CMakeLists.txt | 28 ++ README.md | 16 + donIV/CMakeLists.txt | 24 ++ donIV/include/inject.hpp | 7 + donIV/include/process.hpp | 61 ++++ donIV/include/ptrace.hpp | 105 +++++++ donIV/include/syscalls.hpp | 11 + donIV/shellcode/CMakeLists.txt | 39 +++ donIV/shellcode/bin2cppheader.py | 71 +++++ donIV/shellcode/dlopen_shellcode.s | 32 ++ donIV/src/inject.cpp | 461 +++++++++++++++++++++++++++++ donIV/src/process.cpp | 188 ++++++++++++ donIV/src/ptrace.cpp | 182 ++++++++++++ include/cli.hpp | 28 ++ src/cli.cpp | 132 +++++++++ src/main.cpp | 31 ++ 19 files changed, 1538 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 donIV/CMakeLists.txt create mode 100644 donIV/include/inject.hpp create mode 100644 donIV/include/process.hpp create mode 100644 donIV/include/ptrace.hpp create mode 100644 donIV/include/syscalls.hpp create mode 100644 donIV/shellcode/CMakeLists.txt create mode 100644 donIV/shellcode/bin2cppheader.py create mode 100644 donIV/shellcode/dlopen_shellcode.s create mode 100644 donIV/src/inject.cpp create mode 100644 donIV/src/process.cpp create mode 100644 donIV/src/ptrace.cpp create mode 100644 include/cli.hpp create mode 100644 src/cli.cpp create mode 100644 src/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b7f788c --- /dev/null +++ b/.clang-format @@ -0,0 +1,91 @@ +--- +Language: Cpp +BasedOnStyle: Microsoft +# Includes +SortIncludes: CaseInsensitive +IncludeCategories: + # Windows.h comes first to avoid some wonky redefinitions + - Regex: "Windows.h" + Priority: 10000 + CaseSensitive: false +# Alignment +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Right +AlignConsecutiveAssignments: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true + AlignCompound: true + AlignFunctionPointers: true + PadOperators: true +AlignConsecutiveDeclarations: true +AlignConsecutiveMacros: true +AlignEscapedNewlines: Left +AlignOperands: AlignAfterOperator +AlignTrailingComments: Always +# Single-line blocks +AllowShortBlocksOnASingleLine: Empty +#AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +# ??? +BinPackArguments: false +BinPackParameters: false +# ??? +BitFieldColonSpacing: Both +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: true + AfterStruct: false + AfterUnion: false + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +# idk +BreakAfterAttributes: Always +BreakBeforeConceptDeclarations: Allowed +BreakBeforeInlineASMColon: OnlyMultiline +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +PackConstructorInitializers: NextLineOnly # Could change to CurrentLine or NextLine +# idk +ColumnLimit: 120 +IndentWidth: 4 +TabWidth: 4 +InsertNewlineAtEOF: true +# KeepEmptyLines: +# AtEndOfFile: false +# AtStartOfBlock: false +# AtStartOfFile: false +LineEnding: DeriveLF +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +PointerAlignment: Left +QualifierAlignment: Left +# ReflowComments: true +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..f5f059c --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,2 @@ +Checks: '-*,cppcoreguidelines-*,performance-*,bugprone-*,clang-diagnostic-*,clang-analyzer-*' +WarningsAsErrors: '' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef52374 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Visual Studio Code +.vscode/ + +# IntelliJ IDEs +.idea/ + +# CMake +build/ +target/ +out/ + +# clangd +.cache/ + +# Build artefacts +*.exe +*.dll +*.lib +*.pdb +*.dylib +*.so +*.o +*.a + +# Binaries +# *.bin + +# Miscellaneous stuff +.directory diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9ad3b05 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.20) + +project( + doniv-cli + VERSION 0.1 + DESCRIPTION "donIV injector" + LANGUAGES CXX +) + +# Depends on donIV library +add_subdirectory("donIV/") + +add_executable( + doniv-cli + src/main.cpp + src/cli.cpp +) +target_compile_features(doniv-cli PRIVATE cxx_std_23) +target_link_libraries(doniv-cli doniv ctre) +target_include_directories(doniv-cli PRIVATE "include/" ctre) + +# Compile-Time Regular Expressions (CTRE) +FetchContent_Declare( + ctre + GIT_REPOSITORY https://github.com/hanickadot/compile-time-regular-expressions.git + GIT_TAG v3.9.0 +) +FetchContent_MakeAvailable(ctre) diff --git a/README.md b/README.md new file mode 100644 index 0000000..9363f7e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# donIV-cli + +A shared library injector for Linux. + +## Building + +1. Change directory to the root directory of this project +2. `mkdir build` +3. `cmake -S . -B build/` +4. `cd build` +5. `make` + +## References + +- Man pages: ptrace(2), wait(2), syscalls(2), syscall(2) +- https://x64.syscall.sh/ diff --git a/donIV/CMakeLists.txt b/donIV/CMakeLists.txt new file mode 100644 index 0000000..b20e7f9 --- /dev/null +++ b/donIV/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library( + doniv + STATIC + src/process.cpp + src/ptrace.cpp + src/inject.cpp +) + +target_compile_features(doniv PRIVATE cxx_std_23) +target_include_directories(doniv PUBLIC "include/") +add_subdirectory("shellcode/") +add_dependencies(doniv shellcode) + +include(FetchContent) +target_link_libraries(doniv PRIVATE ctre) +target_include_directories(doniv PRIVATE ctre ${SHELLCODE_INCLUDE_DIR}) + +# Compile-Time Regular Expressions (CTRE) +FetchContent_Declare( + ctre + GIT_REPOSITORY https://github.com/hanickadot/compile-time-regular-expressions.git + GIT_TAG v3.9.0 +) +FetchContent_MakeAvailable(ctre) diff --git a/donIV/include/inject.hpp b/donIV/include/inject.hpp new file mode 100644 index 0000000..0539add --- /dev/null +++ b/donIV/include/inject.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "process.hpp" + +#include + +bool inject_library(const std::filesystem::path& library_path, const process& process); diff --git a/donIV/include/process.hpp b/donIV/include/process.hpp new file mode 100644 index 0000000..74a5e30 --- /dev/null +++ b/donIV/include/process.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class memory_region { + public: + std::uintptr_t start_addr = 0x0; + std::uintptr_t end_addr = 0x0; + std::uintptr_t offset = 0x0; + std::string path_name; + std::uint8_t dev_major = 0; + std::uint8_t dev_minor = 0; + std::uint64_t inode = 0; + bool read = false; + bool write = false; + bool execute = false; + bool priv = false; + bool shared = false; +}; + +class process { + public: + using pid_t = std::uint32_t; + using lib_map_t = std::unordered_map>; + static constexpr pid_t INVALID_PID = 0; + + process(pid_t pid) + : m_process_id(pid) {}; + + public: + inline pid_t pid() const noexcept { + return m_process_id; + } + + std::expected read_memory(void* target_addr, + void* local_addr, + std::size_t len) const noexcept; + std::expected write_memory(void* target_addr, + void* local_addr, + std::size_t len) const noexcept; + std::expected, std::error_code> find_bytes_in_memory( + void* start_addr, std::size_t len, std::span bytes) const noexcept; + std::expected send_signal(int signum) const noexcept; + + std::optional get_exe_path() const; + std::vector get_memory_regions() const; + std::vector get_library_memory_regions(std::string_view lib_name) const; + lib_map_t get_loaded_libraries() const; + + private: + pid_t m_process_id = INVALID_PID; +}; diff --git a/donIV/include/ptrace.hpp b/donIV/include/ptrace.hpp new file mode 100644 index 0000000..efe8521 --- /dev/null +++ b/donIV/include/ptrace.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +extern "C" +{ +#include +#include +#include + +#include // Must be included after +} + +namespace ptrace_cpp +{ +using void_expected_t = std::expected; +using int_expected_t = std::expected; +using syscall_info_t = ptrace_syscall_info; +using syscall_info_expected_t = std::expected; +using user_regs_t = user_regs_struct; +using regs_expected_t = std::expected; + +struct sig_stop_t { + int signal = 0; +}; + +struct exited_t { + int exit_code = 0; +}; + +struct term_by_t { + int term_sig = 0; +}; + +struct continued_t {}; + +using syscall_stop_t = syscall_info_t; // ptrace_syscall_info +using wait_status_type_t = std::variant; + +constexpr int TRACESYSGOOD_TRAP_MASK = (SIGTRAP | 0x80); + +void_expected_t attach_to_process(std::uint32_t pid); +void_expected_t detach_from_process(std::uint32_t pid); +void_expected_t set_options(std::uint32_t pid, std::bitset<32> options); + +regs_expected_t get_regs(std::uint32_t pid); +syscall_info_expected_t get_syscall_info(std::uint32_t pid); +void_expected_t set_regs(std::uint32_t pid, const user_regs_t& regs); + +wait_status_type_t wait_on_process(std::uint32_t pid); +void_expected_t continue_execution(std::uint32_t pid, int sig_num = 0); +void_expected_t continue_to_next_syscall(std::uint32_t pid, int sig_num = 0); +void_expected_t singlestep(std::uint32_t pid, int sig_num = 0); +} // namespace ptrace_cpp + +namespace ptrace_cpp::status +{ + +inline int status_to_stop_sig(int status) noexcept { + return WSTOPSIG(status); +} + +inline bool is_stopped(int status) noexcept { + return WIFSTOPPED(status); +} + +inline bool is_exited(int status) noexcept { + return WIFEXITED(status); +} + +inline bool is_signalled(int status) noexcept { + return WIFSIGNALED(status); +} + +inline bool is_continued(int status) noexcept { + return WIFCONTINUED(status); +} + +} // namespace ptrace_cpp::status + +namespace ptrace_cpp::signal +{ + +inline bool is_syscall(int sig_num) noexcept { + return sig_num == TRACESYSGOOD_TRAP_MASK; +} + +inline int unmask_sysgood(int sig_num) noexcept { + return sig_num & 0x7F; +} + +} // namespace ptrace_cpp::signal + +namespace ptrace_cpp::debug +{ + +void print_user_regs(const user_regs_t& regs); +void print_wait_status(const wait_status_type_t& wait); + +} // namespace ptrace_cpp::debug diff --git a/donIV/include/syscalls.hpp b/donIV/include/syscalls.hpp new file mode 100644 index 0000000..3aea05f --- /dev/null +++ b/donIV/include/syscalls.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace syscalls +{ +namespace x64 +{ + constexpr const unsigned int mmap = 9; + constexpr const unsigned int munmap = 11; + constexpr const unsigned int kill = 62; +} // namespace x64 +} // namespace syscalls diff --git a/donIV/shellcode/CMakeLists.txt b/donIV/shellcode/CMakeLists.txt new file mode 100644 index 0000000..b138d38 --- /dev/null +++ b/donIV/shellcode/CMakeLists.txt @@ -0,0 +1,39 @@ +set( + SHELLCODE_INCLUDE_DIR + "${CMAKE_CURRENT_BINARY_DIR}/shellcode/" + PARENT_SCOPE +) + +set( + SHELLCODE_OUT_DIR + ${CMAKE_CURRENT_BINARY_DIR}/shellcode +) + +add_custom_command( + OUTPUT ${SHELLCODE_OUT_DIR}/dlopen_shellcode.o + COMMAND as --64 -o ${SHELLCODE_OUT_DIR}/dlopen_shellcode.o ./dlopen_shellcode.s + DEPENDS ./dlopen_shellcode.s + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Assemble dlopen() shellcode (x86_64)" +) + +add_custom_command( + OUTPUT ${SHELLCODE_OUT_DIR}/dlopen_shellcode.bin + COMMAND objcopy -j .text -O binary ${SHELLCODE_OUT_DIR}/dlopen_shellcode.o ${SHELLCODE_OUT_DIR}/dlopen_shellcode.bin + DEPENDS ${SHELLCODE_OUT_DIR}/dlopen_shellcode.o + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Copy dlopen() shellcode to binary file (x86_64)" +) + +add_custom_command( + OUTPUT ${SHELLCODE_OUT_DIR}/dlopen_shellcode.hpp + COMMAND python bin2cppheader.py ${SHELLCODE_OUT_DIR}/dlopen_shellcode.bin + DEPENDS ${SHELLCODE_OUT_DIR}/dlopen_shellcode.bin + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Convert dlopen() shellcode binary to C++ header (x86_64)" +) + +add_custom_target( + shellcode + DEPENDS ${SHELLCODE_OUT_DIR}/dlopen_shellcode.hpp +) diff --git a/donIV/shellcode/bin2cppheader.py b/donIV/shellcode/bin2cppheader.py new file mode 100644 index 0000000..f99d241 --- /dev/null +++ b/donIV/shellcode/bin2cppheader.py @@ -0,0 +1,71 @@ +# bin2cppheader.py +# Convert a binary file to its static std::array representation as an +# includable header file in your C++ project. +# Author: Adam Macdonald [https://github.com/twokilohertz] + +usage = "Usage: python bin2cppheader.py in_file [out_file]" + +import sys +import mmap +from pathlib import Path + +# Helper function for printing to stderr +def eprint(*args, **kwargs): + return print(*args, file=sys.stderr, **kwargs) + +def read_bin(bin_path): + ret_buf = [] + + with open(bin_path, "rb") as f: + with mmap.mmap(f.fileno(), length=0, prot=mmap.PROT_READ) as mm: + ret_buf = mm.read() + return ret_buf + + return ret_buf + +def bin_to_header(data, symbol_name, namespace = "bin"): + ret_str = "#pragma once\n#include \n#include \nnamespace " + namespace + " { constexpr const std::array " + symbol_name + " {" + + for b in data: + ret_str += "{:#04x}".format(b) + ", " + + ret_str = ret_str[:-2] + ret_str += "};}" + + return ret_str + +if __name__ == "__main__": + if len(sys.argv) < 2: + eprint(usage) + exit(1) + + # Some validation on the input path + in_path = Path(sys.argv[1]) + if not in_path.exists(): + eprint("input path does not exist") + exit(1) + if not in_path.is_file(): + eprint("input path is not a file") + exit(1) + + # Read data & convert to a string of valid C++ + data = read_bin(in_path) + header_str = bin_to_header(data, in_path.stem) + + out_path = Path() + + if len(sys.argv) == 3: + out_path = Path(sys.argv[2]) + if not out_path.exists(): + eprint("output path does not exist") + exit(1) + if not out_path.is_dir(): + eprint("output path is not a directory") + exit(1) + else: + out_path = in_path.parents[0] / f"{in_path.stem}.hpp" + + with open(out_path, "w") as f: + f.write(header_str) + + exit(0) diff --git a/donIV/shellcode/dlopen_shellcode.s b/donIV/shellcode/dlopen_shellcode.s new file mode 100644 index 0000000..331feaf --- /dev/null +++ b/donIV/shellcode/dlopen_shellcode.s @@ -0,0 +1,32 @@ +# dlopen() shellcode +# - Calls dlopen() on our library filepath +# - Signature: void shellcode(void* dlopen_fn_ptr, char* lib_path_sz); +# dlopen_fn_ptr - RDI +# lib_path_sz - RSI + +.org 0x0 +.global _start + +.section .text + _start: + movq %rdi, %r11 # Temporarily remove dlopen_fn_ptr + + # Set up dlopen() call + movq %rsi, %rdi # Move path string ready for dlopen() + movq $2, %rsi # RTLD_NOW + call *%r11 + + # Check if dlopen() returned NULL + cmpq $0, %rax + jz set_sigusr2 + + # Prepare kill() syscall + set_sigusr1: + movq $10, %rsi # SIGUSR1 = 10 + jmp signal_injector + set_sigusr2: + movq $12, %rsi # SIGUSR2 = 12 + signal_injector: + movq $62, %rax # kill() syscall + movq $0, %rdi # PID, dummy value - the kill() syscall + syscall # is replaced with munmap() anyway diff --git a/donIV/src/inject.cpp b/donIV/src/inject.cpp new file mode 100644 index 0000000..ecb7004 --- /dev/null +++ b/donIV/src/inject.cpp @@ -0,0 +1,461 @@ +#include "inject.hpp" +#include "process.hpp" +#include "ptrace.hpp" +#include "syscalls.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" +{ +#include +#include +#include +#include +#include +#include +#include + +#include // Must be included after +} + +#include "dlopen_shellcode.hpp" + +using namespace std::string_view_literals; + +static constexpr const std::string_view LIBC_SO_NAME = "libc.so.6"sv; +static constexpr const char DLOPEN_SYMBOL_NAME[7] = "dlopen"; +static constexpr const std::array SYSCALL_ENCODING {0x0f, 0x05}; + +std::optional resolve_remote_dlopen(const process& process, std::uintptr_t libc_base_addr) { + void* ret = nullptr; + + std::array ident_buf {}; + auto maybe_read = process.read_memory(reinterpret_cast(libc_base_addr), ident_buf.data(), ident_buf.size()); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read e_ident from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (std::memcmp(ident_buf.data(), ELFMAG, SELFMAG) != 0) { + std::println(stderr, "[inject] e_ident did not match"); + std::for_each(ident_buf.cend(), ident_buf.cend(), [](const char c) { + std::print("> {:#04x}", c); + }); + return std::nullopt; + } + + if (ident_buf[4] != ELFCLASS64) { + std::println(stderr, "[inject] Only supporting ELFCLASS64 for now. Bailing."); + return std::nullopt; + } + + Elf64_Ehdr ehdr {}; + maybe_read = process.read_memory(reinterpret_cast(libc_base_addr), &ehdr, sizeof(ehdr)); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read ELF header from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (!ehdr.e_phoff) { + std::println(stderr, "[inject] Program header offset is null"); + return std::nullopt; + } + + for (std::uint32_t i = 0; i < ehdr.e_phnum; ++i) { + Elf64_Phdr phdr {}; + maybe_read = process.read_memory( + reinterpret_cast((libc_base_addr + ehdr.e_phoff) + (i * ehdr.e_phentsize)), &phdr, sizeof(phdr)); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read program header from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (phdr.p_type != PT_DYNAMIC) + continue; + + std::uint64_t str_tab_addr = 0; + std::uint64_t str_tab_size = 0; + std::uint64_t sym_tab_addr = 0; + std::uint64_t sym_tab_ent_size = 0; + const std::uint64_t dyn_start_addr = libc_base_addr + phdr.p_vaddr; + const std::uint64_t dyn_end_addr = dyn_start_addr + phdr.p_memsz; + + for (std::uint64_t j = dyn_start_addr; j < dyn_end_addr; j += sizeof(Elf64_Dyn)) { + Elf64_Dyn dyn {}; + maybe_read = process.read_memory(reinterpret_cast(j), &dyn, sizeof(dyn)); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read dynamic linking information from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (dyn.d_tag == DT_STRTAB) + str_tab_addr = dyn.d_un.d_ptr; + else if (dyn.d_tag == DT_STRSZ) + str_tab_size = dyn.d_un.d_val; + else if (dyn.d_tag == DT_SYMTAB) + sym_tab_addr = dyn.d_un.d_ptr; + else if (dyn.d_tag == DT_SYMENT) + sym_tab_ent_size = dyn.d_un.d_val; + } + + if (!str_tab_addr || !str_tab_size || !sym_tab_addr || !sym_tab_ent_size) { + std::println(stderr, "[inject] Failed to find runtime symbol & string table in libc"); + return std::nullopt; + } + + // Relevant to the cur_addr < str_tab_addr comparison in the loop below + if (sym_tab_addr >= str_tab_addr) { + std::println(stderr, "[inject] Just let the developer know 🙄"); + return std::nullopt; + } + + for (std::uint64_t cur_addr = sym_tab_addr; cur_addr < str_tab_addr; cur_addr += sizeof(Elf64_Sym)) { + Elf64_Sym sym {}; + maybe_read = process.read_memory(reinterpret_cast(cur_addr), &sym, sizeof(sym)); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read symbol from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (sym.st_name == 0 || sym.st_value == 0) + continue; + + std::array name_buf {}; + maybe_read = process.read_memory( + reinterpret_cast(str_tab_addr + sym.st_name), name_buf.data(), sizeof(name_buf)); + if (!maybe_read.has_value()) { + std::println(stderr, + "[inject] Failed to read symbol name from target libc: {} ({})", + maybe_read.error().message(), + maybe_read.error().value()); + return std::nullopt; + } + + if (std::strcmp(name_buf.data(), DLOPEN_SYMBOL_NAME) != 0) + continue; + + ret = reinterpret_cast(libc_base_addr + sym.st_value); + break; + } + + if (ret != nullptr) + break; + } + + if (ret == nullptr) + return std::nullopt; + + return ret; +} + +bool inject_library(const std::filesystem::path& library_path, const process& process) { + // Attach + + // Note to self: + // ptrace goes: op, pid, addr, data + // Meaning of addr and data changes depending on the ptrace request + // See: man 2 ptrace + + const process::pid_t pid = process.pid(); + const std::string path_str = library_path.string(); + + const auto attached = ptrace_cpp::attach_to_process(pid); + if (!attached) { + std::println(stderr, + "[ptrace] Failed to attach to remote process: {} ({})", + attached.error().message(), + attached.error().value()); + return false; + } + + const auto first_wait = ptrace_cpp::wait_on_process(pid); + if (std::get_if(&first_wait) == nullptr && + std::get_if(&first_wait) == nullptr) { + std::println(stderr, "[inject] Remote process did not stop by signal or syscall"); + ptrace_cpp::debug::print_wait_status(first_wait); + return false; + } + ptrace_cpp::debug::print_wait_status(first_wait); + + // Find a SYSCALL instruction (0x0f05) to jump from for use with mmap() & munmap() + + const auto libc_mem_regions = process.get_library_memory_regions(LIBC_SO_NAME); + if (libc_mem_regions.empty()) { + std::println(stderr, "[inject] Failed to get libc's memory map"); + return false; + } + + const auto exec_region_it = + std::find_if(libc_mem_regions.cbegin(), libc_mem_regions.cend(), [](const memory_region& r) { + return r.execute; + }); + + const auto syscall_instn_locs = + process.find_bytes_in_memory(reinterpret_cast(exec_region_it->start_addr), + (exec_region_it->end_addr - exec_region_it->start_addr), + SYSCALL_ENCODING); + if (!syscall_instn_locs) { + std::println(stderr, + "[inject] Failed to search for byte sequence in memory: {} ({})", + syscall_instn_locs.error().message(), + syscall_instn_locs.error().value()); + return false; + } + + if (syscall_instn_locs.value().empty()) { + std::println(stderr, "[inject] Did not find any SYSCALL instructions (0x0F05) sequences in memory"); + return false; + } + const auto& syscall_locs_vec = syscall_instn_locs.value(); + + // Store signal & execution state + + const auto orig_regs = ptrace_cpp::get_regs(pid); + if (!orig_regs) { + std::println(stderr, "[inject] Failed to get remote process' register state"); + return false; + } + + const auto set_opts = ptrace_cpp::set_options(pid, PTRACE_O_TRACESYSGOOD); + if (!set_opts) { + std::println(stderr, + "[ptrace] Failed to set ptrace options: {} ({})", + set_opts.error().message(), + set_opts.error().value()); + return false; + } + + const auto target_loaded_libs = process.get_loaded_libraries(); + const auto target_libc_it = std::find_if(target_loaded_libs.cbegin(), target_loaded_libs.cend(), [](const auto& a) { + return a.first.contains(LIBC_SO_NAME); + }); + if (target_libc_it == target_loaded_libs.cend()) { + std::println(stderr, "[inject] Failed to find libc in memory regions map"); + return false; + } + + const auto dlopen = resolve_remote_dlopen(process, target_libc_it->second[0].start_addr); + if (!dlopen.has_value()) { + std::println(stderr, "[inject] Failed to resolve dlopen() in remote process' libc"); + return false; + } + + std::println("[inject] Resolved dlopen() in remote process: {:#x}", + reinterpret_cast(dlopen.value())); + + // Call mmap() + + const auto shcode_alloc_size = std::bit_ceil(bin::dlopen_shellcode.size()); + const auto path_str_alloc_size = std::bit_ceil(path_str.size() + 1); + const auto write_buf_size = shcode_alloc_size + path_str_alloc_size; + + user_regs_struct mmap_regs = orig_regs.value(); + mmap_regs.rax = syscalls::x64::mmap; // mmap syscall number (x86_64) + mmap_regs.rdi = 0x0; // addr, NULL for any address + mmap_regs.rsi = write_buf_size; // length + mmap_regs.rdx = PROT_READ | PROT_WRITE | PROT_EXEC; // prot, all perms + mmap_regs.r10 = MAP_PRIVATE | MAP_ANONYMOUS; // flags + mmap_regs.r8 = -1; // fd, -1 seems to be "ignore" (?) + mmap_regs.r9 = 0; // offset + mmap_regs.rip = syscall_locs_vec[0]; + + if (!ptrace_cpp::set_regs(pid, mmap_regs)) { + std::println(stderr, "[ptrace] Failed to set register state for mmap() syscall"); + return false; + } + + std::println("[inject] Calling mmap({:#x}, {}, {:#x}, {:#x}, {:#x}, {})", + mmap_regs.rdi, + mmap_regs.rsi, + mmap_regs.rdx, + mmap_regs.r10, + mmap_regs.r8, + mmap_regs.r9); + + bool seen_mmap_entry = false; + bool seen_mmap_exit = false; + std::uintptr_t remote_alloc_addr = 0; + + while (!(seen_mmap_entry && seen_mmap_exit)) { + const auto cont = ptrace_cpp::continue_to_next_syscall(pid); + if (!cont) { + std::println(stderr, + "[ptrace] Failed to continue process execution: {} ({})", + cont.error().message(), + cont.error().value()); + return false; + } + + const auto mmap_wait = ptrace_cpp::wait_on_process(pid); + ptrace_cpp::debug::print_wait_status(mmap_wait); + + if (std::get_if(&mmap_wait) == nullptr) + continue; + + const auto& sys_stop = std::get(mmap_wait); + + if (sys_stop.op == PTRACE_SYSCALL_INFO_ENTRY) { + seen_mmap_entry = (sys_stop.entry.nr == syscalls::x64::mmap); + } else if (sys_stop.op == PTRACE_SYSCALL_INFO_EXIT) { + if (sys_stop.exit.is_error) { + std::println(stderr, "[inject] mmap() syscall failed"); + return false; + } + + remote_alloc_addr = sys_stop.exit.rval; + seen_mmap_exit = seen_mmap_entry; + } + } + + std::println("[inject] Allocated memory in remote process at {:#x}", remote_alloc_addr); + + std::vector write_buf(write_buf_size); + std::copy(bin::dlopen_shellcode.cbegin(), bin::dlopen_shellcode.cend(), write_buf.begin()); + const auto strcopy_end_it = std::copy(path_str.cbegin(), path_str.cend(), write_buf.begin() + shcode_alloc_size); + *strcopy_end_it = '\0'; // Null terminator + + const auto remote_lib_path_addr = remote_alloc_addr + shcode_alloc_size; + std::println("[inject] Writing shellcode & library path to {:#x} and {:#x}, total {} bytes", + remote_alloc_addr, + remote_lib_path_addr, + write_buf.size()); + process.write_memory((void*) remote_alloc_addr, (void*) &write_buf[0], write_buf.size()).value(); + + // Call the shellcode similarly to mmap + + user_regs_struct shellcode_regs = orig_regs.value(); + shellcode_regs.rip = remote_alloc_addr; + shellcode_regs.rdi = reinterpret_cast(dlopen.value()); + shellcode_regs.rsi = remote_lib_path_addr; + + if (!ptrace_cpp::set_regs(pid, shellcode_regs)) { + std::println(stderr, "[ptrace] Failed to set register state for shellcode"); + return false; + } + std::println("[inject] Running shellcode ..."); + + bool shellcode_success = false; + + while (!shellcode_success) { + const auto cont = ptrace_cpp::continue_to_next_syscall(pid); + if (!cont) { + std::println(stderr, + "[ptrace] Failed to continue process execution: {} ({})", + cont.error().message(), + cont.error().value()); + return false; + } + + const auto shellcode_wait = ptrace_cpp::wait_on_process(pid); + ptrace_cpp::debug::print_wait_status(shellcode_wait); + + if (std::get_if(&shellcode_wait)) { + const auto sys_stop = std::get(shellcode_wait); + if (sys_stop.op == PTRACE_SYSCALL_INFO_ENTRY) { + shellcode_success = (sys_stop.entry.nr == syscalls::x64::kill); + } + } + } + + // Zero out the old memory + + std::fill(write_buf.begin(), write_buf.end(), '\0'); + process.write_memory((void*) remote_alloc_addr, &write_buf[0], write_buf_size).value(); + + std::println("[inject] Zeroed memory in remote process"); + + // Call munmap() + // We end the shellcode on a syscall-enter-stop to kill(), swap orig_rax and wait for syscall-exit-stop + + user_regs_struct munmap_regs = orig_regs.value(); + munmap_regs.orig_rax = syscalls::x64::munmap; // munmap syscall number (x86_64) + munmap_regs.rdi = remote_alloc_addr; // addr + munmap_regs.rsi = write_buf_size; // length + + std::println("[inject] Calling munmap({:#x}, {})", munmap_regs.rdi, munmap_regs.rsi); + if (!ptrace_cpp::set_regs(pid, munmap_regs)) { + std::println(stderr, "[ptrace] Failed to set register state for munmap() call"); + return false; + } + + bool seen_munmap_exit = false; + while (!seen_munmap_exit) { + const auto cont = ptrace_cpp::continue_to_next_syscall(pid); + if (!cont) { + std::println(stderr, + "[ptrace] Failed to continue process execution: {} ({})", + cont.error().message(), + cont.error().value()); + return false; + } + + const auto munmap_wait = ptrace_cpp::wait_on_process(pid); + ptrace_cpp::debug::print_wait_status(munmap_wait); + + if (std::get_if(&munmap_wait) == nullptr) + continue; + + const auto sys_stop = std::get(munmap_wait); + + if (sys_stop.op == PTRACE_SYSCALL_INFO_EXIT) { + seen_munmap_exit = true; + if (sys_stop.exit.is_error) { + std::println(stderr, "[inject] WARNING: munmap() call failed"); + } + } + } + + std::println("[inject] Unmapped memory in remote process"); + + // Restore regs and signal + + ptrace_cpp::set_regs(pid, orig_regs.value()).value(); + std::println("[inject] Restoring original register state"); + + // Detach + + const auto detached = ptrace_cpp::detach_from_process(pid); + if (!detached) { + std::println(stderr, + "[ptrace] Remote process failed to detach: {} ({})", + detached.error().message(), + detached.error().value()); + return false; + } + + std::println("[inject] Detached from remote process"); + + return shellcode_success; +} diff --git a/donIV/src/process.cpp b/donIV/src/process.cpp new file mode 100644 index 0000000..36dac5d --- /dev/null +++ b/donIV/src/process.cpp @@ -0,0 +1,188 @@ +#include "process.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" +{ +#include +#include +#include +} + +std::expected process::read_memory(void* target_addr, + void* local_addr, + std::size_t len) const noexcept { + const iovec local {.iov_base = local_addr, .iov_len = len}; + const iovec remote {.iov_base = target_addr, .iov_len = len}; + + const auto n_bytes_read = process_vm_readv(m_process_id, &local, 1, &remote, 1, 0); + if (n_bytes_read <= 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return n_bytes_read; +} + +std::expected process::write_memory(void* target_addr, + void* local_addr, + std::size_t len) const noexcept { + const iovec local {.iov_base = local_addr, .iov_len = len}; + const iovec remote {.iov_base = target_addr, .iov_len = len}; + + const auto n_bytes_written = process_vm_writev(m_process_id, &local, 1, &remote, 1, 0); + if (n_bytes_written <= 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return n_bytes_written; +} + +std::expected, std::error_code> process::find_bytes_in_memory( + void* start_addr, std::size_t len, std::span bytes) const noexcept { + static constexpr const std::size_t max_fetch_size = 0x1000; + const std::size_t fetch_size = (len < max_fetch_size) ? len : max_fetch_size; + std::vector ret_vec; + + std::vector buf(fetch_size); + + for (std::size_t offset = 0; offset < len; offset += fetch_size) { + const auto read_addr = reinterpret_cast(start_addr) + offset; + const auto bytes_read = read_memory(reinterpret_cast(read_addr), &buf[0], fetch_size); + if (!bytes_read) { + return std::unexpected(bytes_read.error()); + } + + auto it = buf.cbegin(); + while (it != buf.cend()) { + it = std::search(it, buf.cend(), bytes.cbegin(), bytes.cend()); + if (it == buf.cend()) + continue; + + ret_vec.push_back(read_addr + std::distance(buf.cbegin(), it)); + std::advance(it, bytes.size()); + } + } + + return ret_vec; +} + +std::expected process::send_signal(int signum) const noexcept { + if (kill(m_process_id, signum) < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +std::optional process::get_exe_path() const { + std::filesystem::path proc_exe_path(std::format("/proc/{}/exe", m_process_id)); + + try { + const auto exists = std::filesystem::exists(proc_exe_path); + const auto is_symlink = std::filesystem::is_symlink(proc_exe_path); + + if (exists && is_symlink) { + return std::filesystem::read_symlink(proc_exe_path); + } else { + return std::nullopt; + } + } catch (const std::filesystem::filesystem_error& ex) { + return std::nullopt; + } +}; + +std::vector process::get_memory_regions() const { + std::vector ret_vec; + + const std::filesystem::path proc_maps_path(std::format("/proc/{}/maps", m_process_id)); + std::ifstream stream(proc_maps_path, std::ios::in); + + static constexpr auto regex_string = ctll::fixed_string { + R"(^([0-9a-fA-F]+)\-([0-9a-fA-F]+)\s([r|w|x|s|p|\-]{4})\s([0-9a-fA-F]+)\s([0-9a-fA-F]{2}):([" + "0-9a-fA-F]{2})\s(\d+)\s+(.*)$)"}; + + std::string curr_line; + while (!stream.eof() && !stream.bad()) { + std::getline(stream, curr_line); + if (curr_line.empty()) + continue; + + const auto match = ctre::match(curr_line); + + const auto start_addr = std::stoull(match.get<1>().str(), 0, 16); + const auto end_addr = std::stoull(match.get<2>().str(), 0, 16); + const auto& permissions = match.get<3>().str(); + const auto offset = std::stoull(match.get<4>().str(), 0, 16); + const std::uint8_t dev_major = stoul(match.get<5>().str(), 0, 16); + const std::uint8_t dev_minor = stoul(match.get<6>().str(), 0, 16); + const auto inode = stoull(match.get<7>().str(), 0, 10); + const auto path_name = match.get<8>().str(); + + ret_vec.emplace_back(start_addr, + end_addr, + offset, + path_name, + dev_major, + dev_minor, + inode, + permissions[0] == 'r', + permissions[1] == 'w', + permissions[2] == 'x', + permissions[3] == 'p', + permissions[3] == 's'); + } + + return ret_vec; +} + +std::vector process::get_library_memory_regions(std::string_view lib_name) const { + const auto memory_regions = get_memory_regions(); + + std::vector lib_mem_regions; + std::copy_if(memory_regions.cbegin(), + memory_regions.cend(), + std::back_inserter(lib_mem_regions), + [&lib_name](const auto& x) { + return x.path_name.contains(lib_name); + }); + return lib_mem_regions; +} + +process::lib_map_t process::get_loaded_libraries() const { + process::lib_map_t ret_map {}; + const auto memory_regions = get_memory_regions(); + ret_map.reserve(memory_regions.size()); + + for (const auto& region : memory_regions) { + if (region.path_name.empty()) + continue; + + if (ret_map.contains(region.path_name)) { + auto& it = ret_map.at(region.path_name); + it.push_back(region); + } else { + std::vector new_v {}; + auto inserted = ret_map.insert({region.path_name, std::move(new_v)}); + if (inserted.second) + inserted.first->second.push_back(region); + } + } + + return ret_map; +} diff --git a/donIV/src/ptrace.cpp b/donIV/src/ptrace.cpp new file mode 100644 index 0000000..e66daf0 --- /dev/null +++ b/donIV/src/ptrace.cpp @@ -0,0 +1,182 @@ +#include "ptrace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" +{ +#include +} + +using pt_req = __ptrace_request; +using ptrace_cpp::int_expected_t; +using ptrace_cpp::regs_expected_t; +using ptrace_cpp::syscall_info_expected_t; +using ptrace_cpp::void_expected_t; +using ptrace_cpp::wait_status_type_t; + +void_expected_t ptrace_cpp::attach_to_process(std::uint32_t pid) { + const auto err = ::ptrace(static_cast(PTRACE_ATTACH), pid, nullptr, nullptr); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +void_expected_t ptrace_cpp::detach_from_process(std::uint32_t pid) { + const auto err = ::ptrace(static_cast(PTRACE_DETACH), pid, nullptr, nullptr); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +void_expected_t ptrace_cpp::set_options(std::uint32_t pid, std::bitset<32> options) { + const auto err = ::ptrace(static_cast(PT_SETOPTIONS), pid, nullptr, options.to_ulong()); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +regs_expected_t ptrace_cpp::get_regs(std::uint32_t pid) { + user_regs_struct regs {}; + const auto err = ::ptrace(static_cast(PTRACE_GETREGS), pid, nullptr, ®s); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return regs; +} + +syscall_info_expected_t ptrace_cpp::get_syscall_info(std::uint32_t pid) { + ptrace_syscall_info syscall_info {}; + const auto err = ::ptrace(static_cast(PTRACE_GET_SYSCALL_INFO), pid, sizeof(syscall_info), &syscall_info); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return syscall_info; +} + +void_expected_t ptrace_cpp::set_regs(std::uint32_t pid, const user_regs_t& regs) { + const auto err = ::ptrace(static_cast(PTRACE_SETREGS), pid, nullptr, ®s); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +wait_status_type_t ptrace_cpp::wait_on_process(std::uint32_t pid) { + int status = 0; + const auto err = ::waitpid(pid, &status, 0); + if (err < 0) { + return std::make_error_code(static_cast(errno)); + } + + if (WIFSTOPPED(status)) { + const int signal = ptrace_cpp::status::status_to_stop_sig(status); + if (ptrace_cpp::signal::is_syscall(signal)) { + auto syscall_info = ptrace_cpp::get_syscall_info(pid); + if (!syscall_info) + return std::make_error_code(static_cast(errno)); + + return ptrace_cpp::syscall_stop_t(syscall_info.value()); + } else { + return ptrace_cpp::sig_stop_t(signal); + } + } else if (WIFEXITED(status)) { + return ptrace_cpp::exited_t(WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + return ptrace_cpp::term_by_t(WTERMSIG(status)); + } else if (WIFCONTINUED(status)) { + return ptrace_cpp::continued_t(); + } else { + return ptrace_cpp::continued_t(); + } + + return ptrace_cpp::continued_t(); +} + +void_expected_t ptrace_cpp::continue_execution(std::uint32_t pid, int sig_num) { + const auto err = ::ptrace(static_cast(PTRACE_CONT), pid, nullptr, sig_num); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +void_expected_t ptrace_cpp::continue_to_next_syscall(std::uint32_t pid, int sig_num) { + const auto err = ::ptrace(static_cast(PTRACE_SYSCALL), pid, nullptr, sig_num); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +void_expected_t ptrace_cpp::singlestep(std::uint32_t pid, int sig_num) { + const auto err = ::ptrace(static_cast(PTRACE_SINGLESTEP), pid, nullptr, sig_num); + if (err < 0) { + return std::unexpected(std::make_error_code(static_cast(errno))); + } + + return {}; +} + +void ptrace_cpp::debug::print_user_regs(const user_regs_t& regs) { + std::println("[ptrace] -- RIP: {:#018x}", regs.rip); + std::println("[ptrace] -- RAX: {:#018x} (Original)", regs.orig_rax); + std::println("[ptrace] -- RAX: {:#018x}", regs.rax); + std::println("[ptrace] -- RBX: {:#018x}", regs.rbx); + std::println("[ptrace] -- RCX: {:#018x}", regs.rcx); + std::println("[ptrace] -- RDX: {:#018x}", regs.rdx); + std::println("[ptrace] -- RDI: {:#018x}", regs.rdi); + std::println("[ptrace] -- RSI: {:#018x}", regs.rsi); + return; +} + +void ptrace_cpp::debug::print_wait_status(const wait_status_type_t& wait) { + std::visit( + [&wait](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + const auto sig_stop = std::get(wait); + std::println("[ptrace] -- Signal stop: {} ({})", strsignal(sig_stop.signal), sig_stop.signal); + } else if constexpr (std::is_same_v) { + const auto sys_stop = std::get(wait); + if (sys_stop.op == PTRACE_SYSCALL_INFO_ENTRY) { + std::println("[ptrace] -- Syscall entry: {}", sys_stop.entry.nr); + } else if (sys_stop.op == PTRACE_SYSCALL_INFO_EXIT) { + std::println("[ptrace] -- Syscall exit: {}, {}", + sys_stop.exit.is_error ? "error" : "success", + sys_stop.exit.rval); + } + } else if constexpr (std::is_same_v) + std::println("[ptrace] -- Process exited: {}", std::get(wait).exit_code); + else if constexpr (std::is_same_v) + std::println("[ptrace] -- Process terminated by signal: {}", + std::get(wait).term_sig); + else if constexpr (std::is_same_v) + std::println("[ptrace] -- Continued ..."); + else if constexpr (std::is_same_v) { + const std::error_code& err = std::get(wait); + std::println("[ptrace] -- Error: {} ({})", err.message(), err.value()); + } else + static_assert(false, "non-exhaustive visitor!"); + }, + wait); + return; +} diff --git a/include/cli.hpp b/include/cli.hpp new file mode 100644 index 0000000..5753e94 --- /dev/null +++ b/include/cli.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "process.hpp" + +#include +#include +#include + +enum class cli_parse_err : std::uint8_t { + missing_target_process, + missing_library_path, + invalid_argument, + not_enough_args, +}; + +using std::operator""sv; + +class cli { + public: + static std::expected try_construct(int argc, char** argv); + + static void print_usage(); + static std::string_view cli_parse_err_str(cli_parse_err err); + + public: + process::pid_t process_id = process::INVALID_PID; + std::filesystem::path library_path; +}; diff --git a/src/cli.cpp b/src/cli.cpp new file mode 100644 index 0000000..05e1f77 --- /dev/null +++ b/src/cli.cpp @@ -0,0 +1,132 @@ +#include "cli.hpp" +#include "process.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::operator""sv; + +void cli::print_usage() { + std::println("Usage: doniv-cli [-P process_name | -p process_id] -L library_path"); + return; +} +std::string_view cli::cli_parse_err_str(cli_parse_err err) { + switch (err) { + case cli_parse_err::missing_target_process: + return "Missing target process identifier (process name/PID)"sv; + case cli_parse_err::missing_library_path: + return "Missing path to library to inject"sv; + case cli_parse_err::invalid_argument: + return "Invalid command line arguments"sv; + case cli_parse_err::not_enough_args: + return "Not enough command line parameters"sv; + default: + return ""sv; + } +} + +std::expected cli::try_construct(int argc, char** argv) { + if (argc < 5) { + return std::unexpected(cli_parse_err::not_enough_args); + } + + const std::span cli_args(argv, argc); + int proc_name_arg_idx = 0; + int proc_pid_arg_idx = 0; + int lib_path_arg_idx = 0; + + for (int i = 0; i < cli_args.size(); ++i) { + const auto& arg = cli_args[i]; + const int next_idx = (i + 1); + + if (std::strcmp(arg, "-P") == 0) { + if (next_idx < cli_args.size()) { + proc_name_arg_idx = next_idx; + } + } else if (std::strcmp(arg, "-p") == 0) { + if (next_idx < cli_args.size()) { + proc_pid_arg_idx = next_idx; + } + } else if (std::strcmp(arg, "-L") == 0) { + if (next_idx < cli_args.size()) { + lib_path_arg_idx = next_idx; + } + } + } + + if (!proc_name_arg_idx && !proc_pid_arg_idx) { + return std::unexpected(cli_parse_err::missing_target_process); + } + + if (!lib_path_arg_idx) { + return std::unexpected(cli_parse_err::missing_library_path); + } + + if (proc_pid_arg_idx) { + // Specify process by PID (takes priority over process name) + const auto& pid_str = cli_args[proc_pid_arg_idx]; + const auto parsed_pid = std::strtoul(pid_str, nullptr, 0); + return cli {static_cast(parsed_pid), std::filesystem::path(cli_args[lib_path_arg_idx])}; + } else { + // Specify process by process name + for (const auto& procfs_entry : std::filesystem::directory_iterator("/proc/")) { + // Not a directory + if (!procfs_entry.is_directory()) + continue; + + std::string_view dir_name(procfs_entry.path().filename().c_str()); + const bool is_dir_name_numeric = std::all_of(dir_name.cbegin(), dir_name.cend(), [](const char c) { + return std::isdigit(c); + }); + + // Ensure this is a PID directory + if (!is_dir_name_numeric) + continue; + + // Ensure "exe" file node exists + const auto proc_exe_path = procfs_entry.path() / "exe"; + + try { + const auto exists = std::filesystem::exists(proc_exe_path); + const auto is_symlink = std::filesystem::is_symlink(proc_exe_path); + + if (!exists || !is_symlink) { + continue; + } + + const auto bin_path = std::filesystem::read_symlink(proc_exe_path); + if (bin_path.filename().string().compare(cli_args[proc_name_arg_idx]) == 0) { + const auto pid = std::strtoul(procfs_entry.path().filename().c_str(), nullptr, 0); + + if (pid == 0) { + return std::unexpected(cli_parse_err::invalid_argument); + } + + if (pid > std::numeric_limits::max() || errno == ERANGE) { + return std::unexpected(cli_parse_err::invalid_argument); + } + + return cli {static_cast(pid), std::filesystem::path(cli_args[lib_path_arg_idx])}; + } else { + continue; + } + } catch (const std::filesystem::filesystem_error& ex) { + // Missing permissions (?) + continue; + } + } + } + + return std::unexpected(cli_parse_err::missing_target_process); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9d8ee66 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,31 @@ +#include "cli.hpp" +#include "inject.hpp" +#include "process.hpp" + +#include +#include + +int main(int argc, char* argv[]) { + const auto maybe_cli_args = cli::try_construct(argc, argv); + + if (!maybe_cli_args.has_value()) { + const auto err = maybe_cli_args.error(); + std::println(stderr, "{}", cli::cli_parse_err_str(err)); + cli::print_usage(); + return EXIT_FAILURE; + } + + const auto& cli_args = maybe_cli_args.value(); + + process proc(cli_args.process_id); + std::println("[doniv-cli] Process ID: {}", proc.pid()); + std::println("[doniv-cli] Binary path: {}", proc.get_exe_path()->c_str()); + + if (!inject_library(cli_args.library_path, proc)) { + std::println("[inject] Failed to inject"); + return EXIT_FAILURE; + } + + std::println("[inject] Successfully injected"); + return EXIT_SUCCESS; +}