Public release 0.1
This commit is contained in:
commit
984b49d683
91
.clang-format
Normal file
91
.clang-format
Normal file
@ -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
|
2
.clang-tidy
Normal file
2
.clang-tidy
Normal file
@ -0,0 +1,2 @@
|
||||
Checks: '-*,cppcoreguidelines-*,performance-*,bugprone-*,clang-diagnostic-*,clang-analyzer-*'
|
||||
WarningsAsErrors: ''
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -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
|
28
CMakeLists.txt
Normal file
28
CMakeLists.txt
Normal file
@ -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)
|
16
README.md
Normal file
16
README.md
Normal file
@ -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/
|
24
donIV/CMakeLists.txt
Normal file
24
donIV/CMakeLists.txt
Normal file
@ -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)
|
7
donIV/include/inject.hpp
Normal file
7
donIV/include/inject.hpp
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "process.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
bool inject_library(const std::filesystem::path& library_path, const process& process);
|
61
donIV/include/process.hpp
Normal file
61
donIV/include/process.hpp
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<std::string, std::vector<memory_region>>;
|
||||
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<std::size_t, std::error_code> read_memory(void* target_addr,
|
||||
void* local_addr,
|
||||
std::size_t len) const noexcept;
|
||||
std::expected<std::size_t, std::error_code> write_memory(void* target_addr,
|
||||
void* local_addr,
|
||||
std::size_t len) const noexcept;
|
||||
std::expected<std::vector<std::uintptr_t>, std::error_code> find_bytes_in_memory(
|
||||
void* start_addr, std::size_t len, std::span<const std::uint8_t> bytes) const noexcept;
|
||||
std::expected<void, std::error_code> send_signal(int signum) const noexcept;
|
||||
|
||||
std::optional<std::filesystem::path> get_exe_path() const;
|
||||
std::vector<memory_region> get_memory_regions() const;
|
||||
std::vector<memory_region> 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;
|
||||
};
|
105
donIV/include/ptrace.hpp
Normal file
105
donIV/include/ptrace.hpp
Normal file
@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <bitset>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <expected>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/user.h>
|
||||
|
||||
#include <linux/ptrace.h> // Must be included after <sys/ptrace.h>
|
||||
}
|
||||
|
||||
namespace ptrace_cpp
|
||||
{
|
||||
using void_expected_t = std::expected<void, std::error_code>;
|
||||
using int_expected_t = std::expected<int, std::error_code>;
|
||||
using syscall_info_t = ptrace_syscall_info;
|
||||
using syscall_info_expected_t = std::expected<syscall_info_t, std::error_code>;
|
||||
using user_regs_t = user_regs_struct;
|
||||
using regs_expected_t = std::expected<user_regs_t, std::error_code>;
|
||||
|
||||
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<sig_stop_t, syscall_stop_t, exited_t, term_by_t, continued_t, std::error_code>;
|
||||
|
||||
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
|
11
donIV/include/syscalls.hpp
Normal file
11
donIV/include/syscalls.hpp
Normal file
@ -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
|
39
donIV/shellcode/CMakeLists.txt
Normal file
39
donIV/shellcode/CMakeLists.txt
Normal file
@ -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
|
||||
)
|
71
donIV/shellcode/bin2cppheader.py
Normal file
71
donIV/shellcode/bin2cppheader.py
Normal file
@ -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 <array>\n#include <cstdint>\nnamespace " + namespace + " { constexpr const std::array<std::uint8_t, "+ str(len(data)) + "> " + 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)
|
32
donIV/shellcode/dlopen_shellcode.s
Normal file
32
donIV/shellcode/dlopen_shellcode.s
Normal file
@ -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
|
461
donIV/src/inject.cpp
Normal file
461
donIV/src/inject.cpp
Normal file
@ -0,0 +1,461 @@
|
||||
#include "inject.hpp"
|
||||
#include "process.hpp"
|
||||
#include "ptrace.hpp"
|
||||
#include "syscalls.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <print>
|
||||
#include <sys/ptrace.h>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/user.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <linux/ptrace.h> // Must be included after <sys/ptrace.h>
|
||||
}
|
||||
|
||||
#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<std::uint8_t, 2> SYSCALL_ENCODING {0x0f, 0x05};
|
||||
|
||||
std::optional<void*> resolve_remote_dlopen(const process& process, std::uintptr_t libc_base_addr) {
|
||||
void* ret = nullptr;
|
||||
|
||||
std::array<char, EI_NIDENT> ident_buf {};
|
||||
auto maybe_read = process.read_memory(reinterpret_cast<void*>(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<void*>(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<void*>((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<void*>(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<void*>(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<char, sizeof(DLOPEN_SYMBOL_NAME)> name_buf {};
|
||||
maybe_read = process.read_memory(
|
||||
reinterpret_cast<void*>(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<void*>(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<ptrace_cpp::sig_stop_t>(&first_wait) == nullptr &&
|
||||
std::get_if<ptrace_cpp::syscall_info_t>(&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<void*>(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<std::uint64_t>(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<ptrace_cpp::syscall_stop_t>(&mmap_wait) == nullptr)
|
||||
continue;
|
||||
|
||||
const auto& sys_stop = std::get<ptrace_cpp::syscall_stop_t>(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<std::uint8_t> 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<std::uintptr_t>(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<ptrace_cpp::syscall_stop_t>(&shellcode_wait)) {
|
||||
const auto sys_stop = std::get<ptrace_cpp::syscall_stop_t>(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<ptrace_cpp::syscall_stop_t>(&munmap_wait) == nullptr)
|
||||
continue;
|
||||
|
||||
const auto sys_stop = std::get<ptrace_cpp::syscall_stop_t>(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;
|
||||
}
|
188
donIV/src/process.cpp
Normal file
188
donIV/src/process.cpp
Normal file
@ -0,0 +1,188 @@
|
||||
#include "process.hpp"
|
||||
|
||||
#include <ctre.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <print>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/uio.h>
|
||||
}
|
||||
|
||||
std::expected<std::size_t, std::error_code> 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<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return n_bytes_read;
|
||||
}
|
||||
|
||||
std::expected<std::size_t, std::error_code> 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<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return n_bytes_written;
|
||||
}
|
||||
|
||||
std::expected<std::vector<std::uintptr_t>, std::error_code> process::find_bytes_in_memory(
|
||||
void* start_addr, std::size_t len, std::span<const std::uint8_t> 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<std::uintptr_t> ret_vec;
|
||||
|
||||
std::vector<std::uint8_t> buf(fetch_size);
|
||||
|
||||
for (std::size_t offset = 0; offset < len; offset += fetch_size) {
|
||||
const auto read_addr = reinterpret_cast<std::uintptr_t>(start_addr) + offset;
|
||||
const auto bytes_read = read_memory(reinterpret_cast<void*>(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<void, std::error_code> process::send_signal(int signum) const noexcept {
|
||||
if (kill(m_process_id, signum) < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> 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<memory_region> process::get_memory_regions() const {
|
||||
std::vector<memory_region> 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<regex_string>(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<memory_region> process::get_library_memory_regions(std::string_view lib_name) const {
|
||||
const auto memory_regions = get_memory_regions();
|
||||
|
||||
std::vector<memory_region> 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<memory_region> 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;
|
||||
}
|
182
donIV/src/ptrace.cpp
Normal file
182
donIV/src/ptrace.cpp
Normal file
@ -0,0 +1,182 @@
|
||||
#include "ptrace.hpp"
|
||||
|
||||
#include <asm/ptrace-abi.h>
|
||||
#include <bitset>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <linux/ptrace.h>
|
||||
#include <print>
|
||||
#include <sys/ptrace.h>
|
||||
#include <utility>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <sys/wait.h>
|
||||
}
|
||||
|
||||
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<pt_req>(PTRACE_ATTACH), pid, nullptr, nullptr);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void_expected_t ptrace_cpp::detach_from_process(std::uint32_t pid) {
|
||||
const auto err = ::ptrace(static_cast<pt_req>(PTRACE_DETACH), pid, nullptr, nullptr);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void_expected_t ptrace_cpp::set_options(std::uint32_t pid, std::bitset<32> options) {
|
||||
const auto err = ::ptrace(static_cast<pt_req>(PT_SETOPTIONS), pid, nullptr, options.to_ulong());
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
regs_expected_t ptrace_cpp::get_regs(std::uint32_t pid) {
|
||||
user_regs_struct regs {};
|
||||
const auto err = ::ptrace(static_cast<pt_req>(PTRACE_GETREGS), pid, nullptr, ®s);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(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<pt_req>(PTRACE_GET_SYSCALL_INFO), pid, sizeof(syscall_info), &syscall_info);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(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<pt_req>(PTRACE_SETREGS), pid, nullptr, ®s);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(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<std::errc>(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<std::errc>(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<pt_req>(PTRACE_CONT), pid, nullptr, sig_num);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void_expected_t ptrace_cpp::continue_to_next_syscall(std::uint32_t pid, int sig_num) {
|
||||
const auto err = ::ptrace(static_cast<pt_req>(PTRACE_SYSCALL), pid, nullptr, sig_num);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(errno)));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void_expected_t ptrace_cpp::singlestep(std::uint32_t pid, int sig_num) {
|
||||
const auto err = ::ptrace(static_cast<pt_req>(PTRACE_SINGLESTEP), pid, nullptr, sig_num);
|
||||
if (err < 0) {
|
||||
return std::unexpected(std::make_error_code(static_cast<std::errc>(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<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, ptrace_cpp::sig_stop_t>) {
|
||||
const auto sig_stop = std::get<ptrace_cpp::sig_stop_t>(wait);
|
||||
std::println("[ptrace] -- Signal stop: {} ({})", strsignal(sig_stop.signal), sig_stop.signal);
|
||||
} else if constexpr (std::is_same_v<T, ptrace_cpp::syscall_stop_t>) {
|
||||
const auto sys_stop = std::get<ptrace_cpp::syscall_stop_t>(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<T, ptrace_cpp::exited_t>)
|
||||
std::println("[ptrace] -- Process exited: {}", std::get<ptrace_cpp::exited_t>(wait).exit_code);
|
||||
else if constexpr (std::is_same_v<T, ptrace_cpp::term_by_t>)
|
||||
std::println("[ptrace] -- Process terminated by signal: {}",
|
||||
std::get<ptrace_cpp::term_by_t>(wait).term_sig);
|
||||
else if constexpr (std::is_same_v<T, ptrace_cpp::continued_t>)
|
||||
std::println("[ptrace] -- Continued ...");
|
||||
else if constexpr (std::is_same_v<T, std::error_code>) {
|
||||
const std::error_code& err = std::get<std::error_code>(wait);
|
||||
std::println("[ptrace] -- Error: {} ({})", err.message(), err.value());
|
||||
} else
|
||||
static_assert(false, "non-exhaustive visitor!");
|
||||
},
|
||||
wait);
|
||||
return;
|
||||
}
|
28
include/cli.hpp
Normal file
28
include/cli.hpp
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "process.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
|
||||
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<cli, cli_parse_err> 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;
|
||||
};
|
132
src/cli.cpp
Normal file
132
src/cli.cpp
Normal file
@ -0,0 +1,132 @@
|
||||
#include "cli.hpp"
|
||||
#include "process.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <print>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
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, cli_parse_err> cli::try_construct(int argc, char** argv) {
|
||||
if (argc < 5) {
|
||||
return std::unexpected(cli_parse_err::not_enough_args);
|
||||
}
|
||||
|
||||
const std::span<char*> 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<process::pid_t>(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<process::pid_t>::max() || errno == ERANGE) {
|
||||
return std::unexpected(cli_parse_err::invalid_argument);
|
||||
}
|
||||
|
||||
return cli {static_cast<process::pid_t>(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);
|
||||
}
|
31
src/main.cpp
Normal file
31
src/main.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
#include "cli.hpp"
|
||||
#include "inject.hpp"
|
||||
#include "process.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <print>
|
||||
|
||||
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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user