• 主页
友链

  • 主页

聊聊Linux进程参数变形与隐藏

2024-01-21

渗透测试场景下,基于命令行检测恶意行为是一个常见的防御方案,例如 xxx -h 10.0.0.1/24 就容易被认为是一个恶意的扫描命令。对于这种检测方式,容易想到的是通过修改程序对读取参数的逻辑做修改。但是在渗透测试场景下,通常需要使用各种不同的工具,如代理、网络转发等,逐一修改并跟踪上游软件的更新是一件繁琐的工作。因而本文尝试讨论一种相对通用的参数变形与隐藏方案。

已有方案

最简单的方案是从日志、shell history中隐藏命令,例如alias、脚本可以从历史记录中隐藏命令执行的参数,环境变量如 example -arg $ARG 的方式可以隐藏一些关键的参数。然而,这些方案只能绕过极少部分防御措施,要真正实现参数隐藏,我们需要采取更加深入的方法。

一个入门级的方案是在程序中修改参数。例如,在C语言中,我们可以通过修改argv来隐藏敏感信息。下面这个程序是一个简单的sample,将输入的参数做一次 rot13 变换之后再处理。这种方案下,可以绕过大部分基于参数的检测方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void read_and_print_cmdline() {
FILE* fp = fopen("/proc/self/cmdline", "r");
if (fp == NULL) {
perror("fopen");
return;
}

char ch;
while ((ch = fgetc(fp)) != EOF) {
if (ch == '\0') {
putchar(' ');
} else {
putchar(ch);
}
}

fclose(fp);
putchar('\n');
}

void apply_rot13(char *str) {
char *p = str;
while (*p != '\0') {
if ('a' <= *p && *p <= 'z') {
*p = ((*p - 'a') + 13) % 26 + 'a';
} else if ('A' <= *p && *p <= 'Z') {
*p = ((*p - 'A') + 13) % 26 + 'A';
}
p++;
}
}

void process_args(int argc, char *argv[]) {
for (int i = 1; i < argc; ++i) {
apply_rot13(argv[i]);
}
}

int main(int argc, char *argv[]) {
read_and_print_cmdline();
process_args(argc, argv);
read_and_print_cmdline();
// 调用参数解析逻辑
// ...
process_args(argc, argv);
read_and_print_cmdline();

return 0;
}

执行程序的一个示例输入输出如下

1
2
3
4
$ ./a.out --name grfg
./a.out --name grfg
./a.out --anzr test
./a.out --name grfg

然而,这种方法并不一定完全通用,因为程序可能在初始化之后仍然依赖这些参数,如果初始化之后再次通过 argv 来获取相关的参数值,这种方法就可能会影响程序的正常运行,

在有高权限的情况下,可以通过内核机制来实现参数隐藏,例如可以使用基于内核hook get_cmdline等函数、隐藏进程结构体等方式来实现参数隐藏,也可以通过eBPF等机制hook对应的处理逻辑,相对简单的来实现参数的隐藏。

Golang方案

前文提到了一些已有的方案,但是C语言修改源码方案可能因为后续调用而没有足够的稳定性。使用rootkit等方案又会引入额外的复杂度和渗透痕迹。

那么,是否存在一种相对简单、稳定、没有权限要求的方案来实现参数隐藏呢?

初看之下没有很合适的方案,而在探索过程中,本文发现许多常用的渗透工具,如gost、frp和fscan,都是使用Golang实现的。

因此,本文尝试先缩小问题范围,考虑是否有合适的方案来实现面向Golang应用程序的参数隐藏方案。

首先,让我们来看看Golang的参数实现。从源码中不难发现,无论是 cobra 等框架,还是使用flag等Golang自带的实现,它们都是基于os.Args[1:]来获取参数。

那么,os.Args是如何赋值的呢?在 src/os/proc 中可以看到 Args = runtime_args() 。其中 runtime_args 实际是返回了 argslice 的数组。

最后在汇编代码中可以看到 runtime 相关的参数实际是从 argv 中拷贝出来的。

1
2
3
4
5
6
7
MOVW    8(RSP), R0  // copy argc
MOVW R0, -8(RSP)
MOVD 16(RSP), R0 // copy argv
MOVD R0, 0(RSP)
BL runtime·args(SB)
BL runtime·osinit(SB)
BL runtime·schedinit(SB)

也就是说,由于 os.Args 是复制的,直接修改 Golang 程序的 os.Args 是不会影响程序的原本的 argv 参数,即修改这个变量不会导致 ps aux 等工具的输出被修改,也不会有coredump等问题,全程所有的引用也都是面向 os.Args ,不会有不一致的问题。

基于这个原理,可以提出一个隐藏方案,下面是一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// cmdline.go
package main

import (
"strings"
"flag"
"fmt"
"io/ioutil"
)

func hookArgs() {
arg := os.Getenv("ARG")
if arg == "" {
return
}

newArgs := strings.Split(arg, " ")
os.Args = append([]string{os.Args[0]}, newArgs...)
}

func readCmdline() string {
data, err := ioutil.ReadFile("/proc/self/cmdline")
if err != nil {
fmt.Printf("Error reading /proc/self/cmdline: %v\n", err)
return ""
}
return strings.ReplaceAll(string(data), "\x00", " ")
}

func main() {
fmt.Printf("/proc/self/cmdline: %s\n", readCmdline())
hookArgs()
var name string
flag.StringVar(&name, "name", "", "Name to be used")
flag.Parse()
fmt.Println("Real Name:", name)
}

在这个示例程序中,通过从环境变量中读取参数实现了隐藏真实参数的效果,执行以下命令的结果如下:

1
2
3
4
$ export ARG='-name real_passed_name'
$ ./cmdline --fake-arg fake_passed
/proc/self/cmdline: ./cmdline --fake-arg fake_passed
Real Name: real_passed_name

为了更加通用,我们可以将上述方案封装成一个库。只需要导入该库,即可在其他程序中实现参数隐藏,无需重新编译。对于上文的程序,可以用这种方式实现同样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"strings"
"flag"
"fmt"
"io/ioutil"

_ "github.com/lylemi/passerby"
)

func readCmdline() string {
data, err := ioutil.ReadFile("/proc/self/cmdline")
if err != nil {
fmt.Printf("Error reading /proc/self/cmdline: %v\n", err)
return ""
}
return strings.ReplaceAll(string(data), "\x00", " ")
}

func main() {
fmt.Printf("/proc/self/cmdline: %s\n", readCmdline())

var name string
flag.StringVar(&name, "name", "", "Name to be used")
flag.Parse()
fmt.Println("Real Name:", name)
}

再返回来考虑一下原理,不难想到,由于高级语言都有自己的字符串结构而不是简单的一段内存,所以这种隐藏方式同样适用于其他脚本语言和高级语言,只需要每种语言实现一种对应的 hook 方能即可。

通用方案

上文提到的方式对单个语言有效,但是仍然需要重新编译。那么是否有不需要重新编译的方案呢?回顾上文提到的方案,其实也是有的。我们可以通过实现一个C语言 wrapper 的方式来实现上文同样的效果。具体来说,实现逻辑如下:

  • C程序通过execve启动子程序,子进程通过 ptrace(PTRACE_TRACEME, 0, NULL, NULL); 即可设置父进程不需要 root / CAP_SYS_PTRACE 权限可以 ptrace 到子进程。
  • 父进程通过解析子进程对应的可执行程序,获取 main 函数地址
  • 父进程通过 ptrace 在子程序 main 函数处下断点
  • 执行到子进程 main 函数时,父进程通过 PTRACE_POKEDATA 将 argv 修改为环境变量中的参数
  • 执行一段时间后,父进程再次修改子进程为其它参数

总结

综上所述,本文讨论了参数隐藏的技巧与实现方法。通过这些方案,我们可以在渗透测试等场景中实现参数隐藏和变形,提高工具的隐蔽性。

References

  • gost
  • frp
  • fscan
  • cobra
  • Golang flag 实现
  • Golang src/os/proc
  • Golang runtime_args
  • Golang runtime 汇编
  • Hook 库
  • 渗透测试

扫一扫,分享到微信

微信分享二维码
大范围IP端口扫描优化:内核篇
服务识别方案探索:单TCP流识别
© 2024 Lyle
Hexo Theme Yilia by Litten
  • 友链
  • rebirth