为了账号安全,请及时绑定邮箱和手机立即绑定

Android ARM下的的so注入

标签:
Android

被测试代码:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

     

#include<stdio.h>

#include<stdlib.h>

int count = 0;

void print()

{

    printf("hello,%d\n",count);

        sleep(1);

}

int main(int argc, char const *argv[])

{

    while(1){

        print();

        count++;

    }

    return 0;

}</stdlib.h></stdio.h>

 

注入方法都是通过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);

原文链接:http://www.apkbus.com/blog-705730-61822.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消