最新消息:code4apk全新上线,专注于android代码分享,android源码下载,打造专业的android学习分享平台!

Android ARM下的的so注入

android教程 admin 1214浏览 0评论

被测试代码:

注入方法都是通过ptrace实现的.
本文代码在github.

调用系统so库中的函数

目标函数是libc.so中的sleep函数.
正常情况是每输出一次暂停一秒,现在我们让它暂停10秒.

总体思路

  • 获取目标进程sleep函数地址
  • 在目标进程内执行sleep函数

如何获取函数地址

  • 已知条件: 本进程的基址、目标进程的基址、本进程中sleep函数的地址(当然,这些已知条件也是需要获得的 :p)
    /proc/<pid>/maps文件中存储的是进程内存映射详情,我们可以在这个文件中查询进程中so的基址;
    sleep函数在本进程中的地址直接可以获得(void*)

  • 求解: 目标进程中sleep函数地址

  • 计算: 本进程sleep地址 – 本进程基址 + 目标进程基址

获取so库的加载基址

打开/proc/<pid>/maps文件找到基址.

[代码]java代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void* get_module_base(int pid, const char* module_name)
{
    FILE *f;        //文件指针
    long addr = 0//模块地址
    char filename[32];  //maps路径
    char *pch;
    char line[1024];    //每行
    if(pid == 0){
        snprintf(filename, sizeof(filename), "/proc/self/maps");
    } else {
        snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
    }
    f = fopen(filename, "r");
    if(f != NULL){
        while(fgets(line,sizeof(line),f)){
           if(strstr(line, module_name)) {  //找到该行是否含有module_name
               pch = strtok(line,"-");  //分割出基址字符串
               addr = strtoul(pch,NULL,0x10); //转换为16进制数
               if(addr == 0x8000)   //32位linux程序中默认的text加载地址为0x08408000,64位的改为0x00400000,此时计算base地址就没什么用了
                   addr = 0;
               break;
            }
        }
        fclose(f);
    }
    return (void*)addr;
}

计算目标进程中sleep函数地址

[代码]java代码:

01
02
03
04
05
06
07
08
09
10
long get_remote_addr(int target_pid, const char* module_name, void* local_addr)
{
    void* local_handle = get_module_base(0,module_name);
    void* remote_handle = get_module_base(target_pid,module_name);
    printf("local_handle:%p  remote_handle:%p\n", local_handle, remote_handle);
    //计算公式
    long remote_addr = (long)((uint32_t)local_addr - (uint32_t)local_handle + (uint32_t)remote_handle);
    printf("remote_addr:%p\n", remote_addr);
    return remote_addr;
}

如何执行sleep函数

  • 设置函数参数,如果参数个数小于等于4,参数按顺序放入R0~R4寄存器中;如果参数个数大于4,多余的部分需要入栈.
  • 设置pc寄存器的值,设置当前指令集标志位.
  • 应用以上寄存器的修改使之生效.
  • 等待函数执行.

[代码]java代码:

01
02
03
04
05
06
07
08
09
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
//目标进程id,参数地址,参数个数,寄存器地址
int ptrace_call(int pid, long addr, long *params, uint32_t params_num, struct pt_regs* regs)
{
    uint32_t i;
    for (i = 0; i < params_num && i < 4; i++) {     //设置少于4个的参数
        regs->uregs[i] = params[i];
    }
    //设置多于4个的参数
    if (i < params_num) {
        regs->ARM_sp -= (params_num - i) * long_size;    //抬高栈顶指针(分配空间)
        writeData(pid, (long)regs->ARM_sp, (char*)¶ms[i], (params_num - i) * long_size); //写入
    }
    regs->ARM_pc = addr;    //设置pc
    if (regs->ARM_pc & 1) {     //判断是否是Thumb指令
        regs->ARM_pc &= (~1u);  //Thumb的pc最后一位总是0
        regs->ARM_cpsr |= CPSR_T_MASK;  //T标志位为1
    } else {    //arm
        regs->ARM_cpsr &= ~CPSR_T_MASK;  //T标志位为0
    }
    regs->ARM_lr = 0;   //为了使sleep函数执行完毕后产生“内存访问错误”,这样我们就知道什么时候执行完了
    if(ptrace_setregs(pid,regs)==-1 || ptrace_continue(pid)==-1){   //目标进程继续执行
        return -1;
    }
    int stat = 0;                   //WUNTRACED表示如果pid进程进入暂停状态,那么waitpid函数立即返回
    waitpid(pid,&stat,WUNTRACED);   //等待sleep函数执行,等待过程中本进程暂停执行
    printf("%d\n", stat);
    while (stat != 0xb7f) {     //0xb7f表示目标进程进入暂停状态
        printf("%d\n", stat);
        if (ptrace_continue(pid) == -1) {
            return -1;
        }
        waitpid(pid,&stat,WUNTRACED);
    }
    return 0;
}

如何注入

  • 保存寄存器的值
  • 获得sleep函数地址
  • 执行sleep函数
  • 恢复寄存器的值

[代码]java代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
void inject(int pid)
{
    struct pt_regs old_regs,regs;
    long sleep_addr;
    //保存寄存器
    ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
    memcpy(®s, &old_regs, sizeof(regs));
    long parameters[1];
    parameters[0] = 10;
    sleep_addr = get_remote_addr(pid, "libc.so", (void*)sleep);
    ptrace_call(pid,sleep_addr,parameters,1,®s);
    //恢复寄存器
    ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
}

主函数

[代码]java代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<string.h>  // strstr,strtok
#include<stdlib.h>  //strtoul
#include<stdint.h>  //uint32_t
#include<unistd.h>  //sleep
#include<sys ptrace.h="">
#include<linux wait.h="">    // WUNTRACED
#include<time.h>
int main(int argc, char* argv[])
{
    if(argc != 2){
        printf("usage: %s <pid to="" be="" traced="">\n",argv[0]);
        return 1;
    }
    int pid = atoi(argv[1]);
    if(0 != ptrace(PTRACE_ATTACH, pid, NULL, NULL)){
        printf("attach failed.");
        return 1;
    }
    inject(pid);
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    return 0;
}</pid></time.h></linux></sys></unistd.h></stdint.h></stdlib.h></string.h></stdio.h>

番外

/proc/<pid> 文件夹

文件 内容
cmdline 命令行全名(加参数变量)和 ps 命令中的command 列结果一样
cwd 进程的工作目录 和 (pwdx PID) 结果相同
environ 进程的环境变量
exe 一般是/bin/ 的链接
fd 进程打开的文件描述fu .用ls -l 可以查看具体的文件 (可以用lsof -p PID)
status 进程的相关状态
task 该目录下是进程所包含的线程(note: ps 可以查看线程)
mounts 进程挂载点
maps 进程内存映射详情

关于pc寄存器

arm.pdf 中的A2.4.3 Register 15 and the program counter有这样一段话:
是关于指令集在pc寄存器上的表现的.

Reading the program counter
When an instruction reads the PC, the value read depends on which instruction set it comes from:
• For an ARM instruction, the value read is the address of the instruction plus 8 bytes. Bits [1:0] of this
value are always zero, because ARM instructions are always word-aligned.
• For a Thumb instruction, the value read is the address of the instruction plus 4 bytes. Bit [0] of this
value is always zero, because Thumb instructions are always halfword-aligned.

关于CPSR寄存器

31 30 29 28 27 26 25 24 23 20 19 16 15 10 9 8 7 6 5 4 0
N Z C V Q Res J RESERVED GE[3:0] RESERVED E A I F T M[4:0]

其中J和T标记位代表当前指令集:

J T Instruction set
0 0 ARM
0 1 Thumb
1 0 Jazelle
1 1 RESERVED

关于waitpid

详细介绍可看官方文档.

参数status

wait函数调用过后,status指针指向可以被宏解析的值,这些宏在ndk目录下platforms/android-21/arch-arm/usr/include/sys/wait.h文件中定义.

高2字节用于表示导致子进程的退出或暂停状态信号值(WTERMSIG),低2字节表示子进程是退出(0x0)还是暂停(0x7f)状态(WEXITSTATUS)。
如:0xb7f就表示子进程为暂停状态,导致它暂停的信号量为11即sigsegv错误。
关于错误代码的文档可看这里,
定义在ndk目录下platforms/android-21/arch-arm/usr/include/asm/signal.h中.

其中两个宏:
WEXITSTATUS(*statusPtr):
if the child process terminates normally, this macro evaluates to the lower 8 bits of the value passed to the exit or _exit function or returned from main.
WTERMSIG(*statusPtr)
if the child process ends by a signal that was not caught, this macro evaluates to the number of that signal.

参数options

指定了waitpid的额外行动.选项有:

WNOHANG:
告诉waitpid不等程序中止立即返回status信息.
正常情况是当主进程对子进程使用了waitpid,主进程就会阻塞直到waitpid返回status信息;如果指定了WNOHANG选项,主进程就不会阻塞了.
如果还没有可用的status信息,waitpid返回0.

WUNTRACED:
告诉waitpid,如果子进程进入暂停状态或者已经终止,那么就立即返回status信息,正常情况是紫禁城终止的时候才返回.
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用ptrace_cont运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。

总结

程序中的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV了.

调用自定义so库中的函数

  • 保存当前寄存器的状态
  • 获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
  • 调用mmap分配空间保存参数信息
  • 调用dlopen加载so库
  • 调用dlsym找到目标函数地址
  • 执行目标函数
  • 调用dlclose卸载so库
  • 恢复寄存器的状态

保存当前寄存器的状态

[代码]java代码:

1
2
3
struct pt_regs old_regs,regs;
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(®s,&old_regs,sizeof(regs));

获取目标程序的mmap, dlopen, dlsym, dlclose函数地址

[代码]java代码:

1
2
3
4
5
long mmap_addr,dlopen_addr,dlsym_addr,dlclose_addr;
mmap_addr = get_remote_addr(pid, libc_path, (void*)mmap);
dlopen_addr = get_remote_addr(pid, libc_path, (void*)dlopen);
dlsym_addr = get_remote_addr(pid, libc_path, (void*)dlsym);
dlclose_addr = get_remote_addr(pid, libc_path, (void*)dlclose);

调用mmap分配空间保存参数信息

mmap的原型如下:

[代码]java代码:

1
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 描述
addr 映射的起始地址,为0表示由系统决定映射的起始地址
length 映射的长度
prot 映射的内存保存属性,不能与文件的打开模式冲突
flags 指定映射对象的类型,映射选项和映射页是否可以共享
fd 有效的文件描述符,一般是由open()函数返回;其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射
offset 被映射对象内容的起点

这里我们需要的调用语句是mmap(0,0x4000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE,0,0),
PROT_EXEC表示可执行.
PROT_READ表示可读.
PROT_WRITE表示可写.
MAP_PRIVATE表示建.立一个写入时拷贝的私有映射.内存区域的写入不会影响到原文件.这个标志和以上标志是互斥的,只能使用其中一个.
MAP_ANONYMOUS表示匿名映射,映射区不与任何文件关联.
则:

[代码]java代码:

01
02
03
04
05
06
07
08
09
10
11
long parameters[10];   
parameters[0] = 0//构造参数
parameters[1] = 0x4000;
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC;
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE;
parameters[4] = 0;
parameters[5] = 0;
ptrace_call(pid,mmap_addr,parameters,6,®s);
//调用结束后获得r0中保存的返回值
ptrace(PTRACE_GETREGS,pid,NULL,®s);
long mapping_base = regs.ARM_r0;

调用dlopen加载so库

原型:

[代码]java代码:

1
2
     
void *dlopen(const char *filename, int flags);
参数 描述
filename so库名
flags 打开方式

这里我们需要的调用语句是dlopen(so_path,RTLD_NOW | RTLD_GLOBAL),
RTLD_NOW表示需要在dlopen返回前,解析出所有未定义符号,如果解析不出来在dlopen会返回NULL;
RTLD_GLOBAL表示动态库中定义的符号可被其后打开的其它库解析.
则:

[代码]java代码:

1
2
3
4
5
6
writeData(pid, mapping_base, so_path, strlen(so_path)+1);   //将库名字符串放入目标进程空间
parameters[0] = mapping_base;
parameters[1] = RTLD_NOW | RTLD_GLOBAL;
ptrace_call(pid, dlopen_addr, parameters, 2, ®s);
ptrace(PTRACE_GETREGS,pid,NULL,®s);  //调用结束后获得r0中保存的返回值
long handle = regs.ARM_r0;

调用dlsym找到目标函数地址

原型:

[代码]java代码:

1
void *dlsym(void *handle, const char *symbol);
参数 描述
handle so库的基址
symbol 函数名地址

这里我们需要的调用语句是dlsym(handle, function_name),则:

[代码]java代码:

1
2
3
4
5
6
writeData(pid, mapping_base, function_name, strlen(function_name)+1);
parameters[0] = handle;
parameters[1] = mapping_base;
ptrace_call(pid, dlsym_addr, parameters, 2, ®s);
ptrace(PTRACE_GETREGS,pid,NULL,®s);  //调用结束后获得r0中保存的返回值
long function_addr = regs.ARM_r0;

执行目标函数

先写段c程序编译为so文件:

[代码]java代码:

1
2
3
4
5
#include<stdio.h>
void hello(char *str)
{
    printf("hello %s\n",str);
}</stdio.h>

则:

[代码]java代码:

1
2
3
writeData(pid, mapping_base, function_parameters, strlen(function_parameters)+1);
parameters[0] = mapping_base;
ptrace_call(pid, function_addr, parameters, 1, ®s);

调用dlclose卸载so库

原型:

[代码]java代码:

1
int dlclose(void *handle);

则:

[代码]java代码:

1
2
parameters[0] = handle;
ptrace_call(pid,dlclose_addr,parameters,1,®s);

恢复寄存器的状态

[代码]java代码:

1
ptrace(PTRACE_SETREGS,pid,NULL,&old_regs);

转载请注明:android源码下载 » Android ARM下的的so注入

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

网友最新评论 (5)

  1. 皮鞋丝袜﮲吴佩慈丝袜﮲www.meinv95.com
    美腿袜2017-03-19 02:24 回复
  2. ᜕性感美腿᜕撸妹妹爱情的诱惑野兽的诱惑欧洲最大胆人体艺术穿着比基尼的外星人香港小姐写真大胆男性人体艺术meimei666.com
    涂佳香2017-03-19 08:30 回复
  3. ⍔领彩金⍔888真人 2RMB娱乐城美国欧普斯118T.NET
    送彩金2017-03-20 00:11 回复
  4. 新用户4460942017-04-10 14:01 回复
  5. 哈哈,精彩!但是,这世上有个现象,那就是抬死人是为了活人。那么,怀念毛是为了什么呢?实际上,他们也是对现实不满,借尸还魂,渴望一种平等而已。因此,对于猪也罢,牛也罢,不必在意,就当看一出戏。
    新用户9793032017-04-19 18:42 回复