meu*_*euh 25
正如这里所解释的,Linux 将程序的参数放在程序的数据空间中,并保留一个指向该区域开头的指针。这是 etc 用于ps查找和显示程序参数的内容。
由于数据在程序的空间中,它可以操作它。在不更改程序本身的情况下执行此操作涉及加载带有main()函数的 shim,该函数将在程序的真正 main 之前调用。这个 shim 可以将真正的参数复制到一个新的空间,然后覆盖原始参数,这样ps只会看到 nuls。
以下 C 代码执行此操作。
/* https://unix.stackexchange.com/a/403918/119298
* capture calls to a routine and replace with your code
* gcc -Wall -O2 -fpic -shared -ldl -o shim_main.so shim_main.c
* LD_PRELOAD=/.../shim_main.so theprogram theargs...
*/
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <dlfcn.h>
typedef int (*pfi)(int, char **, char **);
static pfi real_main;
/* copy argv to new location */
char **copyargs(int argc, char** argv){
char **newargv = malloc((argc+1)*sizeof(*argv));
char *from,*to;
int i,len;
for(i = 0; i<argc; i++){
from = argv[i];
len = strlen(from)+1;
to = malloc(len);
memcpy(to,from,len);
memset(from,'\0',len); /* zap old argv space */
newargv[i] = to;
argv[i] = 0;
}
newargv[argc] = 0;
return newargv;
}
static int mymain(int argc, char** argv, char** env) {
fprintf(stderr, "main argc %d\n", argc);
return real_main(argc, copyargs(argc,argv), env);
}
int __libc_start_main(pfi main, int argc,
char **ubp_av, void (*init) (void),
void (*fini)(void),
void (*rtld_fini)(void), void (*stack_end)){
static int (*real___libc_start_main)() = NULL;
if (!real___libc_start_main) {
char *error;
real___libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
}
real_main = main;
return real___libc_start_main(mymain, argc, ubp_av, init, fini,
rtld_fini, stack_end);
}
Run Code Online (Sandbox Code Playgroud)
无法干预main(),但您可以干预标准 C 库函数__libc_start_main,该函数继续调用 main。shim_main.c按照开头注释中的说明编译此文件,然后如图所示运行它。我printf在代码中留下了一个,所以你检查它是否真的被调用了。例如,运行
LD_PRELOAD=/tmp/shim_main.so /bin/sleep 100
Run Code Online (Sandbox Code Playgroud)
然后执行a ps,您将看到显示一个空白命令和args。
命令 args 仍有一小段时间可见。为了避免这种情况,例如,您可以更改垫片以从文件中读取您的机密并将其添加到传递给程序的参数中。
Dav*_*ter 16
阅读相关应用程序命令行界面的文档。可能有一个选项可以从文件中提供秘密,而不是直接作为参数提供。
如果失败,请针对该应用程序提交错误报告,理由是没有安全的方式为其提供机密。
您始终可以仔细(!)根据您的特定需求调整meuh 答案中的解决方案。特别注意Stéphane 的评论及其后续行动。
Chr*_*own 12
如果您需要将参数传递给程序以使其工作,那么如果您不能hidepid在 procfs 上使用,那么无论您做什么,您都将不走运。
既然你提到这是一个 bash 脚本,你应该已经有了可用的源代码,因为 bash 不是一种编译语言。
如果做不到这一点,您可以使用gdb或 类似的方法重写进程的 cmdline并在argc/argv一旦它已经启动时使用它,但是:
我真的只是建议获取源代码,或与供应商交谈以修改代码。在 POSIX 操作系统中的命令行上提供机密与安全操作不兼容。
Sté*_*las 11
当一个进程执行一个命令(通过execve()系统调用)时,它的内存会被擦除。为了在整个执行过程中传递一些信息,execve()系统调用需要两个参数:theargv[]和envp[]array。
这是两个字符串数组:
argv[] 包含参数envp[]以字符串的var=value形式包含环境变量定义(按照约定)。当你这样做时:
export SECRET=value; cmd "$SECRET"
Run Code Online (Sandbox Code Playgroud)
(这里在参数扩展周围添加了缺失的引号)。
你执行cmd与秘密(value)传入两个argv[]和envp[]。argv[]将是["cmd", "value"]和envp[]类似的东西[..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]。由于cmd没有执行任何getenv("SECRET")或等效的操作来从该SECRET环境变量中检索秘密的值,因此将其放入环境中是没有用的。
argv[]是公共知识。它显示在 的输出中ps。envp[]现在不是。在 Linux 上,它显示在/proc/pid/environ. 它显示在ps ewwwBSD的输出中(以及psLinux 上的procps-ng ),但只显示在使用相同有效 uid 运行的进程中(并且对 setuid/setgid 可执行文件有更多限制)。它可能会显示在一些审计日志中,但这些审计日志应该只能由管理员访问。
简而言之,传递给可执行文件的环境是私有的,或者至少与进程的内部存储器一样私有(在某些情况下,具有正确权限的其他进程也可以使用调试器进行访问,并且可以也被转储到磁盘)。
由于argv[]是公共知识,因此期望数据在其命令行上保密的命令被设计破坏了。
通常,需要提供机密的命令会为您提供另一个界面,例如通过环境变量。例如:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Run Code Online (Sandbox Code Playgroud)
或者通过像 stdin 这样的专用文件描述符:
echo secret | openssl rsa -passin stdin ...
Run Code Online (Sandbox Code Playgroud)
(echo内置,它不会显示在输出中ps)
或者一个文件,比如.netrcforftp和其他一些命令或者
mysql --defaults-extra-file=/some/file/with/password ....
Run Code Online (Sandbox Code Playgroud)
一些应用程序curl(这也是@meuh 在这里采用的方法)试图隐藏他们argv[]从窥探中收到的密码(在某些系统上通过覆盖存储argv[]字符串的内存部分)。但这并没有真正的帮助,而是提供了一个虚假的安全承诺。这在execve()和覆盖之间留下了一个窗口,ps仍然会显示秘密。
例如,如果攻击者知道您正在运行一个执行 a 的脚本curl -u user:somesecret https://...(例如在 cron 作业中),他所要做的就是从缓存中驱逐curl使用(例如通过运行 a sh -c 'a=a;while :; do a=$a$a;done')的(许多)库为了减慢它的启动速度,即使做一个非常低效的操作until grep 'curl.*[-]u' /proc/*/cmdline; do :; done也足以在我的测试中捕获该密码。
如果参数是您将秘密传递给命令的唯一方法,那么您可能仍然可以尝试一些事情。
在某些系统上,包括较旧版本的 Linux,只能argv[]查询in 字符串的前几个字节(Linux 4.1 及之前版本为 4096)。
在那里,你可以这样做:
IPMI_PASSWORD=secret ipmitool -I lan -U admin...
Run Code Online (Sandbox Code Playgroud)
秘密将被隐藏,因为它超过了前 4096 个字节。现在使用该方法的人现在一定会后悔的,因为 Linux 自 4.2 起不再截断/proc/pid/cmdline. 还请注意,这并不是因为ps不能使用相同的 APIps来获得更多的命令行(例如在 FreeBSD 上,它似乎仅限于 2048 个字节)不会显示更多字节。然而,这种方法在ps普通用户检索该信息的唯一方式的系统上是有效的(例如当 API 具有特权并且ps是 setgid 或 setuid 以便使用它时),但在那里仍然可能不是面向未来的。
另一种方法是不传递秘密,argv[]而是在程序启动之前将代码注入程序(使用gdb或$LD_PRELOAD黑客)main(),将秘密插入argv[]到从execve().
对于LD_PRELOADGNU 系统上的非 setuid/setgid 动态链接的可执行文件:
echo secret | openssl rsa -passin stdin ...
Run Code Online (Sandbox Code Playgroud)
然后:
$ gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl
$ LD_PRELOAD=$PWD/inject_secret.so ps '*****' 9<<< "-opid,args"
PID COMMAND
7659 /bin/zsh
8828 ps *****
Run Code Online (Sandbox Code Playgroud)
在任何时候都ps不会显示ps -opid,args那里(-opid,args在这个例子中是秘密)。请注意,我们正在替换指针argv[]数组的元素,而不是覆盖这些指针指向的字符串,这就是为什么我们的修改没有显示在.ps
有了gdb,还是非的setuid / setgid程序动态链接的可执行文件和GNU系统:
mysql --defaults-extra-file=/some/file/with/password ....
Run Code Online (Sandbox Code Playgroud)
仍然使用gdb,不依赖于动态链接的可执行文件或具有调试符号的非 GNU 特定方法至少应该适用于 Linux 上的任何 ELF 可执行文件:
(exec -a "$(printf %-4096s cmd)" cmd "$secret")
Run Code Online (Sandbox Code Playgroud)
使用静态链接的可执行文件进行测试:
$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****
Run Code Online (Sandbox Code Playgroud)
当可执行文件可能是静态的时,我们没有一种可靠的方法来分配内存来存储秘密,因此我们必须从进程内存中已经存在的其他地方获取秘密。这就是为什么这里的环境是显而易见的选择。我们还将该SECRETenv var隐藏到进程中(通过将其更改为SECRE=)以避免在进程出于某种原因决定转储其环境或执行不受信任的应用程序时它会泄漏。
这也适用于 Solaris 11(前提是安装了 gdb 和 GNU binutils(您可能需要重命名objdump为gobjdump)。
在 FreeBSD 上(至少 x86_64,我不确定堆栈上的前 24 个字节(当 gdb (8.0.1) 交互时变成 16 表明 gdb 中可能存在错误)是什么),替换argc和argv定义和:
set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]
Run Code Online (Sandbox Code Playgroud)
(您可能还需要安装gdb软件包/端口,因为系统附带的版本是古老的)。
| 归档时间: |
|
| 查看次数: |
4877 次 |
| 最近记录: |