赞
Linux进程伪装:动态修改/proc/self/exe
序
我们知道/proc/pid目录下记录了进程的很多信息,比如/proc/pid/cmdline是进程启动时的命令行、/proc/pid/exe是一个软连接指向了该进程可执行ELF、/proc/pid/cwd是一个软连接指向了进程运行的当前路径等等。
为监控查杀恶意进程,主动防御软件在一个进程在启动时,会记录该进程ID、命令行、可执行ELF路径等。其中进程ID、命令行都可以非常容易的动态修改,这里直接给出代码,网上资料很多,细节不再赘述。
extern char **environ;
static char **g_main_Argv = NULL; /* pointer to argument vector */
static char *g_main_LastArgv = NULL; /* end of argv */
void setproctitle_init(int argc, char **argv, char **envp)
{
int i;
for (i = 0; envp[i] != NULL; i++) // calc envp num
continue;
environ = (char **) malloc(sizeof (char *) * (i + 1)); // malloc envp pointer
for (i = 0; envp[i] != NULL; i++)
{
environ[i] = malloc(sizeof(char) * strlen(envp[i]));
strcpy(environ[i], envp[i]);
}
environ[i] = NULL;
g_main_Argv = argv;
if (i > 0)
g_main_LastArgv = envp[i - 1] + strlen(envp[i - 1]);
else
g_main_LastArgv = argv[argc - 1] + strlen(argv[argc - 1]);
}
void setproctitle(const char *fmt, ...)
{
char *p;
int i;
char buf[MAXLINE];
extern char **g_main_Argv;
extern char *g_main_LastArgv;
va_list ap;
p = buf;
va_start(ap, fmt);
vsprintf(p, fmt, ap);
va_end(ap);
i = strlen(buf);
if (i > g_main_LastArgv - g_main_Argv[0] - 2)
{
i = g_main_LastArgv - g_main_Argv[0] - 2;
buf[i] = '\0';
}
//修改argv[0]
(void) strcpy(g_main_Argv[0], buf);
p = &g_main_Argv[0][i];
while (p < g_main_LastArgv)
*p++ = '\0';
g_main_Argv[1] = NULL;
//调用prctl
prctl(PR_SET_NAME,buf);
}
int main(int argc, char * argv[])
{
setproctitle_init(argc, argv, environ);
setproctitle("[sshd]");
...
}
以上代码将进程名修改为[sshd]。变换进程ID代码就更加简单
//fork 返回值为0,表示是子进程;返回值大于0,父进程;小于0,错误
if(fork() > 0) exit(0);
父进程退出,子进程继续,进程ID改变、逻辑不变。
修改/proc/self/exe链接的几种办法
execve系列函数
这是最直接的办法,execve会在进程ID不变的情况下,将进程内容全部替换、重新装载进程,所以/proc/self/exe也同样被替换了。比如:
//file: a.c
int main()
{
printf("PID=%d\n", getpid());
getchar();
return 0;
}
将a.c编译生成a,直接./a执行a,查看该进程的/proc/pid/exe
root@ubuntu1804:~# ls -l /proc/94635/exe
lrwxrwxrwx 1 root root 0 Sep 9 16:13 /proc/94635/exe -> /root/a
修改代码为
//file: a.c
int main()
{
printf("PID=%d\n", getpid());
execve("/bin/sh", 0, 0);
return 0;
}
重新编译执行,查看其exe已经变化
root@ubuntu1804:~# ls -l /proc/94656/exe
lrwxrwxrwx 1 root root 0 Sep 9 16:14 /proc/94656/exe -> /bin/dash
为了更具有实用性,可以使用内存ELF文件的形式,结合系统调用execveat
static unsigned char elf_file_data[] = {
//ELF文件内容,太长省略
};
#define PAGE_SZ 0x1000
#define PAGE_ALIGN(size) (size % PAGE_SZ == 0 ? size : ((size / PAGE_SZ + 1)*PAGE_SZ))
int main()
{
int fd = memfd_create("", 1);
const int len = sizeof(elf_file_data);
ftruncate(fd, len);
char * shm = mmap(NULL, PAGE_ALIGN(len), 3, 1, fd, 0);
if(shm == MAP_FAILED)
{
perror("mmap");
return -1;
}
memcpy(shm, elf_file_data, len);
munmap(shm, PAGE_ALIGN(len));
execveat(fd, "", 0, 0, 0x1000);
return 0;
}
查看该进程的/proc/pid/exe,会变为'/memfd: (deleted)'
root@ubuntu1804:~# ls -l /proc/94901/exe
lrwxrwxrwx 1 root root 0 Sep 9 16:47 /proc/94901/exe -> '/memfd: (deleted)'
prctl(PR_SET_MM, PR_SET_MM_EXE_FILE, ...)
又是prctl系统调用提供的能力,man中原文如下:
PR_SET_MM_EXE_FILE
Supersede the /proc/pid/exe symbolic link with a
new one pointing to a new executable file
identified by the file descriptor provided in arg3
argument. The file descriptor should be obtained
with a regular open(2) call.
To change the symbolic link, one needs to unmap all
existing executable memory areas, including those
created by the kernel itself (for example the
kernel usually creates at least one executable
memory area for the ELF .text section).
In Linux 4.9 and earlier, the PR_SET_MM_EXE_FILE
operation can be performed only once in a process's
lifetime; attempting to perform the operation a
second time results in the error EPERM. This
restriction was enforced for security reasons that
were subsequently deemed specious, and the
restriction was removed in Linux 4.10 because some
user-space applications needed to perform this
operation more than once.
需要注意的是,这里有一个前提:one needs to unmap all existing executable memory areas, including those created by the kernel itself。
翻译:要把旧的/proc/self/exe映射的内存都unmap掉,才能使用此命令字。这个操作几乎所有的加壳软件都会做,比如开源的UPX。先写如下代码:
//file: a.c
int prctl_routine(char* name)
{
errno = 0;
int fd = open(name, O_RDONLY);
if(fd < 0)
{
perror("open");
return EXIT_FAILURE;
}
int ret = prctl(PR_SET_MM, PR_SET_MM_EXE_FILE, fd, 0, 0);
if(ret < 0)
{
perror("prctl");
}
close(fd);
return 0;
}
int main()
{
char exe[256] = {0};
readlink("/proc/self/exe", exe, 255);
printf("Before %s\n", exe);
prctl_routine("/bin/sh");
memset(exe, 0, sizeof(exe));
readlink("/proc/self/exe", exe, 255);
printf("After %s\n", exe);
return 0;
}
以上代码尝试动态修改/proc/self/exe,由于这里不再使用execve,因此可以使用readlink读取exe链接内容进行前后对比。直接编译执行会报错:
Before /root/a
prctl: Device or resource busy
After /root/a
我们对a进行UPX加壳,UPX壳在解壳过程中会对映射的可执行ELF内存进行unmap
gcc a.c -o a -static
upx a -o a_upx
之后我们执行a_upx,发现/proc/self/exe成功修改为/bin/dash
Before /root/a_upx
After /bin/dash
其他方式
待探索