diff --git a/README.md b/README.md index dc954d6..ae87892 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,11 @@ This creates a tmux session with editor, compiler, and miscellaneous panes for e ### Core Debugging Engine - **Target**: Process control, breakpoint management, and binary inspection +- **Register Modification**: View and write register contents - **Macho**: Mach-O parser supporting 64-bit architectures and byte swapping -- **Exception Ports**: Mach-based exception handling for traps and signals - **ASLR**: Automatic slide detection for address resolution + ## Project Status This is an early-stage project under active development. The core expression evaluation system is functional, with binary analysis and other features in development. diff --git a/src/cmd/callable.hpp b/src/cmd/callable.hpp index e79d25f..cfe50fe 100644 --- a/src/cmd/callable.hpp +++ b/src/cmd/callable.hpp @@ -5,9 +5,11 @@ #include #include +#include "cmd/subcommand.hpp" #include "core/context.hpp" #include "core/target.hpp" #include "object.hpp" +#include "typedefs.hpp" class Interpreter; @@ -32,4 +34,30 @@ struct std::formatter { // NOLINT(cert-dcl58-cpp) } }; +class SubcommandCallable : public Callable { + protected: + SubcommandHandler m_subcmds; + + explicit SubcommandCallable(SubcommandHandler subcmds) + : m_subcmds(std::move(subcmds)) {} + + public: + Object call(std::vector args) override { + if (args.empty()) return std::monostate{}; + auto* subcmdStr = std::get_if(&args.front()); + if (subcmdStr == nullptr) return "Subcommand must be a string"; + std::string subcmd = *subcmdStr; + args.erase(args.begin()); + return m_subcmds.exec(subcmd, args); + } +}; + +inline FnPtr requiresRunningTarget(FnPtr fn) { + return [fn = std::move(fn)](const std::vector& args) -> Object { + auto& target = Context::getTarget(); + if (!target || !target->m_started) return "Target is not running!"; + return fn(args); + }; +} + #endif diff --git a/src/cmd/scanner.cpp b/src/cmd/scanner.cpp index 74393d2..c03beed 100644 --- a/src/cmd/scanner.cpp +++ b/src/cmd/scanner.cpp @@ -112,6 +112,18 @@ void Scanner::string() { } void Scanner::number() { + // Check for hex prefix + if (peek() == 'x' || peek() == 'X') { + advance(); + while (isxdigit(peek()) != 0) { + advance(); + } + const std::string text = m_source.substr(m_start, m_current - m_start); + auto value = static_cast(std::stoull(text, nullptr, 16)); + addToken(TokenType::NUMBER, value); + return; + } + while (isdigit(peek()) != 0) { advance(); } diff --git a/src/cmd/stdlib.hpp b/src/cmd/stdlib.hpp index 0b9194f..137d637 100644 --- a/src/cmd/stdlib.hpp +++ b/src/cmd/stdlib.hpp @@ -16,7 +16,6 @@ #include "core/target.hpp" #include "error.hpp" #include "expected.hpp" -#include "subcommand.hpp" #include "token_type.hpp" #include "typedefs.hpp" @@ -49,14 +48,9 @@ class PrintFn : public Callable { Object call(std::vector args) override { return args[0]; } }; -class BreakpointFn : public Callable { +class BreakpointFn : public SubcommandCallable { private: - static Object rmOrToggleBreakpoint(const std::vector& args, - bool toggle = false) { - Expected addrRes = detail::strToAddr(args[0]); - if (!addrRes) return addrRes.error(); - u64 addr = *addrRes; - + static Object rmOrToggleBreakpoint(u64 addr, bool toggle = false) { auto& target = Context::getTarget(); auto& bps = target->getRegisteredBreakpoints(); @@ -74,47 +68,48 @@ class BreakpointFn : public Callable { return std::format("Removed breakpoint at {}", addr); } - FnPtr list = [](const std::vector& args) -> Object { + static inline FnPtr list = + requiresRunningTarget([](const std::vector& args) -> Object { #pragma unused(args) - auto& breakpoints = Context::getTarget()->getRegisteredBreakpoints(); - std::string retStr{}; - - if (breakpoints.empty()) return "No breakpoints set!"; - - for (const auto& [k, v] : breakpoints) { - retStr += std::format("Breakpoint @ {} ({})\n", detail::toHex(k), - breakpoints[k].enabled); - } - - retStr.pop_back(); - return retStr; - }; - - FnPtr set = [](const std::vector& args) -> Object { - Expected addrRes = detail::strToAddr(args[0]); - if (!addrRes) return addrRes.error(); - const u64 addr = *addrRes; - - const i32 bpRes = Context::getTarget()->setBreakpoint(addr); - if (bpRes != 0) - return "Error setting breakpoint!"; - else - return std::format("Breakpoint set at: {}", detail::toHex(addr)); - }; - - FnPtr remove = [](const std::vector& args) -> Object { - return BreakpointFn::rmOrToggleBreakpoint(args, false); - }; - - FnPtr toggle = [](const std::vector& args) -> Object { - return BreakpointFn::rmOrToggleBreakpoint(args, true); - }; - - SubcommandHandler m_subcmds{{{sv("list"), list}, - {sv("set"), set}, - {sv("remove"), remove}, - {sv("toggle"), toggle}}, - "breakpoint"}; + auto& breakpoints = Context::getTarget()->getRegisteredBreakpoints(); + std::string retStr{}; + + if (breakpoints.empty()) return "No breakpoints set!"; + + for (const auto& [k, v] : breakpoints) { + retStr += std::format("Breakpoint @ {} ({})\n", detail::toHex(k), + breakpoints[k].enabled); + } + + retStr.pop_back(); + return retStr; + }); + + static inline FnPtr set = + requiresRunningTarget([](const std::vector& args) -> Object { + auto addr = detail::asU64(args.front()); + if (!addr) return addr.error(); + + const i32 bpRes = Context::getTarget()->setBreakpoint(*addr); + if (bpRes != 0) + return "Error setting breakpoint!"; + else + return std::format("Breakpoint set at: {}", detail::toHex(*addr)); + }); + + static inline FnPtr remove = + requiresRunningTarget([](const std::vector& args) -> Object { + auto addr = detail::asU64(args.front()); + if (!addr) return addr.error(); + return BreakpointFn::rmOrToggleBreakpoint(*addr, false); + }); + + static inline FnPtr toggle = + requiresRunningTarget([](const std::vector& args) -> Object { + auto addr = detail::asU64(args.front()); + if (!addr) return addr.error(); + return BreakpointFn::rmOrToggleBreakpoint(*addr, true); + }); public: [[nodiscard]] int arity() const override { return 1; } @@ -122,17 +117,12 @@ class BreakpointFn : public Callable { return ""; } - Object call(std::vector args) override { - if (m_target == nullptr) { - CoreError::error("Target is not running!"); - return std::monostate{}; - } - std::vector convertedArgs = detail::convertToStr(args); - if (convertedArgs.empty()) return std::monostate{}; - const std::string subcmd = convertedArgs.front(); - convertedArgs.erase(convertedArgs.begin()); - return m_subcmds.exec(subcmd, convertedArgs); - } + BreakpointFn() + : SubcommandCallable({{{sv("list"), list}, + {sv("set"), set}, + {sv("remove"), remove}, + {sv("toggle"), toggle}}, + "breakpoint"}) {} }; class RunFn : public Callable { @@ -221,32 +211,29 @@ class ContinueFn : public Callable { } }; -class TargetFn : public Callable { +class TargetFn : public SubcommandCallable { private: - FnPtr info = [](const std::vector& args) -> Object { + static inline FnPtr info = [](const std::vector& args) -> Object { #pragma unused(args) auto& target = Context::getTarget(); if (!target) return "Target not set!"; return target->getInfo(); }; - FnPtr set = [](const std::vector& args) -> Object { - auto& target = Context::getTarget(); - if (target) { - if (target->getTargetState() == TargetState::RUNNING || - target->getTargetState() == TargetState::STOPPED) - return "Cannot set new target whilst one is running!"; - } + static inline FnPtr set = + requiresRunningTarget([](const std::vector& args) -> Object { + auto& target = Context::getTarget(); - if (Target::isFileValid(args[0])) { - Context::setTarget(Target::create(args[0])); - return std::format("Target: {}", args[0]); - } + auto newTarget = detail::asString(args.front()); + if (newTarget) return newTarget.error(); - return std::format("{} is not a valid target path!", args[0]); - }; + if (Target::isFileValid(*newTarget)) { + Context::setTarget(Target::create(*newTarget)); + return std::format("Target: {}", *newTarget); + } - SubcommandHandler m_subcmds{{{sv("info"), info}, {sv("set"), set}}, "target"}; + return std::format("{} is not a valid target path!", *newTarget); + }); public: [[nodiscard]] int arity() const override { return 1; } @@ -254,42 +241,59 @@ class TargetFn : public Callable { return ""; } - Object call(std::vector args) override { - std::vector convertedArgs = detail::convertToStr(args); - if (convertedArgs.empty()) return std::monostate{}; - const std::string subcmd = convertedArgs.front(); - convertedArgs.erase(convertedArgs.begin()); - return m_subcmds.exec(subcmd, convertedArgs); + TargetFn() + : SubcommandCallable({{{sv("info"), info}, {sv("set"), set}}, "target"}) { } }; -class RegisterFn : public Callable { +class RegisterFn : public SubcommandCallable { private: - FnPtr view = [](const std::vector& args) -> Object { - auto& target = Context::getTarget(); - if (!target && !target->m_started) return "Target is not running!"; - if (args.front() == "all") - return target->formatRegisterOutput(&target->getLastKnownThreadState()); - auto res = findRegEntry(args.front()); - if (!res.hasValue()) return res.error(); - return std::format("{}: {}", args.front(), - detail::toHex(readRegValue( - target->getLastKnownThreadState(), *res.value()))); - }; - SubcommandHandler m_subcmds{{{sv("view"), view}}, "register"}; + static inline FnPtr view = + requiresRunningTarget([](const std::vector& args) -> Object { + auto& target = Context::getTarget(); + + auto reg = detail::asString(args.front()); + if (*reg == "all") + return target->formatRegisterOutput( + &target->getLastKnownThreadState()); + + auto entry = findRegEntry(*reg); + if (!entry) return entry.error(); + + return std::format( + "{}: {}", *reg, + detail::toHex(readRegValue(target->getLastKnownThreadState(), + *entry.value()))); + }); + + static inline FnPtr write = + requiresRunningTarget([](const std::vector& args) -> Object { + auto& target = Context::getTarget(); + + auto regName = detail::asString(args.front()); + if (!regName) return regName.error(); + + auto entry = findRegEntry(*regName); + if (!entry) return entry.error(); + + auto val = detail::asU64(args[1]); + if (!val) return val.error(); + + if (target->writeRegValue(*entry.value(), *val) != 0) + return "Error writing to register!"; + + return std::format("{}: {}", *regName, *val); + }); public: [[nodiscard]] int arity() const override { return 2; } [[nodiscard]] std::string str() const override { return ""; } - Object call(std::vector args) override { - std::vector convertedArgs = detail::convertToStr(args); - if (convertedArgs.empty()) return std::monostate{}; - const std::string subcmd = convertedArgs.front(); - convertedArgs.erase(convertedArgs.begin()); - return m_subcmds.exec(subcmd, convertedArgs); - } + + RegisterFn() + : SubcommandCallable( + {{{sv("view"), view}, {sv("write"), write}}, "register"}) {} }; #endif diff --git a/src/cmd/subcommand.hpp b/src/cmd/subcommand.hpp index 16878a7..85b62e8 100644 --- a/src/cmd/subcommand.hpp +++ b/src/cmd/subcommand.hpp @@ -24,7 +24,7 @@ class SubcommandHandler { const std::string_view callee) : m_subcommands(fns), m_callee(callee) {}; - Object exec(const std::string& subcmd, const std::vector& args) { + Object exec(const std::string& subcmd, const std::vector& args) { if (m_subcommands.contains(subcmd)) return std::invoke(m_subcommands[subcmd], args); CmdError::error(TokenType::IDENTIFIER, diff --git a/src/cmd/util.hpp b/src/cmd/util.hpp index 4810fb2..a87fdd7 100644 --- a/src/cmd/util.hpp +++ b/src/cmd/util.hpp @@ -5,13 +5,42 @@ #include #include "cmd/object.hpp" +#include "expected.hpp" #include "token_type.hpp" +#include "typedefs.hpp" namespace detail { bool isop(char c); TokenType ctoop(char c); double parseNumber(const std::string& str); std::vector convertToStr(const std::vector& vec); + +// Object extraction helpers +inline Expected asU64(const Object& obj) { + if (const auto* d = std::get_if(&obj)) { + return static_cast(*d); + } + if (const auto* s = std::get_if(&obj)) { + // Try parsing as hex/decimal + try { + if (s->starts_with("0x") || s->starts_with("0X")) { + return static_cast(std::stoull(*s, nullptr, 16)); + } + return static_cast(std::stoull(*s, nullptr, 10)); + } catch (...) { + return Unexpected{std::format("Cannot parse '{}' as number", *s)}; + } + } + return Unexpected{"Expected numeric value"}; +} + +inline Expected asString(const Object& obj) { + if (const auto* s = std::get_if(&obj)) { + return *s; + } + return Unexpected{"Expected string"}; +} + } // namespace detail #endif diff --git a/src/core/macho/macho.cpp b/src/core/macho/macho.cpp index ed59967..818d10c 100644 --- a/src/core/macho/macho.cpp +++ b/src/core/macho/macho.cpp @@ -349,6 +349,7 @@ kern_return_t catch_mach_exception_raise_state_identity( #pragma unused(codeCnt) #pragma unused(newState) #pragma unused(newStateCnt) +#pragma unused(flavour) auto& target = Context::getTarget(); target->setTargetState(TargetState::STOPPED); @@ -365,16 +366,6 @@ kern_return_t catch_mach_exception_raise_state_identity( if (macho->getThreadPort() == 0) macho->setThreadPort(thread); - if (exc == EXC_BAD_INSTRUCTION && *flavour == ARM_THREAD_STATE64) { - std::cout << std::format( - "Fault @ {}!\n", - detail::toHex(oldArmState->pc - macho->getAslrSlide())); - newArmState->pc += 4; - std::cout << std::format( - "Resuming @ {}\n", - detail::toHex(newArmState->pc - macho->getAslrSlide())); - } - if (exc == EXC_BREAKPOINT) { macho->restorePrevIns(oldArmState->pc - macho->getAslrSlide()); } @@ -631,3 +622,28 @@ void Macho::setThreadState(ThreadState* state) { } ThreadState& Macho::getLastKnownThreadState() { return m_last_thread_state; } + +u64 Macho::writeRegValue(const RegEntryT& regEntry, u64 val) { + auto* ptr = reinterpret_cast(&m_last_thread_state) + regEntry.offset; + + if (regEntry.size == 8) { + auto* reg = reinterpret_cast(ptr); + *reg = val; + } else { + auto* reg = reinterpret_cast(ptr); + *reg = val; + } + + const kern_return_t kr = + thread_set_state(m_thread_port, Macho::THREAD_FLAVOUR, + reinterpret_cast(&m_last_thread_state), + Macho::THREAD_STATE_COUNT); + + if (kr != KERN_SUCCESS) { + std::cout << "thread_set_state fail!\n"; + CoreError::error(mach_error_string(kr)); + return -1; + } + + return 0; +} diff --git a/src/core/macho/macho.hpp b/src/core/macho/macho.hpp index 4cf8688..415ba25 100644 --- a/src/core/macho/macho.hpp +++ b/src/core/macho/macho.hpp @@ -49,6 +49,7 @@ class Macho final : public Target { void resume(ResumeType cond) override; void setThreadState(ThreadState* state) override; ThreadState& getLastKnownThreadState() override; + u64 writeRegValue(const RegEntryT& regEntry, u64 val) override; static std::string exceptionReason(exception_type_t exc, mach_msg_type_number_t codeCnt, diff --git a/src/core/platform.hpp b/src/core/platform.hpp index 59b9e3f..1ba3e75 100644 --- a/src/core/platform.hpp +++ b/src/core/platform.hpp @@ -3,7 +3,6 @@ #include #include -#include #include #include diff --git a/src/core/target.hpp b/src/core/target.hpp index cc2489f..e20ab69 100644 --- a/src/core/target.hpp +++ b/src/core/target.hpp @@ -57,6 +57,7 @@ class Target { virtual void resume(ResumeType cond) = 0; virtual void setThreadState(ThreadState* state) = 0; virtual ThreadState& getLastKnownThreadState() = 0; + virtual u64 writeRegValue(const RegEntryT& regEntry, u64 val) = 0; void setTargetState(TargetState s) { m_state = s; } std::atomic& getTargetState() { return m_state; } diff --git a/src/expected.hpp b/src/expected.hpp index 7015347..e97f61e 100644 --- a/src/expected.hpp +++ b/src/expected.hpp @@ -18,21 +18,20 @@ class Expected { public: // NOLINTBEGIN(google-explicit-constructor) - Expected(const Result& val) : m_data(val) {} - Expected(Result&& val) : m_data(std::move(val)) {} - Expected(Unexpected err) : m_data(std::move(err.error)) {} + Expected(const Result& val) : m_data(std::in_place_index<0>, val) {} + Expected(Result&& val) : m_data(std::in_place_index<0>, std::move(val)) {} + Expected(Unexpected err) + : m_data(std::in_place_index<1>, std::move(err.error)) {} // NOLINTEND(google-explicit-constructor) - [[nodiscard]] bool hasValue() const { - return std::holds_alternative(m_data); - } + [[nodiscard]] bool hasValue() const { return m_data.index() == 0; } explicit operator bool() const { return hasValue(); } - Result& value() { return std::get(m_data); } - const Result& value() const { return std::get(m_data); } + Result& value() { return std::get<0>(m_data); } + const Result& value() const { return std::get<0>(m_data); } - Error& error() { return std::get(m_data); } - const Error& error() const { return std::get(m_data); } + Error& error() { return std::get<1>(m_data); } + const Error& error() const { return std::get<1>(m_data); } Result& operator*() { return value(); } Result* operator->() { return &value(); } diff --git a/src/typedefs.hpp b/src/typedefs.hpp index cd50878..c3ef851 100644 --- a/src/typedefs.hpp +++ b/src/typedefs.hpp @@ -4,13 +4,14 @@ #include #include #include +#include using u32 = std::uint32_t; using i32 = std::int32_t; using i64 = std::int64_t; using u64 = std::uint64_t; using MagicBytes = std::array; -using FnPtr = Object (*)(const std::vector&); +using FnPtr = std::function&)>; using sv = std::string_view; using u8 = std::uint8_t; diff --git a/test/cmd/test_environment.cpp b/test/cmd/test_environment.cpp index 8232f3b..395e71b 100644 --- a/test/cmd/test_environment.cpp +++ b/test/cmd/test_environment.cpp @@ -174,13 +174,13 @@ TEST_CASE("Test SubcommandHandler exec", "[subcommand][exec]") { SubcommandHandler handler({{"test", helpers::testSubcmd}}, "testcmd"); SECTION("Execute valid subcommand") { - Object result = handler.exec("test", {"hello"}); + Object result = handler.exec("test", {std::string("hello")}); REQUIRE(std::get(result) == "hello"); } SECTION("Execute invalid subcommand produces error") { auto captured = helpers::captureStream(std::cerr, [&handler]() { - Object result = handler.exec("invalid", {}); + Object result = handler.exec("invalid", std::vector{}); REQUIRE(std::holds_alternative(result)); }); REQUIRE(captured.find("Subcommand invalid is not valid") != diff --git a/test/cmd/test_helpers.hpp b/test/cmd/test_helpers.hpp index 1bdfdad..6d4fd81 100644 --- a/test/cmd/test_helpers.hpp +++ b/test/cmd/test_helpers.hpp @@ -93,8 +93,8 @@ inline Token makeToken(const std::string& name) { } // Simple subcommand for testing -inline Object testSubcmd(const std::vector& args) { - return args.empty() ? std::string("ok") : args[0]; +inline Object testSubcmd(const std::vector& args) { + return args.empty() ? Object{std::string("ok")} : args[0]; } } // namespace helpers diff --git a/test/cmd/test_stdlib.cpp b/test/cmd/test_stdlib.cpp index 4dddf2f..da88769 100644 --- a/test/cmd/test_stdlib.cpp +++ b/test/cmd/test_stdlib.cpp @@ -35,11 +35,10 @@ TEST_CASE("Test BreakpointFn without target", "[stdlib][breakpoint]") { SECTION("call when target is null produces error") { std::vector args = {std::string("list")}; - auto captured = helpers::captureStream(std::cerr, [&bp, &args]() { - Object result = bp.call(args); - REQUIRE(std::holds_alternative(result)); - }); - REQUIRE(captured.find("Target is not running") != std::string::npos); + Object result = bp.call(args); + REQUIRE(std::holds_alternative(result)); + REQUIRE(std::get(result).find("Target is not running") != + std::string::npos); } }