亚洲日本免费-啊轻点灬太粗太长了三男一女-麻豆av电影在线观看-日韩一级片毛片|www.grbbt.com

Linux 進(jìn)程感染:Part 1

前言

在紅隊(duì)需要執(zhí)行的各種任務(wù)中有一項(xiàng)因其使用的技術(shù)而引人注目:在系統(tǒng)中植入APT(高級(jí)持續(xù)性威脅),并確保它的持久性。不幸的是,這種持久性機(jī)制大多依賴于通過(guò)一種或多種激活技術(shù)(例如shell腳本、別名、鏈接、系統(tǒng)啟動(dòng)腳本等)在不同位置保存可執(zhí)行文件的副本,因此藍(lán)隊(duì)只需找到這些副本就能進(jìn)行分析。

雖然安全人員遲早會(huì)發(fā)現(xiàn)到底發(fā)生了什么,但可以通過(guò)一些技術(shù)使得在受感染的計(jì)算機(jī)中難以(或者至少延遲)檢測(cè)APT。在本文中,我們將詳細(xì)介紹一種基于進(jìn)程樹而不是常規(guī)的基于文件系統(tǒng)存儲(chǔ)的持久性機(jī)制。

前提條件

這種技術(shù)應(yīng)用于x86-64 GNU/Linux,盡管理論上可以很容易地?cái)U(kuò)展到任何具有較為完整的調(diào)試API的操作系統(tǒng)。最起碼的要求是:任何現(xiàn)代GCC版本都能進(jìn)行這項(xiàng)工作。

使用其他進(jìn)程的地址空間作為倉(cāng)庫(kù)

這種技術(shù)的思想是將正在運(yùn)行的非特權(quán)進(jìn)程的地址空間作為存儲(chǔ)區(qū)域,方法是在其中注入兩個(gè)線程:第一個(gè)線程將試圖感染其他進(jìn)程,而另一個(gè)線程將包含攻擊載荷(在本例中,用于確保文件系統(tǒng)持久性)。如果文件被刪除,它將通過(guò)別名還原。

這種技術(shù)受到機(jī)器正常運(yùn)行時(shí)間的嚴(yán)格限制,因此它應(yīng)該用于不會(huì)頻繁重啟的系統(tǒng)。在其他系統(tǒng)中,它可以被當(dāng)作一種補(bǔ)充的持久性機(jī)制。

注入

顯然最關(guān)鍵一步之一就是代碼注入本身。由于不可能事先知道代碼在受害者地址空間中的地址,所以代碼應(yīng)該是PIC(position-independent code,與位置無(wú)關(guān)的代碼)。這顯然表明需要借助動(dòng)態(tài)庫(kù),因?yàn)樗鼈冊(cè)趯?shí)際應(yīng)用時(shí)會(huì)按照預(yù)期出現(xiàn)在內(nèi)存中。但存在一些缺點(diǎn):

  • 注入的大部分信息將是元數(shù)據(jù)
  • 解析和加載庫(kù)所需的代碼,雖然不是過(guò)于復(fù)雜,但與攻擊載荷的大小相比,也是不可忽略的。
  • 共享庫(kù)使用常見文件格式,導(dǎo)致生成的文件易于分析。

理想情況下,注入應(yīng)該盡可能小:幾個(gè)代碼頁(yè),或者再多一個(gè)用于數(shù)據(jù)。而這其中還可能包含鏈接腳本。不論如何,為了證明這個(gè)概念,我們將實(shí)現(xiàn)一個(gè)共享庫(kù)。

另一個(gè)需要記住的限制是,目標(biāo)進(jìn)程不需要作為動(dòng)態(tài)可執(zhí)行文件加載(因此,C庫(kù)可能不需要?jiǎng)討B(tài)加載)。另外,在加載的共享庫(kù)上手工解析符號(hào)是很麻煩的,因?yàn)橐蕾囉贏BI,而且?guī)缀鯚o(wú)法維護(hù)。這意味著需要手工重新實(shí)現(xiàn)許多標(biāo)準(zhǔn)C函數(shù)。

另外,注入需要依賴ptrace系統(tǒng)調(diào)用。如果進(jìn)程沒有足夠的權(quán)限(或者管理員禁用了這個(gè)功能),就無(wú)法使用這種技術(shù)。

最后還會(huì)遇到動(dòng)態(tài)內(nèi)存使用限制的問(wèn)題。動(dòng)態(tài)內(nèi)存的使用涉及處理堆,而堆的內(nèi)部結(jié)構(gòu)沒有標(biāo)準(zhǔn)。通常不會(huì)在程序的地址空間中保持較大的內(nèi)存占用,應(yīng)該盡可能少地使用動(dòng)態(tài)內(nèi)存來(lái)減少內(nèi)存占用。

概念證明

概念證明如下:

  • 這個(gè)庫(kù)將包含兩個(gè)入口點(diǎn)。入口點(diǎn)的位置可以事先知道(因?yàn)樗鼈兾挥趶目蓤?zhí)行文件開始的固定距離),并且對(duì)應(yīng)于注入線程主函數(shù)的開始處。
  • 注入線程將列出系統(tǒng)中所有正在運(yùn)行的進(jìn)程,查找可能受攻擊的進(jìn)程。
  • 將嘗試對(duì)每個(gè)進(jìn)程進(jìn)行ptrace(PTRACE_SEIZE),并讀取內(nèi)存,以便檢測(cè)是否已被感染。
  • 為了準(zhǔn)備目標(biāo)地址空間,必須注入系統(tǒng)調(diào)用。這些系統(tǒng)調(diào)用必須分配必要的內(nèi)存頁(yè)來(lái)存儲(chǔ)注入的代碼。
  • 生成兩個(gè)線程并繼續(xù)執(zhí)行調(diào)試的進(jìn)程。

每一個(gè)階段都需要進(jìn)行一些仔細(xì)的準(zhǔn)備,下面將詳細(xì)介紹。

準(zhǔn)備環(huán)境

為了讓代碼盡可能簡(jiǎn)潔,使用一個(gè)編譯為共享庫(kù)的小型C程序作為入口點(diǎn)。此外,為了在使用程序前進(jìn)行測(cè)試,將提供另一個(gè)在庫(kù)中運(yùn)行特定符號(hào)的小型C程序。為了簡(jiǎn)化開發(fā),還將包括一個(gè)包含所有構(gòu)建規(guī)則的Makefile。

對(duì)于可注入庫(kù)的入口點(diǎn),將使用以下模板:

void
persist(void)
{
  /* Implement me */
}
void
propagate(void)
{
  /* Implement me */
}

執(zhí)行入口點(diǎn)初始執(zhí)行的程序?qū)⒚麨椤皊pawn.c”,如下所示:

#include 
#include 
#include 
int
main(int argc, char *argv[])
{
  void *handle;
  void (*entry)(void);
  if (argc != 3) {
    fprintf(stderr, "Usagen%s file symboln", argv[0]);
    exit(EXIT_FAILURE);
  }
  if ((handle = dlopen(argv[1], RTLD_NOW)) == NULL) {
    fprintf(stderr, "%s: failed to load %s: %sn", argv[0], argv[1], dlerror());
    exit(EXIT_FAILURE);
  }
  if ((entry = dlsym(handle, argv[2])) == NULL) {
    fprintf(stderr, "%s: symbol `%s' not found in %sn", argv[0], argv[2], argv[1]);
    exit(EXIT_FAILURE);
  }
  printf("Symbol `%s' found in %p. Jumping to function...n", argv[2], entry);
  (entry) ();
  printf("Function returned!n");
  dlclose(handle);
  return 0;
}

最后,編譯這兩個(gè)程序的Makefile,如下所示:

CC=gcc
INF_CFLAGS=--shared -fPIE -fPIC -nostdlib
all : injectable.so spawn
injectable.so : injectable.c
        $(CC) $(INF_CFLAGS) injectable.c -o injectable.so
spawn : spawn.c
        $(CC) spawn.c -o spawn -ldl

運(yùn)行make命令編譯所有內(nèi)容:

% make
(…)
% ./spawn injectable.so propagate
Symbol `propagate' found in 0x7ffff76352ea. Jumping to function...
Function returned!

系統(tǒng)調(diào)用

對(duì)于上面的Makefile,需要注意的是,injectable.so是通過(guò)-nostdlib編譯的(這是必需的),因此我們將不能訪問(wèn)高級(jí)C系統(tǒng)調(diào)用接口。為了突破這一限制,需要混合使用C和內(nèi)聯(lián)匯編,以便與操作系統(tǒng)進(jìn)行交互。

通常情況下,x86-64 Linux系統(tǒng)調(diào)用是通過(guò)syscall指令執(zhí)行的(而在較早的x86系統(tǒng)中,則使用0x80中斷)。在任何情況下,基本思想都是一樣的:寄存器使用系統(tǒng)調(diào)用參數(shù)填充,然后通過(guò)一些特殊指令調(diào)用系統(tǒng)。%rax的內(nèi)容由系統(tǒng)調(diào)用函數(shù)代碼初始化,其參數(shù)按%rdi、%rsi、%rdx、%r10、%r8和%r9的順序傳遞。返回值存儲(chǔ)在%rax中,錯(cuò)誤用負(fù)返回值表示。因此,在匯編中使用write()系統(tǒng)調(diào)用的簡(jiǎn)單“hello world”如下所示:

    movq $1, %rax           // Syscall code for write(): 1
    movq $1, %rdi           // Arg 1: File descriptor (stdout)
    leaq %rip(saludo), %rsi // Arg 2: Buffer address
    movq $11, %rdx          // Arg 3: size (11 bytes)
    syscall                 // All set, call the kernel
[…]
saludo: .ascii “Hola mundon”

得益于GCC的內(nèi)聯(lián)匯編語(yǔ)法,在C中使用匯編語(yǔ)言是相當(dāng)容易的,而且由于它的簡(jiǎn)潔性,它可以被簡(jiǎn)化成一句代碼。GCC的write wrapper可以簡(jiǎn)化為:

#include 
#include 
ssize_t
write(int fd, const void *buffer, size_t size)
{
  size_t result;
  asm volatile(“syscall” : “=a” (result) : “a” (__NR_write), “S” (fd), “D” (buffer), ”d” (size);
  return result;
}

在“syscall”之后傳遞的值指定在執(zhí)行匯編代碼之前如何初始化寄存器。在這種情況下,%rax(specifier:“a”)被初始化為__NR_write(擴(kuò)展到系統(tǒng)調(diào)用代碼以進(jìn)行寫入的宏,如syscall.h中定義的那樣)、帶有buffer地址的%rdi(specifier:“D”)、%rsi(specifier:“S”)和包含字符串大小的%rsi(specifier:“S”)。返回值被收集回%rax(specifier:“=a”,等號(hào)表示“結(jié)果”是一個(gè)只寫的值,編譯器不需要擔(dān)心它的初始值)。

由于字符串解析在許多程序中很常見,而且通常都需要這一步,編寫strlen的實(shí)現(xiàn)(按照string.h中的原型)來(lái)度量字符串長(zhǎng)度是很方便的:

size_t
strlen(const char *buffer)
{
  size_t len = 0;
  while (*buffer++)
    ++len;
  return len;
}

它允許定義以下宏:

#define puts(string) write(1, string, strlen(string))

它提供了一種在標(biāo)準(zhǔn)輸出中顯示調(diào)試消息的簡(jiǎn)單方法:

void
persist(void)
{
  puts("This is persist()n");
}
void
propagate(void)
{
  puts("This is propagate()n");
}

運(yùn)行后應(yīng)該產(chǎn)生以下輸出:


% ./spawn injectable.so persist
Symbol `persist' found in 0x7f3eb58403be. Jumping to function...
This is persist()
Function returned!
% ./spawn injectable.so propagate
Symbol `propagate' found in 0x7fb8874403db. Jumping to function...
This is propagate()
Function returned!

第一個(gè)困難解決,從現(xiàn)在開始,對(duì)于任何缺少的系統(tǒng)調(diào)用功能,都應(yīng)該實(shí)現(xiàn)相應(yīng)的C wrapper,所需的庫(kù)函數(shù)(如strlen)應(yīng)該按照我們需要的相應(yīng)的標(biāo)準(zhǔn)頭原型來(lái)實(shí)現(xiàn)。

枚舉過(guò)程

為了在其他進(jìn)程中注入惡意代碼,第一步是了解系統(tǒng)中可用的進(jìn)程。有兩種方法可以做到這一點(diǎn):

  • 訪問(wèn)/proc并列出所有文件夾,或者
  • 檢測(cè)所有系統(tǒng)PID,從PID 2到給定的PID_MAX

雖然第一種方法看起來(lái)是最快的,但它也是最復(fù)雜的,因?yàn)椋?/p>

  • /proc可能沒有安裝。
  • Linux缺少處理文件夾的opendir/readdir系統(tǒng)調(diào)用。它實(shí)際上依賴于getdents,它返回一個(gè)需要手動(dòng)處理的可變大小結(jié)構(gòu)的buffer。
  • 文件名必須手動(dòng)轉(zhuǎn)換為整數(shù),以便提取它們所引用的PID。因?yàn)槲覀儫o(wú)法訪問(wèn)庫(kù)函數(shù),所以這種轉(zhuǎn)換特性也應(yīng)該手動(dòng)實(shí)現(xiàn)。

雖然第二種方法看起來(lái)比較慢,但在現(xiàn)代操作系統(tǒng)中幾乎都能正常工作。在這種方法中,在PID范圍內(nèi)通過(guò)信號(hào)0多次調(diào)用Kill,如果PID存在且調(diào)用進(jìn)程可以向其發(fā)送信號(hào)(這反過(guò)來(lái)與調(diào)用進(jìn)程的權(quán)限有關(guān)),則返回0,否則將返回錯(cuò)誤代碼。

現(xiàn)在唯一未知的是PID_MAX,它在每個(gè)系統(tǒng)不一定都是相同的。幸運(yùn)的是,在絕大多數(shù)情況下,PID_MAX被設(shè)置為默認(rèn)值(32768)。由于在沒有發(fā)送信號(hào)的情況下,kill是非常快的,所以調(diào)用kill 33000次似乎是可行的。

使用這種技術(shù),需要一個(gè)用于kill的wrapper。遍歷2到32768之間的所有可能的PID(因?yàn)镻ID 1是為init保留的),并為找到的每個(gè)進(jìn)程打印一條消息:

int
kill(pid_t pid, int sig)
{
  int result;
  asm volatile("syscall" : "=a" (result) : "a" (__NR_kill), "D" (pid), "S" (sig));
  return result;
}

編寫一個(gè)函數(shù),打印十進(jìn)制數(shù)字:

void
puti(unsigned int num)
{
  unsigned int max = 1000000000;
  char c;
  unsigned int msd_found = 0;

  while (max > 0) {
    c = '0' + num / max;
    msd_found |= c != '0' || max == 1;
    if (msd_found)
      write(1, &c, 1);
    num %= max;
    max /= 10;
  }
}

現(xiàn)在剩下的工作是修改propagate(),用來(lái)進(jìn)行枚舉:

void
propagate(void)
{
  pid_t pid;

  for (pid = 2; pid < PID_MAX; ++pid) if (kill(pid, 0) >= 0) {
      puts("Process found: ");
      puti(pid);
      puts("n");
    }
}

編譯后,預(yù)期得到這樣的結(jié)果:

% ./spawn injectable.so propagate
Process found: 1159
Process found: 1160
Process found: 1166
Process found: 1167
Process found: 1176
Process found: 1324
Process found: 1328
Process found: 1352

對(duì)于常規(guī)的桌面GNU/Linux發(fā)行版來(lái)說(shuō),通常會(huì)發(fā)現(xiàn)超過(guò)100個(gè)用戶進(jìn)程。這相當(dāng)于說(shuō)有一百多個(gè)可能的感染目標(biāo)。

嘗試PTRACE_SEIZE

這種技術(shù)的主要缺點(diǎn):由于訪問(wèn)限制(例如setuid進(jìn)程),無(wú)法對(duì)上面列舉的一些進(jìn)程進(jìn)行調(diào)試。對(duì)每個(gè)已發(fā)現(xiàn)進(jìn)程的ptrace調(diào)用(PTRACE_SEIZE)都可以用于標(biāo)識(shí)哪些進(jìn)程是可調(diào)試的。

雖然對(duì)于調(diào)試運(yùn)行中的程序,首先想到的是使用PTRACE_ATTACH,但是這種技術(shù)有副作用:如果成功,它將停止調(diào)試,直到使用PTRACE_CONT恢復(fù)調(diào)試為止。這可能會(huì)影響目標(biāo)進(jìn)程(特別是當(dāng)它對(duì)時(shí)間敏感時(shí)),從而被用戶發(fā)現(xiàn)。但是PTRACE_SEIZE(在Linux3.4中引入)并不會(huì)停止目標(biāo)進(jìn)程。

根據(jù)libc,ptrace是一個(gè)可變的函數(shù),因此通過(guò)始終接受4個(gè)參數(shù)、填充參數(shù)或不根據(jù)請(qǐng)求的命令填充參數(shù),可以很方便地簡(jiǎn)化wrapper:

long
ptrace4(int request, pid_t pid, void *addr, void *data)
{
  long result;
  register void* r10 asm("r10") = data;
  asm volatile("syscall" : "=a" (result) : "a" (__NR_ptrace), "S" (pid), "D" (request), "d" (addr));
  return result;
}

現(xiàn)在propagate函數(shù)如下:

void
propagate(void)
{
  pid_t pid;
  int err;

  for (pid = 2; pid < PID_MAX; ++pid) if (kill(pid, 0) >= 0) {
      puts("Process found: ");
      puti(pid);
      puts(": ");
      if ((err = ptrace4(PTRACE_SEIZE, pid, NULL, NULL)) >= 0) {
        puts("seizable!n");
        ptrace4(PTRACE_DETACH, pid, NULL, NULL);
      } else {
        puts("but cannot be debugged <img draggable="false" class="emoji" alt="??" src="https://s.w.org/images/core/emoji/11/svg/1f641.svg"> [errno=");
        puti(-err);
        puts("]n");
      }
    }
}

它將列出系統(tǒng)上所有可調(diào)試的進(jìn)程。

結(jié)論

前面的測(cè)試讓我們快速地了解了這種技術(shù)的可行性。到這一步,已經(jīng)接近普通的調(diào)試器了,最大區(qū)別是我們的代碼是自動(dòng)運(yùn)行的。在下一篇文章中,我們將介紹如何捕獲調(diào)試器的系統(tǒng)來(lái)遠(yuǎn)程注入系統(tǒng)調(diào)用。這些遠(yuǎn)程系統(tǒng)調(diào)用將用于創(chuàng)建生成注入線程的代碼和數(shù)據(jù)頁(yè)。

原文地址:https://www.tarlogic.com/en/blog/linux-process-infection-part-i/

上一篇:Linux系統(tǒng)內(nèi)存執(zhí)行ELF的多種方式

下一篇:針對(duì) Windows 事件跟蹤日志篡改的攻防研究