Linux SO 文件注入。






4.94/5 (15投票s)
将库文件注入到正在运行的进程中。
引言
本文旨在介绍 Linux 中对进程进行实时补丁。读者将能够将 SO 文件注入到 Linux 上运行的远程进程中(x86-64 位进程,在 Ubuntu 16.04, 4.4.0-22-generic 上测试过),前提是拥有必要的访问权限。我们将重新审视 Linux 平台的调试,并逐步了解 Linux 版本的 ASLR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)。
当你没有源代码但希望进程强制加载你的 SO 文件时,这是最佳选择。在 SO 文件中的全局对象的构造函数里,你可以执行各种操作,包括 API 挂钩。
背景
我建议用户参考我之前的文章:https://codeproject.org.cn/Articles/1073879/Write-Your-Own-Linux-Debugger。对于 Windows 开发者,我们可以使用 `CreateRemoteThread` API,如 https://codeproject.org.cn/Articles/535677/Memory-Analyzer-x-bit-a-Free-Detour 中所述。
不幸的是,对于 Linux 开发者来说,我们没有直接的方法来做到这一点,这就是本文的意义所在 :-) 。因为我们实际上是在编写某种调试器(使用 `ptrace`),所以你也可以使用 `gdb` 来实现这一点。
用户将需要 Qtcreator:`sudo apt-get install -y qtcreator qt5-default`
Using the Code
与我之前的所有文章一样,代码必须随时参考。
我们首先生成需要注入 SO 文件的目标进程(如附带示例所示)。我们也可以提供 `PID`。`Fork` 总是能奏效。
switch(rpid = fork()) //spqwn a process
{
case 0://child process
{
int y=execlp("../build-QTUI_App-Desktop-Debug/QTUI_App",0);
//int y=execlp("../build-Test-Desktop-Debug/Test",0);
break;
}
case -1:
printf("error spawning process\n");exit(-1);
break;
//parent continues execution
}
下一步是附加你的进程到这个目标进程,你的进程负责操作这个目标进程以加载所需的 SO 文件。
`ptrace` 是操作系统提供的最关键的 API,用于辅助调试。
int status=0;
ptrace(PTRACE_ATTACH,rpid,NULL);printf("error %u\n",errno); //lets attach the process
pid_t tid=wait(&status);
ptrace(PTRACE_SETOPTIONS, tid, NULL, PTRACE_O_TRACEFORK |
PTRACE_O_TRACEVFORK | PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXIT);
现在到了最精彩的部分……
我们必须通过注入指令来操作目标进程,以欺骗它加载我们的模块(*lib.so*)。**此处使用的指令仅适用于 x86(64 位)**,我**可能会**移植到 x86 32 位。
当调试器附加到进程时,它会向被调试进程发送 `SIGSTOP`。发生这种情况时,我们就忙起来了。我们希望调试器以 `SIGTRAP` 停止,这样跟踪目标进程就会**永久**停止,我们就可以查询寄存器状态,这就是为什么我们需要添加额外代码。
ptrace(PTRACE_SINGLESTEP, tid, 0, 0);
tid=waitpid(-1, &status, __WALL);
这将导致处理器执行下一条指令,然后触发 `SIGTRAP`。这就是调试器在反汇编调试时用于单步执行每条指令的操作。
对于 x86,调用 `ptrace`(`PTRACE_SINGLESTEP`...)会设置跟踪标志(https://en.wikipedia.org/wiki/FLAGS_register)。当跟踪标志被设置时,每次执行指令都会触发调试异常。当调用 ISR 时,由于显而易见的原因,它不会单步执行。
在更改进程状态之前,我们必须保存其状态。
API `process_vm_readv(rpid,&Originaliovec,1,&remote_iov,1,0)`** 用于复制目标进程内存,以便稍后恢复。我们还必须保存寄存器状态,*ptrace 来帮忙*。
ptrace(PTRACE_GETREGS,tid,NULL,&uregs);
既然我们已经有了足够的信息来将进程恢复到原始状态,我们就可以更改目标进程地址空间中的值了。
WriteProcessMemory 用于此目的。
我们将所需的参数移到 **RSI** 和 **RDI**,然后调用 `dlopen`。所有这些都是通过将所需的指令注入到目标进程的地址空间来完成的,该函数使用编号(1)、(2)、... 进行了很好的注释。
`ptrace(PTRACE_POKETEXT...)` 用于写入目标进程的内存。
void WriteProcessMemory(const unsigned int rpid,user uregs={})
{
char *str = libName;
memcpy(data_opcodes, str,strlen(str)+1); //copied the name of the so
unsigned char MovRaxtoRDI[] = { 0x48, 0x8B, 0xf8 }; //these are the opcodes for move RAX=>RDI
unsigned char Mov1toRBX[] = { 0x48, 0xc7, 0xc3, 01, 0, 0, 0 }; //move 1=RBX
unsigned char MovRBXtoRSI[] = { 0x48, 0x8B, 0xF3 }; //move RBX=>RSI
unsigned char CallRax[] = {0xff, 0xd0, 0xcc }; //Call RAX and then break (int 3)
//compine all the opcodes
unsigned char opcodes[50];
//copy the address of the lib file to RAX, we are placing the lib.so file
//after all the opcodes (including the breakpoint)
//so the flow is (intel assembly format):
/*
* mov rax,address of the so file (1)
* mov rdi,rax (2)
* mov rbx,1 (3)
* mov rsi,rbx (4)
* mov rax,function address of dlopen (5)
* call RAX (6)
* breakpoint
* .
* .
* /
* /
* l
* i
* b
* .
* s
* o
*/
/*(1)*/unsigned char MovtoRax[2 + 8] = { 0x48, 0xb8 };
void *p = uregs.regs.rip+sizeof(MovtoRax) + sizeof(MovRaxtoRDI) +
sizeof(Mov1toRBX) + sizeof(MovRBXtoRSI) + sizeof(MovtoRax) + sizeof(CallRax);
memcpy(&MovtoRax[2], &p, 8);
memcpy(opcodes, MovtoRax, sizeof(MovtoRax)); //move first paramter to RAX-> then to RDI
/*(2)*/memcpy(opcodes + sizeof(MovtoRax), MovRaxtoRDI, sizeof(MovRaxtoRDI));
/*(3)*/memcpy(opcodes + sizeof(MovtoRax) + sizeof(MovRaxtoRDI),
Mov1toRBX, sizeof(Mov1toRBX)); //move second parameter to RBX -> then to RSI
/*(4)*/memcpy(opcodes + sizeof(MovtoRax) + sizeof(MovRaxtoRDI) +
sizeof(Mov1toRBX), MovRBXtoRSI, sizeof(MovRBXtoRSI));
/*(5)*/
p = FindFuncAddr("libdl",dlopen,rpid); //find out where libdl is loaded in the remote process,
//this is randomly loaded for every process (thanks to ASLR)
memcpy(&MovtoRax[2], &p, 8); //move function address to RAX->call RAX (in this case Sleep)
memcpy(opcodes + sizeof(MovtoRax) + sizeof(MovRaxtoRDI) + sizeof(Mov1toRBX) +
sizeof(MovRBXtoRSI), MovtoRax, sizeof(MovtoRax));
/*(6)*/memcpy(opcodes + sizeof(MovtoRax) + sizeof(MovRaxtoRDI) +
sizeof(Mov1toRBX) + sizeof(MovRBXtoRSI) + sizeof(MovtoRax), CallRax, sizeof(CallRax));
memcpy(data_opcodes, opcodes, sizeof(MovtoRax) + sizeof(MovRaxtoRDI) +
sizeof(Mov1toRBX) + sizeof(MovRBXtoRSI) + sizeof(MovtoRax) + sizeof(CallRax));
memcpy(data_opcodes+sizeof(MovtoRax) + sizeof(MovRaxtoRDI) +
sizeof(Mov1toRBX) + sizeof(MovRBXtoRSI) + sizeof(MovtoRax) + sizeof(CallRax),
str,strlen(str)+1);
//now write these opcodes to the remote process.
for(int i=0;i<sizeof(data_opcodes);++i){
ptrace(PTRACE_POKETEXT,rpid,uregs.regs.rip+i,data_opcodes[i]);
}
}
如前所述,要调用 `dlopen`,我们需要知道它在目标进程中的地址。这可以通过参考文件 `/proc/
下面的函数将找出 `libdl` 在哪里加载。
void *FindSoAddress(const char *strLibName,pid_t pid)
下面的函数将找出 `dlopen` 在目标进程中的函数地址。
void *FindFuncAddr(const char *strLibName,const void *pLocalFuncAddr,pid_t pid)
现在加载 SO 文件的指令已经到位(以及在 **call RAX** 之后的期望断点),让目标进程继续执行。
ptrace(PTRACE_CONT, tid, NULL,0);
这将导致目标进程加载 SO 文件然后停止(使用 `int 3: **0xcc**`),请参考函数 `WriteProcessMemory` 中的 `unsigned char CallRax[] = {0xff, 0xd0, **0xcc** }; //Call RAX`*** 然后停止(int 3)***。
完成这些之后,开始恢复并退出你的进程。
if(siginfo.si_signo==5 && bFirst)
{
bFirst=false;
RestoreMemory(rpid,originalRegs);
ptrace(PTRACE_DETACH,tid,0,0);
exit(0);//your work is done
}
`RestoreMemory` 将利用 `ptrace(PTRACE_POKETEXT...)` 写回目标进程,并使用 `ptrace(PTRACE_SETREGS,rpid,NULL,&originalRegs)` 设置原始寄存器上下文,然后分离并退出。
别忘了构建 *lib.so* 文件:`**gcc lib.cpp -shared -fpic -o lib.so**`
关注点
有了这些,读者现在就可以在 Linux 系统上实现 `CreateRemoteThread`,用于远程进程的 API 挂钩。
历史
- 2016 年 6 月 4 日:初始版本