diff --git a/flake.nix b/flake.nix index be403067..b00197ae 100644 --- a/flake.nix +++ b/flake.nix @@ -71,6 +71,14 @@ inputs.pwndbg = self; isDev = true; }; + pwndbg-lldb = import ./nix/pwndbg.nix { + pkgs = pkgsBySystem.${system}; + python3 = pkgsBySystem.${system}.python3; + gdb = pkgsBySystem.${system}.gdb; + inputs.pwndbg = self; + isDev = true; + isLLDB = true; + }; } // (portableDrvs system) // (tarballDrv system) @@ -82,6 +90,7 @@ pkgs = pkgsBySystem.${system}; python3 = pkgsBySystem.${system}.python3; inputs.pwndbg = self; + isLLDB = true; } ); }; diff --git a/nix/devshell.nix b/nix/devshell.nix index 7ef27d86..dd3b555e 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -12,11 +12,12 @@ import nixpkgs { overlays = [ ]; }, python3 ? pkgs.python3, inputs ? null, + isLLDB ? false, ... }: let pyEnv = import ./pyenv.nix { - inherit pkgs python3 inputs; + inherit pkgs python3 inputs isLLDB; lib = pkgs.lib; isDev = true; }; @@ -25,7 +26,7 @@ in default = pkgs.mkShell { NIX_CONFIG = "extra-experimental-features = nix-command flakes repl-flake"; # Anything not handled by the poetry env - nativeBuildInputs = with pkgs; [ + nativeBuildInputs = (with pkgs; [ # from setup-dev.sh nasm gcc @@ -38,7 +39,9 @@ in go pyEnv - ]; + ]) ++ pkgs.lib.optionals isLLDB (with pkgs; [ + lldb_19 + ]); shellHook = '' export PWNDBG_VENV_PATH="PWNDBG_PLEASE_SKIP_VENV" export ZIGPATH="${pkgs.lib.getBin pkgs.zig_0_10}/bin/" diff --git a/nix/pwndbg.nix b/nix/pwndbg.nix index 306ac896..73cc2619 100644 --- a/nix/pwndbg.nix +++ b/nix/pwndbg.nix @@ -4,6 +4,8 @@ gdb ? pkgs.gdb, inputs ? null, isDev ? false, + isLLDB ? false, + lldb ? pkgs.lldb_19, }: let binPath = pkgs.lib.makeBinPath ( @@ -14,6 +16,9 @@ let python3.pkgs.ropper # ref: https://github.com/pwndbg/pwndbg/blob/2023.07.17/pwndbg/commands/ropper.py#L30 python3.pkgs.ropgadget # ref: https://github.com/pwndbg/pwndbg/blob/2023.07.17/pwndbg/commands/rop.py#L34 ] + ++ pkgs.lib.optionals isLLDB [ + python3.pkgs.gnureadline + ] ); pyEnv = import ./pyenv.nix { @@ -22,7 +27,7 @@ let python3 inputs isDev - ; + isLLDB; lib = pkgs.lib; }; @@ -35,33 +40,60 @@ let '' ); - pwndbg = pkgs.stdenv.mkDerivation { - name = "pwndbg"; + pwndbg = let + pwndbgName = if isLLDB then "pwndbg-lldb" else "pwndbg"; + in pkgs.stdenv.mkDerivation { + name = pwndbgName; version = pwndbgVersion; - src = pkgs.lib.sourceByRegex inputs.pwndbg [ + src = pkgs.lib.sourceByRegex inputs.pwndbg ([ "pwndbg" "pwndbg/.*" + ] ++ (if isLLDB then [ + "lldbinit.py" + "pwndbg-lldb.py" + ] else [ "gdbinit.py" - ]; + ])); nativeBuildInputs = [ pkgs.makeWrapper ]; + buildInputs = [ pyEnv ]; - installPhase = '' + installPhase = let + fix_init_script = { target, line }: '' + # Build self-contained init script for lazy loading from vanilla gdb + # I purposely use insert() so I can re-import during development without having to restart gdb + sed "${line} i import sys, os\n\ + sys.path.insert(0, '${pyEnv}/${pyEnv.sitePackages}')\n\ + sys.path.insert(0, '$out/share/pwndbg/')\n\ + os.environ['PATH'] += ':${binPath}'\n" -i ${target} + ''; + in (if isLLDB then '' + mkdir -p $out/share/pwndbg + mkdir -p $out/bin + + cp -r lldbinit.py pwndbg $out/share/pwndbg + cp pwndbg-lldb.py $out/bin/${pwndbgName} + + ${fix_init_script { target = "$out/bin/${pwndbgName}"; line = "4"; } } + + touch $out/share/pwndbg/.skip-venv + wrapProgram $out/bin/${pwndbgName} \ + --prefix PATH : ${ pkgs.lib.makeBinPath [ lldb ] } \ + '' + (pkgs.lib.optionalString (!pkgs.stdenv.isDarwin) '' + --set LLDB_DEBUGSERVER_PATH ${ pkgs.lib.makeBinPath [ lldb ] }/lldb-server \ + '') + '' + --set PWNDBG_LLDBINIT_DIR $out/share/pwndbg + '' else '' mkdir -p $out/share/pwndbg cp -r gdbinit.py pwndbg $out/share/pwndbg - # Build self-contained init script for lazy loading from vanilla gdb - # I purposely use insert() so I can re-import during development without having to restart gdb - sed "2 i import sys, os\n\ - sys.path.insert(0, '${pyEnv}/${pyEnv.sitePackages}')\n\ - sys.path.insert(0, '$out/share/pwndbg/')\n\ - os.environ['PATH'] += ':${binPath}'\n" -i $out/share/pwndbg/gdbinit.py + ${fix_init_script { target = "$out/share/pwndbg/gdbinit.py"; line = "2"; } } touch $out/share/pwndbg/.skip-venv - makeWrapper ${gdb}/bin/gdb $out/bin/pwndbg \ + makeWrapper ${gdb}/bin/gdb $out/bin/${pwndbgName} \ --add-flags "--quiet --early-init-eval-command=\"set auto-load safe-path /\" --command=$out/share/pwndbg/gdbinit.py" - ''; + ''); meta = { pwndbgVenv = pyEnv; diff --git a/nix/pyenv.nix b/nix/pyenv.nix index 5a464b8c..6855e893 100644 --- a/nix/pyenv.nix +++ b/nix/pyenv.nix @@ -3,12 +3,13 @@ python3 ? pkgs.python3, inputs ? null, isDev ? false, + isLLDB ? false, lib, ... }: pkgs.poetry2nix.mkPoetryEnv { - groups = lib.optionals isDev [ "dev" ]; - checkGroups = lib.optionals isDev [ "dev" ]; + groups = lib.optionals isDev [ "dev" ] ++ lib.optionals isLLDB [ "lldb" ]; + checkGroups = lib.optionals isDev [ "dev" ] ++ lib.optionals isLLDB [ "lldb" ]; projectDir = inputs.pwndbg; python = python3; overrides = pkgs.poetry2nix.overrides.withDefaults ( diff --git a/pwndbg-lldb.py b/pwndbg-lldb.py index 0a4abf85..ae617ac4 100755 --- a/pwndbg-lldb.py +++ b/pwndbg-lldb.py @@ -41,16 +41,24 @@ def find_lldb_python_path() -> str: if __name__ == "__main__": + debug = "PWNDBG_LLDB_DEBUG" in os.environ + # Find the path for the LLDB Python bindings. path = find_lldb_python_path() sys.path.append(path) + if debug: + print(f"[-] Launcher: LLDB Python path: {path}") + # Older LLDB versions crash newer versions of CPython on import, so check # for it, and stop early with an error message. # # See https://github.com/llvm/llvm-project/issues/70453 lldb_version = find_lldb_version() + if debug: + print(f"[-] Launcher: LLDB version {lldb_version[0]}.{lldb_version[1]}") + if sys.version_info.minor >= 12 and lldb_version[0] <= 18: print("LLDB 18 and earlier is incompatible with Python 3.12 and later", file=sys.stderr) sys.exit(1) @@ -60,13 +68,32 @@ if __name__ == "__main__": lldb.SBDebugger.Initialize() debugger = lldb.SBDebugger.Create() - debugger.HandleCommand("command script import ./lldbinit.py") - debug = "PWNDBG_LLDB_DEBUG" in os.environ + # Resolve the location of lldbinit.py based on the environment, if needed. + lldbinit_dir = os.path.dirname(sys.argv[0]) + if "PWNDBG_LLDBINIT_DIR" in os.environ: + lldbinit_dir = os.environ["PWNDBG_LLDBINIT_DIR"] + lldbinit_dir = os.path.abspath(lldbinit_dir) + lldbinit_path = os.path.join(lldbinit_dir, "lldbinit.py") + + if debug: + print(f"[-] Launcher: Importing main LLDB module at '{lldbinit_path}'") + + if not os.path.exists(lldbinit_path): + print(f"Could not find '{lldbinit_path}, please specify it with PWNDBG_LLDBINIT_DIR") + sys.exit(1) + + if lldbinit_path not in sys.path: + sys.path.append(lldbinit_dir) + + # Load the lldbinit module we just found. + debugger.HandleCommand(f"command script import {lldbinit_path}") # Initialize the debugger, proper. import lldbinit + if debug: + print("[-] Launcher: Initializing Pwndbg") lldbinit.main(debugger, lldb_version[0], lldb_version[1], debug=debug) # Run our REPL until the user decides to leave. @@ -80,6 +107,9 @@ if __name__ == "__main__": from pwndbg.dbg.lldb.repl import run as run_repl + if debug: + print("[-] Launcher: Entering Pwndbg CLI") + run_repl([f"target create '{target}'"] if target else None, debug=debug) # Dispose of our debugger and terminate LLDB.