Skip to content

Instantly share code, notes, and snippets.

@neonbyte1
Created January 22, 2026 20:10
Show Gist options
  • Select an option

  • Save neonbyte1/9feea49b2cd05264049d3d769070ea2c to your computer and use it in GitHub Desktop.

Select an option

Save neonbyte1/9feea49b2cd05264049d3d769070ea2c to your computer and use it in GitHub Desktop.
Wrapper to load/unload shared object (.so) files using gdb, supporting HMR (hot-module-reload)
#!/usr/bin/env bash
#
# MIT License
#
# Copyright (c) 2026 - https://github.com/neonbyte1
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
_INJ_HELP=0
_INJ_EJECT=0
_INJ_HMR=0
_INJ_PROC_ID=0
_INJ_PROC_NAME=''
_INJ_TARGET=''
_INJ_HMR_POLL_RATE=1
while [[ $# -gt 0 ]]; do
case "${1}" in
-h|--help)
_INJ_HELP=1
shift
;;
--hmr|--hot|--hot-reload|--hot-module-reload)
_INJ_HMR=1
shift
;;
-e|--eject)
_INJ_EJECT=1
shift
;;
--process=*)
_INJ_PROC_NAME="${1#*=}"
shift
;;
--process)
_INJ_PROC_NAME="${2}"
shift; shift;
;;
--pid=*)
_INJ_PROC_ID="${1#*=}"
shift
;;
--pid)
_INJ_PROC_ID="${2}"
shift; shift
;;
--target=*)
_INJ_TARGET="${1#*=}"
shift
;;
--target)
_INJ_TARGET="${2}"
shift; shift
;;
--hot-poll-rate=*)
_INJ_HMR_POLL_RATE="${1#*=}"
shift
;;
--hot-poll-rate)
_INJ_HMR_POLL_RATE=
shift; shift
;;
*)
shift
;;
esac
done
if [ $_INJ_HELP -eq 1 ]; then
cat <<EOF
$0 [OPTIONS]
OPTIONS
----------------------------------------------------------------------------------
- Options marked with a star (*) are required. Either --process or --pid must be -
- specified; if both are provided, --pid takes precedence. -
----------------------------------------------------------------------------------
* --process STRING Exact name of the process into which the target
is injected or from which it is ejected.
* --pid INT Specific process ID into which the target is
injected or from which it is ejected. This
should be used if multiple processes with the
same name exist.
* --target STRING Absolute path to the target shared object (.so)
file to be injected or ejected.
--eject BOOL If set, ejects or unloads the shared object
from the target process.
--hot-module-reload BOOL If set, the script detects file changes in the
--hot-reload target shared object, ejects the old version,
--hot and injects the new one.
--hot-poll-rate INT Time in seconds in between the main loop.
Default: 1
Written by https://github.com/neonbyte1
EOF
exit 0
fi
if [ $_INJ_PROC_ID -ne 0 ]; then
if [ -z "$(ps aux | awk -v pid="$_INJ_PROC_ID" '$2 == pid')" ]; then
echo "[!] unable to find a process with pid: $_INJ_PROC_ID"
exit 1
fi
else
if [ -z "${_INJ_PROC_NAME}" ]; then
echo "[!] missing process name or process id, you should probably use the --help command first"
exit 1
fi
_INJ_PROC_ID="$(pidof "${_INJ_PROC_NAME}")"
if [ -z "${_INJ_PROC_ID}" ]; then
echo "[!] unable to find a process with name: ${_INJ_PROC_NAME}"
exit 1
fi
if [ "$(wc -w <<< "${_INJ_PROC_ID}")" -gt 1 ]; then
echo "[!] found multiple PIDs (${_INJ_PROC_ID}), use the --pid option instead"
exit 1
fi
fi
if [ -z "${_INJ_TARGET}" ]; then
echo "[!] missing a target to inject"
exit 1
fi
if [[ ! "${_INJ_TARGET}" =~ ^/ ]]; then
_INJ_TARGET="$(pwd)/${_INJ_TARGET}"
fi
if [ ! -f "${_INJ_TARGET}" ]; then
echo "[!] unable to locate ~${_INJ_TARGET#"$HOME"}"
exit 1
fi
if ! hash gdb 2>/dev/null; then
echo "[-] unable to find gdb in PATH"
exit 1
fi
function _gdb() {
gdb \
-n \
-q \
-batch \
-ex "attach $_INJ_PROC_ID" \
-ex "set \$dlopen = (void*(*)(char*, int)) dlopen" \
-ex "set \$dlclose = (int(*)(void*)) dlclose" \
"${@}" \
-ex "detach" \
-ex "quit" \
1>/dev/null
}
function is_loaded() {
(cat "/proc/$_INJ_PROC_ID/maps" | grep -q "${_INJ_TARGET}") && echo 1 || echo 0
}
function inject() {
if [ "$(is_loaded)" -eq 0 ]; then
_gdb -ex "call \$dlopen(\"${_INJ_TARGET}\", 1)"
if [ $? -eq 0 ]; then
echo "[+] injected library into target process"
fi
fi
}
function eject() {
if [ "$(is_loaded)" -eq 1 ]; then
# dlclose must be called twice, I don't know why but otherwise the
# library won't be ejected.
_gdb -ex "set \$library = \$dlopen(\"${_INJ_TARGET}\", 6)" \
-ex "call \$dlclose(\$library)" \
-ex "call \$dlclose(\$library)"
echo "[+] ejected library from target process"
fi
}
function _hmr_cleanup() {
eject
rm -rf "${_INJ_TARGET}"
exit 0
}
if [ "$(id -u)" -ne 0 ]; then
echo "[!] You need to run this script with sudo permissions"
exit 1
fi
if [ $_INJ_EJECT -eq 1 ]; then
eject
elif [ $_INJ_HMR -eq 0 ]; then
inject
else
_INJ_HMR_TARGET="${_INJ_TARGET}"
_INJ_TARGET="${_INJ_TARGET}.hmr"
# ensure previous file doesn't exists anymore
rm -rf "${_INJ_TARGET}"
trap _hmr_cleanup SIGINT SIGTERM
while true; do
if ! test -f "${_INJ_TARGET}"; then
cp "${_INJ_HMR_TARGET}" "${_INJ_TARGET}" || exit 1
fi
inject
_HMR_HASH_CURRENT="$(sha256sum "${_INJ_TARGET}" | awk '{print $1}')"
_HMR_HASH_NEXT="$(sha256sum "${_INJ_HMR_TARGET}" | awk '{print $1}')"
if [[ "${_HMR_HASH_NEXT}" != "${_HMR_HASH_CURRENT}" ]]; then
echo "[+] file change detected"
eject
rm "${_INJ_TARGET}"
fi
sleep 1
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment