linux运行进程热补丁(一)之函数替换_linux 热补丁的实现-CSDN博客
一、实现目标
在Linux环境下(x86_64)对正在运行进程的函数替换,不改变该进程的可执行文件内容,通过使用汇编指令JMP完成运行中进程的函数替换。
二、代码示例
1.easy的target进程代码
#cat myprint.c
//编译myprint.c变为可执行文件
#gcc -o myprint myprint.c
#include <stdio.h>
void myprint()
{
printf("the old func\n");
return;
}
void new_myprint()
{
printf("the new func\n");
return;
}
int main(int argc,char *argv[])
{
while(1)
{
myprint();
sleep(1);
}
return 0;
}
执行效果:
2.热补丁代码( jmp)
#cat jmp.c
//编译myprint.c变为可执行文件
#gcc -o jmp jmp.c
#include <unistd.h>
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/mman.h>
#define DEBUG(fmt,...) do {printf(fmt, __VA_ARGS__);} while(0)
#define INFO(fmt,...) do {printf(fmt, __VA_ARGS__);} while(0)
#define NOTICE(fmt,...) do {printf(fmt, __VA_ARGS__);} while(0)
#define ERROR(fmt,...) do {printf(fmt, __VA_ARGS__);} while(0)
int set_data(pid_t pid,int addr,void *val,int vlen)
{
int i;
int addr0 = addr & ~3;
int len = (((addr +vlen) - addr0) +3 )/4;
int *lv = malloc(len * sizeof(int));
DEBUG("peek: %d, addr0 = %x, addr-addr0 = %d",len,addr0,addr-addr0);
for (i = 0; i < len; i++) {
if (i % 4 == 0){
DEBUG("\n %p ",(void *)(addr0 + i * sizeof(int)));
}
lv[i] = ptrace(PTRACE_PEEKDATA, pid, addr0 + i * sizeof(int), NULL);
if (lv[i] == -1 && errno != 0) {
perror("ptrace peeku");
return -1;
}
DEBUG("%08x ",lv[i]);
}
memcpy((char *) lv + (addr - addr0) , val, vlen);
DEBUG("\n poke: %d", len);
for (i = 0; i < len; i++) {
if (i%4 == 0) {
DEBUG("\n %p ",(void *) (addr0 + i * sizeof(int)));
}
if (ptrace(PTRACE_POKEDATA, pid, addr0 + i * sizeof(int) , lv[i]) < 0) {
perror("ptrace peeku");
return -1;
}
DEBUG("%08x ",lv[i]);
}
DEBUG("%s","\n") ; /* XXX */
return 0;
}
//"usage: /jmp "target_pid" "source_addr" "target_addr" "0"
int main(int argc, char*argv[])
{
unsigned int addr;
unsigned int addr2;
int jmp_relative;
char jmpbuf[5];
pid_t target_pid;
int status = -1;
if (argc < 4)
{
printf("usage: /jmp \"target_pid\" \"source_addr\" \"target_addr\" \n") ;
return 0;
}
target_pid = atoi(argv[1]);
addr = atoi(argv[2]); //source
addr2 = atoi(argv[3]);//target
jmp_relative = addr2 - (addr + 5);
printf("addr2=%p addr=%p \n",addr2, addr);
printf("jmp relative %ld (0x%08lx)\n", jmp_relative, jmp_relative) ;
jmpbuf[0] = 0xe9; /* jmp */
memcpy(jmpbuf+1, &jmp_relative, sizeof(int));
status = 1;
printf("jmpbuf = 0x%02x%02x%02x%02x%02x \n",jmpbuf[0], jmpbuf[1] , jmpbuf[2] , jmpbuf[3] , jmpbuf[4]);
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0){
perror("ptrace attach") ;
exit(-2) ;
}
DEBUG("attached %d\n", target_pid) ;
wait(NULL);
if (set_data(target_pid, addr, jmpbuf, sizeof(jmpbuf)) < 0){
DEBUG("E: jmp %p %p failed. \n", (void *)addr, (void *) addr2);
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
DEBUG("detached %d \n",target_pid);
exit(-3);
}
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
DEBUG("detached %d \n",target_pid);
exit(status);
}
JMP注入热补丁后执行效果(函数替换):
二、热补丁代码分析
1.需要确定target进程的pid信息
# ps -ef | grep print
root 10417 30217 0 17:49 pts/0 00:00:00 ./myprint
root 14651 14521 0 18:26 pts/1 00:00:00 grep --color=auto print
2.使用Linux工具objdump,确定 myprint和new_myprint函数地址
实际使用中,新函数往往是动态库,只能通过dlopen、dlsym等函数查询。
# objdump -S myprint
//地址十六进制转十进制
0x40057d <myprint> --> 4195709 (source_addr)
0x40058e <new_myprint> --> 4195726 (target_addr)
# objdump -S myprint
myprint: file format elf64-x86-64
...
000000000040057d <myprint>:
40057d: 55 push %rbp
40057e: 48 89 e5 mov %rsp,%rbp
400581: bf 60 06 40 00 mov $0x400660,%edi
400586: e8 c5 fe ff ff callq 400450 <puts@plt>
40058b: 90 nop
40058c: 5d pop %rbp
40058d: c3 retq
000000000040058e <new_myprint>:
40058e: 55 push %rbp
40058f: 48 89 e5 mov %rsp,%rbp
400592: bf 6d 06 40 00 mov $0x40066d,%edi
400597: e8 b4 fe ff ff callq 400450 <puts@plt>
40059c: 90 nop
40059d: 5d pop %rbp
40059e: c3 retq
000000000040059f <main>:
40059f: 55 push %rbp
4005a0: 48 89 e5 mov %rsp,%rbp
4005a3: 48 83 ec 10 sub $0x10,%rsp
4005a7: 89 7d fc mov %edi,-0x4(%rbp)
4005aa: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4005ae: b8 00 00 00 00 mov $0x0,%eax
4005b3: e8 c5 ff ff ff callq 40057d <myprint>
4005b8: bf 01 00 00 00 mov $0x1,%edi
4005bd: b8 00 00 00 00 mov $0x0,%eax
4005c2: e8 b9 fe ff ff callq 400480 <sleep@plt>
4005c7: eb e5 jmp 4005ae <main+0xf>
4005c9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
... ...
3. gdb attach myprint,查看myprint函数的汇编代码
3.1 myprint进程while循环函数跳转到指令 main+15的位置:
0x00000000004005c7 <+40>: jmp 0x4005ae <main+15>
1
3.2 main+15的位置为执行myprint函数入口:
0x00000000004005ae <+15>: mov $0x0,%eax
0x00000000004005b3 <+20>: callq 0x40057d <myprint>
3.3 myprint地址–>0x40057d (需要将push %rbp变成了jmpq 0x40058e,完成函数替换 )
0x000000000040057d <+0>: push %rbp
3.4 new_myprint地址–>0x40058e
(gdb) disassemble new_myprint
Dump of assembler code for function new_myprint:
0x000000000040058e <+0>: push %rbp
# gdb attach 10417
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
(gdb) disassemble main
Dump of assembler code for function main:
0x000000000040059f <+0>: push %rbp
0x00000000004005a0 <+1>: mov %rsp,%rbp
0x00000000004005a3 <+4>: sub $0x10,%rsp
0x00000000004005a7 <+8>: mov %edi,-0x4(%rbp)
0x00000000004005aa <+11>: mov %rsi,-0x10(%rbp)
0x00000000004005ae <+15>: mov $0x0,%eax
0x00000000004005b3 <+20>: callq 0x40057d <myprint>
0x00000000004005b8 <+25>: mov $0x1,%edi
0x00000000004005bd <+30>: mov $0x0,%eax
0x00000000004005c2 <+35>: callq 0x400480 <sleep@plt>
0x00000000004005c7 <+40>: jmp 0x4005ae <main+15>
End of assembler dump.
(gdb) disassemble myprint
Dump of assembler code for function myprint:
0x000000000040057d <+0>: push %rbp
0x000000000040057e <+1>: mov %rsp,%rbp
0x0000000000400581 <+4>: mov $0x400660,%edi
0x0000000000400586 <+9>: callq 0x400450 <puts@plt>
0x000000000040058b <+14>: nop
0x000000000040058c <+15>: pop %rbp
0x000000000040058d <+16>: retq
End of assembler dump.
(gdb) disassemble new_myprint
Dump of assembler code for function new_myprint:
0x000000000040058e <+0>: push %rbp
0x000000000040058f <+1>: mov %rsp,%rbp
0x0000000000400592 <+4>: mov $0x40066d,%edi
0x0000000000400597 <+9>: callq 0x400450 <puts@plt>
0x000000000040059c <+14>: nop
0x000000000040059d <+15>: pop %rbp
0x000000000040059e <+16>: retq
End of assembler dump.
4. main函数分析
int main(int argc, char*argv[])
{
//初始化地址和跳转相关变量
unsigned int addr;
unsigned int addr2;
int jmp_relative;
char jmpbuf[5];
pid_t target_pid;
int status = -1;
if (argc < 4)
{
printf("usage: /jmp \"target_pid\" \"source_addr\" \"target_addr\" \n") ;
return 0;
}
target_pid = atoi(argv[1]);
addr = atoi(argv[2]); //source
addr2 = atoi(argv[3]);//target
//jmp的偏移量:目标地址addr2与下一条指令地址(addr + 5 跳转指令长度)的差值。
//4195709(addr地址即myprint) + 5 + jmp_relative = 4195726 (addr2地址即new_myprint)
jmp_relative = addr2 - ( addr + 5);
//打印相关地址信息
/*
addr2=0x40058e addr=0x40057d
jmp relative 12 (0x0000000c)
*/
printf("addr2=%p addr=%p \n",addr2, addr);
printf("jmp relative %ld (0x%08lx)\n", jmp_relative, jmp_relative) ;
//构造JMP指令,jmp指令直接修改指令寄存器IP的值
//近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9
//E9指令计算方法:跳转地址 = 基地址+偏移量+跳转指令长度
//0x40057d + JMP指令长度 + jmp_relative(12) = 目标地址(0x40058e)
// 4195709 + 5 + 12 = 4195726
jmpbuf[0] = 0xe9; /* jmp */
//拷贝内存jmpbuf+1地址开始的4(sizeof(int))字节到jmp_relative
// jmpbuf = 0xffffffe9 0c00 0000
memcpy(jmpbuf+1, &jmp_relative, sizeof(int));
status = 1;
//jmpbuf = 0xffffff e9 0c 00 00 00
printf("jmpbuf = 0x%02x%02x%02x%02x%02x \n",jmpbuf[0], jmpbuf[1] , jmpbuf[2] , jmpbuf[3] , jmpbuf[4]);
//调用PTRACE_ATTACH
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0){
perror("ptrace attach") ;
exit(-2) ;
}
DEBUG("attached %d\n", target_pid) ;
// wait(NULL)将阻止父进程,直到ptrace子进程完成
wait(NULL);
//调用函数替换set_data
if (set_data(target_pid, addr, jmpbuf, sizeof(jmpbuf)) < 0){
DEBUG("E: jmp %p %p failed. \n", (void *)addr, (void *) addr2);
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
DEBUG("detached %d \n",target_pid);
exit(-3);
}
//调用PTRACE_DETACH
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
DEBUG("detached %d \n",target_pid);
exit(status);
}
指令替换完成后结果:
5. set_data(pid_t pid,int addr,void *val,int vlen)函数分析
//main函数的传参:set_data(target_pid, addr, jmpbuf, sizeof(jmpbuf))
//addr=0x40057d (source 4195709) jmpbuf = 0xffffffe90c000000 sizeof(jmpbuf) = 5
int set_data(pid_t pid,int addr,void *val,int vlen)
{
int i;
//~:按位取反,如~3,由于3用2进制是11,所以~3即是说最低两位变成0,其它的都变成1,起到了掩码的作用。
//addr0 = 40057c addr=0x40057d
// 3 --> 0x0000 0011 ~3 --> 1111 1111 1111 1100
// 0100 0000 0000 0101 0111 1101
// &
// 1111 1111 1111 1111 1111 1100
// 0100 0000 0011 0101 0111 1100 0x40357C (4195708)
int addr0 = addr & ~3;
// 4195709 + 5 - 4195708 + 3 /4 = 2
int len = (((addr +vlen) - addr0) +3 )/4;
// *lv分配2个int空间,8字节 00000000 00000000
int *lv = malloc(len * sizeof(int));
DEBUG("peek: %d, addr0 = %x, addr-addr0 = %d",len,addr0,addr-addr0);
for (i = 0; i < len; i++) {
if (i % 4 == 0){
DEBUG("\n %p ",(void *)(addr0 + i * sizeof(int)));
}
//i=0 lv[0] = 894855ff 0x40057c
//i=1 lv[1] = 0660bfe5 0x40057c + 1(int)
lv[i] = ptrace(PTRACE_PEEKDATA, pid, addr0 + i * sizeof(int), NULL);
if (lv[i] == -1 && errno != 0) {
perror("ptrace peeku");
return -1;
}
DEBUG("%08x ",lv[i]);
}
//val=jmpbuf = 0xffffffe9 0c00 0000
//lv + (addr - addr0) --> 从lv +1 开始覆盖
//lv[0] = 000c e9ff
//lv[1] = 0660 0000
memcpy((char *) lv + (addr - addr0) , val, vlen);
DEBUG("\n poke: %d", len);
for (i = 0; i < len; i++) {
if (i%4 == 0) {
DEBUG("\n %p ",(void *) (addr0 + i * sizeof(int)));
}
// 0x40357d 0x40357C
//lv[0] = 000c e9 ff
//lv[1] = 0660 0000
if (ptrace(PTRACE_POKEDATA, pid, addr0 + i * sizeof(int) , lv[i]) < 0) {
perror("ptrace peeku");
return -1;
}
DEBUG("%08x ",lv[i]);
}
DEBUG("%s","\n") ; /* XXX */
return 0;
}