基于ARM的Ptrace

转:http://hi.baidu.com/harry_lime/item/cce5161e4af86d4a71d5e8ff

前面提到过打算研究一下基于ARM的Ptrace,并在Mobile上实现Hook. 今天程序调通了,记录如下.

平台:Android 2.3.3, 具体Linux Kernel和ARM的版本大家可以自己去查

目标:实现两个程序target和trace. target循环用printf打印语句,trace追踪target的系统调用并替换target的打印语句


在写程序之前查资料的过程中发现一个奇怪的事情,对于ARM ptrace研究实践的文章非常少,仅有的几篇也基本上都是胡言乱语,互相抄袭,根本无法测试通过。所以还是不要希望坐享其成,老老实实根据理论完成实践。


好,先从target开始. target还是相当简单的,写代码,下载ndk,交叉编译,上传到Android,运行,搞定. 以下是target代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
                                                                                                                             
int flag = 1;
int count = 0;
int main()
{
        char * str = "abcdef" ;
        while (flag)
        {
                printf ( "Target is running:%d\n" , count);
                count++;
                sleep(3);
        }
        return 0;
}

在Android上的运行情况:


rbserver@rbserver:~/ndk_test$ adb shell

# /data/harry/target

Target is running:0

Target is running:1

Target is running:2

Target is running:3


接下来是trace, 这个花了点时间, 主要是网上误导的资料太多, 轻信于人走了不少弯路。

第一步是要能成功attach并能捕获syscall, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main( int argc, char *argv[])
{
    if (argc != 2) {
        printf ( "Usage: %s <pid to be traced>\n" , argv[0], argv[1]);
        return 1;
    }
                                                                                                 
    pid_t traced_process;
    int status;
    traced_process = atoi (argv[1]);
    if (0 != ptrace(PTRACE_ATTACH, traced_process, NULL, NULL))
    {
        printf ( "Trace process failed:%d.\n" , errno );
        return 1;
    }
    while (1)
    {
            wait(&status);
            if (WIFEXITED(status))
            {
                break ;
            }
            tracePro(traced_process);
        ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
    }
                                                                                                 
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL);
                                                                                                 
    return 0;
}

这一部分和x86的代码几乎没有任何区别,因为还没有涉及到寄存器,中断这些架构上的概念。

下面要解决的是如何获取syscall的调用号。这一点ARM和x86有很大的不同。

先看x86原先的代码:

1
2
3
4
5
orig_eax = ptrace(PTRACE_PEEKUSER, pid, 4 * ORIG_EAX, NULL);
if (orig_eax == SYS_write)
{
...
}

这样做的原因是在x86架构上,Linux所有的系统调用都是通过软中断 int 0x80来实现的,而系统调用号是存在寄存器EAX中的. 所以如果想获取系统调用号,只需要获取ORIG_EAX的值就可以了。


而在ARM架构上呢,所有的系统调用都是通过SWI来实现的. 虽然也是软中断,但方式不同,因为在ARM 架构中有两个SWI指令,分别针对EABI和OABI (关于EABI和OABI 大家可以搜索相关资料,它们是Linux针对ARM架构的两种系统调用指令):

[EABI]

机器码:1110 1111 0000 0000 -- SWI 0

具体的调用号存放在寄存器r7中.


[OABI]

机器码:1101 1111 vvvv vvvv -- SWI immed_8

调用号进行转换以后得到指令中的立即数。立即数=调用号 | 0x900000


既然需要兼容两种方式的调用,我们在代码上就要分开处理。首先要获取SWI指令判断是EABI还是OABI,如果是EABI,可从r7中获取调用号。如果是OABI,则从SWI指令中获取立即数,反向计算出调用号。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
long getSysCallNo( int pid)
{
        long scno = 0;
        struct pt_regs regs;
        ptrace(PTRACE_GETREGS, pid, NULL, &regs);
        scno = ptrace(PTRACE_PEEKTEXT, pid, ( void *)(regs.ARM_pc - 4), NULL);
        if (scno == 0)
                return 0;
        /* Handle the EABI syscall convention.  We do not
           bother converting structures between the two
           ABIs, but basic functionality should work even
           if strace and the traced program have different
           ABIs.  */
        if (scno == 0xef000000) {
                scno = regs.ARM_r7;
        } else {
                if ((scno & 0x0ff00000) != 0x0f900000) {
                        return -1;
                }
                                                                                  
                /*
                 * Fixup the syscall number
                 */
                scno &= 0x000fffff;
        }
        return scno;
                                                                                  
}

完成了这一步以后我们就可以利用trace打印出target所有的系统调用了。运行结果如下:


从结果可以看出,target每调用一次printf,会引发两次__NR_write调用(调用号为4)。


接下来我们也照葫芦画瓢,翻转__NR_write的输入字符串,全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/syscall.h>
    
int long_size = sizeof ( long );
    
void reverse( char *str)
{  
    int i, j;
    char temp;
    for (i = 0, j = strlen (str) - 2; i <= j; ++i, --j) {
        temp = str[i];
        str[i] = str[j];
        str[j] = temp;
    }
    
}
    
void getdata(pid_t pid, long addr,
        char *str, int len)
{  
    char *laddr;
    int i, j;
    union u {
        long val;
        char chars[long_size];
    }data;
    i = 0;
    j = len / long_size;
    laddr = str;
    while (i < j) {
        data.val = ptrace(PTRACE_PEEKDATA,
                pid, addr + i * 4,
                NULL);
        memcpy (laddr, data.chars, long_size);
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if (j != 0) {
        data.val = ptrace(PTRACE_PEEKDATA,
                pid, addr + i * 4,
                NULL);
        memcpy (laddr, data.chars, j);
    }
    str[len] = '\0' ;
}
    
void putdata(pid_t pid, long addr,
        char *str, int len)
{  
    char *laddr;
    int i, j;
    union u {
        long val;
        char chars[long_size];
    }data;
    i = 0;
    j = len / long_size;
    laddr = str;
    while (i < j) {
        memcpy (data.chars, laddr, long_size);
        ptrace(PTRACE_POKEDATA, pid,
                addr + i * 4, data.val);
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if (j != 0) {
        memcpy (data.chars, laddr, j);
        ptrace(PTRACE_POKEDATA, pid,
                addr + i * 4, data.val);
    }
}
    
long getSysCallNo( int pid, struct pt_regs *regs)
{
    long scno = 0;
    ptrace(PTRACE_GETREGS, pid, NULL, regs);
    scno = ptrace(PTRACE_PEEKTEXT, pid, ( void *)(regs->ARM_pc - 4), NULL);
    if (scno == 0)
        return 0;
         
    if (scno == 0xef000000) {
        scno = regs->ARM_r7;
    } else {
        if ((scno & 0x0ff00000) != 0x0f900000) {
            return -1;
        }
    
        /*
         * Fixup the syscall number
         */
        scno &= 0x000fffff;
    }
    return scno;
    
}
    
void tracePro( int pid)
{
    long scno=0;
    long regV=0;
    struct pt_regs regs;
    char * str;
    
    scno = getSysCallNo(pid, &regs);
    if (scno == __NR_write)
    {
        str = ( char *) calloc (1, (regs.ARM_r2+1) * sizeof ( char ));
        getdata(pid, regs.ARM_r1, str, regs.ARM_r2);
        reverse(str);
        putdata(pid, regs.ARM_r1, str, regs.ARM_r2);
    
        printf ( "Reverse str.\n" );
    
    }
}
    
int main( int argc, char *argv[])
{  
    if (argc != 2) {
        printf ( "Usage: %s <pid to be traced>\n" , argv[0], argv[1]);
    return 1;
    }
        
    pid_t traced_process;
    int status;
    traced_process = atoi (argv[1]);
    if (0 != ptrace(PTRACE_ATTACH, traced_process, NULL, NULL))
    {
        printf ( "Trace process failed:%d.\n" , errno );
    return 1;
    }
    while (1)
    {
        wait(&status);
        if (WIFEXITED(status))
        {
            break ;
        }
        tracePro(traced_process);
    ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
    }
    
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL);
        
    return 0;
}

测试通过,输出如下:


好了,到目前为止我们在Linux+ARM的架构上实现了一个完整的ptrace hook应用,下一步考虑进行实战,hook系统的常驻进程,达到干预其它程序的效果。

 
基于ARM的Ptrace (二)
 

基于ARM的Ptrace (二)

上次在研究Ptrace for Android的时候漏了一个东西,如何hook并修改除了Syscall 以外的函数,今天顺便实现一下。

平台:Android 2.3.3

目标:利用Ptrace拦截进程的自定义函数并修改逻辑。


先看目标进程,代码相当简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
       
int flag = 1;
int count = 0;
       
int sub()
{
    printf ( "Sub call.\n" );
    return 1;
}
       
int main()
{  
    while (flag)
    {
        printf ( "Sub return:%d\n" , sub());
        count++;
        sleep(3);
    }
    return 0;
}

我们要做的是拦截自定义函数sub(),修改函数,跳过printf语句并把返回值改成2.

基本思路是利用Ptrace attach 以后找到函数代码段的入口点,修改相应的代码即可。如何找到函数入口点?静态看或者动态调都可以。我们代码简单,静态看就好了。


静态看的过程并不如想象的顺利,原因是IDA这货真心坑爹,解析Thumb和ARM的混合代码竟然会出错:



只好手动修改一下便于查看:


一目了然,0x84D0处开始返回指针压栈,我们只需要从0x84D2开始把代码改成如下就可以了:

1
2
MOVS R0, #2
POP {R3, PC}


因而得出trace的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/syscall.h>
  
int long_size = sizeof ( long );
  
void putdata(pid_t pid, long addr,
        char *str, int len)
{  
    char *laddr;
    int i, j;
    union u {
        long val;
        char chars[long_size];
    }data;
    i = 0;
    j = len / long_size;
    laddr = str;
    while (i < j) {
        memcpy (data.chars, laddr, long_size);
        ptrace(PTRACE_POKEDATA, pid,
                addr + i * 4, data.val);
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if (j != 0) {
        memcpy (data.chars, laddr, j);
        ptrace(PTRACE_POKEDATA, pid,
                addr + i * 4, data.val);
    }
}
  
void tracePro( int pid)
{
    int len = 4;
    char insertcode[] = "\x02\x20\x08\xBD" ;
      
    putdata(pid, 0x84d2, insertcode, len);
}
  
int main( int argc, char *argv[])
{  
    if (argc != 2) {
        printf ( "Usage: %s <pid to be traced>\n" , argv[0], argv[1]);
    return 1;
    }
      
    pid_t traced_process;
    int status;
    traced_process = atoi (argv[1]);
    if (0 != ptrace(PTRACE_ATTACH, traced_process, NULL, NULL))
    {
        printf ( "Trace process failed:%d.\n" , errno );
    return 1;
    }
      
    tracePro(traced_process);
    ptrace(PTRACE_DETACH, traced_process, NULL, NULL);
    return 0;
}


上传至模拟器调试,一次成功:



### 在 ARM64 架构下实现 Hook Syscall 的内核模块,无需使用 CR0 在 ARM64 架构中,由于不存在类似 x86 的 `CR0` 寄存器来控制内存保护状态,因此需要采用其他方法来修改只读的系统调用表。以下是一个完整的解决方案,包括代码示例和关键点说明。 #### 解决方案概述 ARM64 架构下的系统调用表同样被标记为只读属性,直接修改会导致访问权限错误。为此,可以通过临时更改内存页属性的方式绕过这一限制。具体来说,可以使用 `set_memory_rw` 和 `set_memory_ro` 函数来动态修改内存区域的读写权限[^3]。 --- #### 内核模块代码示例 ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/syscalls.h> #include <asm/ptrace.h> #include <linux/set_memory.h> // 用于 set_memory_rw 和 set_memory_ro #define ORIGIN_SYSCALL_NR 59 // 假设我们 hook 的是 sys_execve #define HOOKED_SYSCALL_NAME "hooked_sys_execve" // 定义原始系统调用指针 asmlinkage long (*original_sys_execve)(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); // 定义我们的 hook 函数 asmlinkage long hooked_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { printk(KERN_INFO "Hooked system call: %s\n", HOOKED_SYSCALL_NAME); return original_sys_execve(filename, argv, envp); // 调用原始系统调用 } static unsigned long **get_sys_call_table(void) { unsigned long offset; unsigned long addr = (unsigned long)&sys_execve; for (offset = 0; offset < 0x1000; offset += sizeof(void *)) { if (*(unsigned long *)(addr + offset) == (unsigned long)sys_execve) { return (unsigned long **)(addr + offset - ORIGIN_SYSCALL_NR * sizeof(void *)); } } return NULL; } static int __init syscall_hook_init(void) { unsigned long **sys_call_table_ptr = get_sys_call_table(); if (!sys_call_table_ptr) { printk(KERN_ERR "Failed to locate sys_call_table.\n"); return -1; } // 修改内存保护以允许写入 if (set_memory_rw((unsigned long)sys_call_table_ptr, 1)) { printk(KERN_ERR "Failed to set memory rw.\n"); return -1; } // 保存原始系统调用 original_sys_execve = (void *)sys_call_table_ptr[ORIGIN_SYSCALL_NR]; // 替换系统调用 sys_call_table_ptr[ORIGIN_SYSCALL_NR] = (unsigned long)hooked_sys_execve; // 恢复内存保护 if (set_memory_ro((unsigned long)sys_call_table_ptr, 1)) { printk(KERN_ERR "Failed to set memory ro.\n"); return -1; } printk(KERN_INFO "System call hooked successfully.\n"); return 0; } static void __exit syscall_hook_exit(void) { unsigned long **sys_call_table_ptr = get_sys_call_table(); if (!sys_call_table_ptr) { printk(KERN_ERR "Failed to locate sys_call_table during cleanup.\n"); return; } // 修改内存保护以允许写入 if (set_memory_rw((unsigned long)sys_call_table_ptr, 1)) { printk(KERN_ERR "Failed to set memory rw during cleanup.\n"); return; } // 恢复原始系统调用 sys_call_table_ptr[ORIGIN_SYSCALL_NR] = (unsigned long)original_sys_execve; // 恢复内存保护 if (set_memory_ro((unsigned long)sys_call_table_ptr, 1)) { printk(KERN_ERR "Failed to set memory ro during cleanup.\n"); return; } printk(KERN_INFO "System call unhooked successfully.\n"); } module_init(syscall_hook_init); module_exit(syscall_hook_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple syscall hook module for ARM64 without CR0."); ``` --- #### 关键点说明 1. **获取系统调用表地址** 使用 `get_sys_call_table` 函数通过遍历内存区域找到 `sys_call_table` 的基地址。此方法基于已知的系统调用号(如 `sys_execve`)来定位表项[^3]。 2. **修改内存保护** 在 ARM64 架构中,使用 `set_memory_rw` 和 `set_memory_ro` 函数来动态更改内存页的读写权限。这一步是必要的,因为直接写入只读内存会导致访问权限错误。 3. **恢复原始系统调用** 在模块卸载时,必须将系统调用表恢复到原始状态,否则可能导致系统不稳定或崩溃。 4. **兼容性问题** 不同架构对系统调用号的定义可能不同。例如,在 MIPS64 上,`__NR_xxx` 宏的值会加上 `__NR_Linux`,需要特别注意这一点[^4]。 --- #### 注意事项 - 该模块仅适用于 Linux Kernel 6.1 和 ARM64 架构。 - 修改系统调用表可能会导致安全风险,请确保在受控环境中测试。 - 如果目标系统启用了 KASLR(内核地址空间布局随机化),需要额外处理随机化的基地址问题[^5]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值