可能在某些情況下,自己運行的程式不想或者不方便被其他人看到,就需要隱藏運行的進程。或者某些攻擊者採用了本文介紹的隱藏技術,也可以讓大家看到如何進行對抗。 隱藏有兩種方法: 1. kernel 層面,不對用戶層暴露該進程的信息,進程不被看見; 2. 用戶層可以看到該進程信息,但不是以真實的身份出現,而 ...
可能在某些情況下,自己運行的程式不想或者不方便被其他人看到,就需要隱藏運行的進程。或者某些攻擊者採用了本文介紹的隱藏技術,也可以讓大家看到如何進行對抗。
隱藏有兩種方法:
- kernel 層面,不對用戶層暴露該進程的信息,進程不被看見;
- 用戶層可以看到該進程信息,但不是以真實的身份出現,而是偽造成別的程式出現,達到隱藏自己的目的。
方法一是需要修改內核,本文主要是講第二種方法。
常用獲取進程信息工具
在 Linux 環境下,主要是通過 top、ps 等工具來查看當前運行的進程,他們主要是通過讀取 /proc/ 文件系統來獲取進程信息,讀取哪些內容,我們可以通過 strace (該工具用於監控系統調用和信號) 工具查看,我們以 296277 進程為例。
top 獲取進程信息
296277 進程信息
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
296277 wangshu+ 20 0 2272 1380 1300 S 0.0 0.0 0:00.00 bash
top 查看到 296277 進程的信息,我們一般主要關心的是最後一列,進程名。
strace 跟蹤結果
stat("/proc/296277", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/296277/stat", O_RDONLY) = 9
read(9, "296277 (bash) S 296271 296271 23"..., 1024) = 322
close(9) = 0
openat(AT_FDCWD, "/proc/296277/statm", O_RDONLY) = 9
read(9, "568 355 335 2 0 78 0\n", 1024) = 21
close(9)
從上面可以看到,主要是讀取了 /proc/296277/stat 和 /proc/296277/statm 文件。
- /proc/296277/stat 內容
296277 (bash) S 296271 296271 230491 34840 296271 4194304 94 155 0 0 0 0 0 0 20 0 1 0 94413592 2326528 345 18446744073709551615 94055941185536 94055941191257 140723269084624 0 0 0 0 4096 0 0 0 0 17 11 0 0 0 0 0 94055941201384 94055941202080 94055956852736 140723269087783 140723269087820 140723269087820 140723269091283 0
- /proc/296277/statm 內容
568 345 325 2 0 78 0
可以看到 top 中的進程名是使用 /proc/296277/stat 中的第二項 (bash)。
ps -aux 獲取進程信息
296277 進程信息
wangshu+ 296277 0.0 0.0 2272 1380 pts/24 S+ 18:59 0:00 /bin/bash
ps -aux 查看到 296277 進程的信息,最後一列是 bash 的完整路徑名。
strace 跟蹤結果
stat("/proc/296277", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/296277/stat", O_RDONLY) = 6
read(6, "296277 (bash) S 296271 296271 23"..., 2048) = 322
close(6) = 0
openat(AT_FDCWD, "/proc/296277/status", O_RDONLY) = 6
read(6, "Name:\tbash\nUmask:\t0022\nState:\tS "..., 2048) = 1095
close(6) = 0
openat(AT_FDCWD, "/proc/296277/cmdline", O_RDONLY) = 6
read(6, "/bin/bash\0", 131072) = 10
read(6, "", 131035) = 0
close(6) = 0
stat("/dev/pts24", 0x7ffe4ce31630) = -1 ENOENT (No such file or directory)
stat("/dev/pts/24", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x18), ...}) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=545, ...}) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=545, ...}) = 0
write(1, "wangshu+ 296277 0.0 0.0 227"..., 104) = 104
從上面可以看到,主要是讀取了 /proc/296277/stat、/proc/296277/status 和 /proc/296277/cmdline。
- /proc/296277/cmdline 內容
/bin/bash
ps -aux 中進程名可以看到是從 /proc/296277/cmdline 讀取到的。
上面列出來proc信息,可以從proc文件內容介紹獲取更詳細的介紹。
小結
從上面實驗中可以看到,進程名主要是通過 /proc/進程號/stat 和 /proc/進程號/cmdline 文件中獲取。
隱藏技術實現
/proc/進程號/stat 和 /proc/進程號/cmdline 這些介面都是內核實現的,通過閱讀內核代碼得到:
- /proc/進程號/stat 中的進程名對應的可執行文件的文件名;
- /proc/進程號/cmdline 內容對應的是啟動該進程的使用的路徑,對應的是入口函數 main(int argc, char *argv[]) 中的 argv[0]。
為了實現隱藏,我們將偽造為 bash 進程, bash 進程比較常見,並且一般不會被過多關註。所以需要進行的操作有:
- 文件名修改為 bash;
- 運行時將 argv[0] 修改成 /bin/bash;
- 為了防止從 /proc/進程號/maps 中發現沒有包含 /bin/bash,我們將 /bin/bash 文件 mmap 進我們的進程中。
{:.warning}
2中 Android 環境中不能直接拿到argv[0] 怎麼辦? 在 Unix 環境下,有定義全局變數 __progname_full, 該變數指向了 argv[0],可以直接使用 __progname_full 進行修改,當然在 Linux 下同樣適用。
代碼示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
extern const char *__progname_full; // 引入該變數
int main(int argc, char **argv)
{
int pid = getpid();
printf("current pid: %d\n", pid);
char buffer[100] = {0};
char cmdline[100] = {0};
sprintf(cmdline, "cat /proc/%d/cmdline", pid);
system(cmdline);
printf("\n");
scanf("%s", &buffer);
printf("%d change the name: %s\n", getpid(), buffer);
//int size = strlen(__progname_full); // 使用 __progname_full 進行修改 cmdline
//printf("name len: %d\n", size);
//memset((char *)__progname_full, 0, size);
//strcpy((char *)__progname_full, buffer);
int size = strlen(argv[0]); // 使用 argv[0] 進行修改 cmdline
printf("name len: %d\n", size);
memset((char *)argv[0], 0, size);
strcpy((char *)argv[0], buffer);
int fd = open(buffer, O_RDONLY); // 打開偽造後的可執行文件, mmap 到我們進程中
if (fd < 0) {
printf("not found the file:%s\n", buffer);
return -1;
}
struct stat st;
fstat(fd, &st);
void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); // 可讀 map
if (addr == MAP_FAILED) {
printf("mmap failed\n");
}
void *addr1 = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); // 讀寫 map
if (addr1 == MAP_FAILED) {
printf("mmap1 failed\n");
}
void *addr2 = mmap(NULL, st.st_size, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0); // 讀可執行 map
if (addr2 == MAP_FAILED) {
printf("mmap2 failed\n");
}
system(cmdline);
printf("\n");
scanf("%s", &buffer);
munmap(addr, st.st_size);
munmap(addr1, st.st_size);
munmap(addr2, st.st_size);
close(fd);
return 0;
}
執行結果
current pid: 301929 # 當前進程
/home/f/doing/linux/change_name/bash # 偽造前 cmdline
/bin/bash # 要偽造的可執行文件
301929 change the name: /bin/bash
name len: 36
/bin/bash # 偽造後的 cmdline
工具查看結果
ps -aux 查看
top 查看
可以看到都已經是 bash。
技術對抗
上述隱藏方式可以騙過 ps 和 top 工具,但是只是修改了名字,但可執行文件等信息沒有改變,所以從其他信息可以推斷出是進程名是偽造的,下麵闡述幾種方式。
/proc/進程名/exe
在 /proc/進程名/exe 中保存的真正執行的可執行文件,所以可以將該文件和偽造後的 /bin/bash 文件對比,若發現不同可以證明進程名是偽造的,但此時還不能確認真正的可執行文件,只能和文件系統中的其他可執行文件對比進行確認。
/proc/進程名/maps
Linux 中記憶體的佈局是有一定的規律的,如下圖,地址從低到高,分別是ELF可執行文件、Data和Bss區域、HEAP、memory map區域、stack等。我們可以根據這些信息來確認真正的可執行文件。
來看下偽造進程的記憶體佈局:
從上面可以看出來,紅色框是ELF可執行文件、Data和Bss區域,其實就是進程的真正可執行文件,接著是 HEAP,後面是我們 mmap 偽造的 bash 文件,所以通過 /proc進程名/maps 可以確認出真正的可執行文件為 /home/f/doing/linux/change_name/bash。
小結
上面中 /proc/進程名/exe 能簡單判斷出進程名是否偽造,但不能確認真正的可執行文件,而通過 /proc/進程名/maps 可以非常直觀的看到可執行文件的信息。除了這兩種方式外,可能還有其他方法,比如 /proc/進程名/status 中 VmExe 欄位,該欄位是記錄 text 段大小,和顯示的可執行文件進行對比,確認是否有偽造。
總結
雖然原理上比較簡單,但是提醒我們有這麼一種方式,可以簡單的騙過我們通過ps和top看到的信息。若發現系統中有異常,也可以通過技術對抗中提到的方式,確認是否有惡意程式存在。
PS
關註公眾號,瞭解更多信息!