From d7bc6cbb3cdc1ec3f6bcc48adf3dd1591837a46f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 18 Apr 2025 00:38:50 +0000 Subject: [PATCH] add harden_memory_linux: harder for other processes to read our memory This module tries to restrict the ability of other processes to access the memory of our process. Traditionally, on Linux, one process can access the memory of another arbitrary process if both are running as the same user (uid). (Root can ofc access the memory of ~any process) Programs can opt-out from this by setting `prctl(PR_SET_DUMPABLE, 0);` also see https://man.archlinux.org/man/PR_SET_DUMPABLE.2const.en ----- Also, from https://unix.stackexchange.com/a/518452 : In a terminal window: ``` % echo $$ # show our pid 6744 % read -sp 'secret password: '; echo secret password: % ``` Then in another terminal window: ``` % grep heap /proc/6744/maps 01bb7000-01c3e000 rw-p 00000000 00:00 0 [heap] % dd if=/proc/6744/mem bs=1 skip=$((0x01bb7000)) count=$((0x01c3e000-0x01bb7000)) status=none | strings | less ... % dd if=/proc/6744/mem bs=1 skip=$((0x01bb7000)) count=$((0x01c3e000-0x01bb7000)) status=none | strings | grep obiwan obiwan_kenobi # "secret password" ``` --- electrum/harden_memory_linux.py | 94 +++++++++++++++++++++++++++++++++ run_electrum | 4 ++ 2 files changed, 98 insertions(+) create mode 100644 electrum/harden_memory_linux.py diff --git a/electrum/harden_memory_linux.py b/electrum/harden_memory_linux.py new file mode 100644 index 000000000..9d1c8bb82 --- /dev/null +++ b/electrum/harden_memory_linux.py @@ -0,0 +1,94 @@ +# Copyright (C) 2020 cptpcrd +# Copyright (C) 2025 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php +# +# based on https://github.com/cptpcrd/pyprctl/blob/578ed3e81066a8a61dede912454d5eeaef37eeea/pyprctl/ffi.py#L28 +# +# This module tries to restrict the ability of other processes to access the memory of our process. +# Traditionally, on Linux, one process can access the memory of another arbitrary process +# if both are running as the same user (uid). (Root can ofc access the memory of ~any process) +# Programs can opt-out from this by setting prctl(PR_SET_DUMPABLE, 0); +# +# Besides PR_SET_DUMPABLE, there are ways to globally restrict this for all processes: +# 1. The Yama (Linux Security Module) ptrace scope can be used to reduce these permissions +# This runtime kernel parameter can be set to the following options: +# 0 - Default attach security permissions. +# 1 - Restricted attach. Only child processes plus normal permissions. +# 2 - Admin-only attach. Only executables with CAP_SYS_PTRACE. +# 3 - No attach. No process may call ptrace at all. Irrevocable. +# # Note: The default value of kernel.yama.ptrace_scope is distro-specific. +# # See `$ cat /proc/sys/kernel/yama/ptrace_scope`. +# # - ubuntu 22.04 sets it to 1 (see /etc/sysctl.d/10-ptrace.conf), +# # - debian 12 sets it to 0 +# # - manjaro sets it to 1 +# 2. SELinux: ptrace can be restricted by setting the selinux deny_ptrace boolean. +# +# For a quick test on your system, try: +# $ cat /proc/$$/mem > /dev/null +# cat: /proc/4907/mem: Permission denied +# Getting "Permission denied" means access failed, "Input/output error" means access succeeded. + +import ctypes +import ctypes.util +import os +import sys +from typing import Optional + +from .logging import get_logger + + +_logger = get_logger(__name__) + +PR_GET_DUMPABLE = 3 +PR_SET_DUMPABLE = 4 + + +_libc = None # type: Optional[ctypes.CDLL] +def _load_libc(): + global _libc + if _libc is not None: + return + #assert sys.platform == "linux", sys.platform + # note: find_library can raise FileNotFoundError(OSError), see https://github.com/python/cpython/issues/93094 + _libc_path = ctypes.util.find_library("c") + _libc = ctypes.CDLL(_libc_path, use_errno=True) + _libc.prctl.argtypes = (ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong) + _libc.prctl.restype = ctypes.c_int + + +def set_dumpable(flag: bool) -> None: + """Set the "dumpable" attribute on the current process. + This controls whether a core dump will be produced if the process receives a signal whose + default behavior is to produce a core dump. + In addition, processes that are not dumpable cannot be attached with ptrace() PTRACE_ATTACH. + + In effect, another process running as the same user as us can read our memory if we are dumpable. + """ + _load_libc() + res = _libc.prctl(PR_SET_DUMPABLE, int(bool(flag)), 0, 0, 0) + if res < 0: + eno = ctypes.get_errno() + raise OSError(eno, os.strerror(eno), None, None, None) + + +def set_dumpable_safe(flag: bool) -> None: + try: + _load_libc() + except Exception as e: + _logger.exception("error loading libc") + return + assert _libc is not None + try: + set_dumpable(flag) + except OSError as e: + _logger.error(f"libc.prctl(PR_SET_DUMPABLE, {flag}) errored: {e}") + + +def get_dumpable() -> bool: + _load_libc() + res = _libc.prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) + if res < 0: + eno = ctypes.get_errno() + raise OSError(eno, os.strerror(eno), None, None, None) + return res != 0 diff --git a/run_electrum b/run_electrum index 970be1a95..eee0f62e4 100755 --- a/run_electrum +++ b/run_electrum @@ -419,6 +419,10 @@ def main(): print_stderr('unknown command:', uri) sys.exit(1) + if sys.platform == "linux" and not is_android: + import electrum.harden_memory_linux + electrum.harden_memory_linux.set_dumpable_safe(False) + if cmdname == 'daemon' and config.get("detach"): # detect lockfile. # This is not as good as get_file_descriptor, but that would require the asyncio loop