首页
社区
课程
招聘
[原创] 不碰/proc/pid/maps拿到目标App地址分布思路分享
发表于: 1天前 2953

[原创] 不碰/proc/pid/maps拿到目标App地址分布思路分享

1天前
2953

安卓 App 跨进程折腾时, 有一个经常遇到的需求就是要拿到目标 App 的地址布局, 用于远程内存读写或者用于调用相关函数实现注入和执行等等.

要实现这个需求, 通常的做法是读取 /proc/pid/maps 这个文件描述了进程的内存地址布局. 但是在一些特殊情况下, 我们没法直接访问这个文件, 具体不细说.

这里介绍一个那么不访问 /proc/pid/maps 拿到目标进程动态连接库地址分布的方法.

简而言之, 就是通过 App 的父进程, 也就是 zygote, 拿到 soinfo 的首地址, 然后直接远程内存读取遍历这个链表达. 这样我们就能拿到所有 so 的首地址和其他相关的信息了. 因为在 soinfo 挖坑风险很大, 所以很少有 App 在soinfo 挖坑, 所以通常可以直接 process_vm_readv.

我们先看 soinfo 的结构, 以安卓 15 为例

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
struct soinfo {
 
#if defined(__work_around_b_24465209__)
 
 private:
 
  char old_name_[SOINFO_NAME_LEN];
 
#endif
 
 public:
 
  const ElfW(Phdr)* phdr;
 
  size_t phnum;
 
#if defined(__work_around_b_24465209__)
 
  ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.
 
#endif
 
  ElfW(Addr) base;
 
  size_t size;
 
#if defined(__work_around_b_24465209__)
 
  uint32_t unused1;  // DO NOT USE, maintained for compatibility.
 
#endif
 
  ElfW(Dyn)* dynamic;
 
#if defined(__work_around_b_24465209__)
 
  uint32_t unused2; // DO NOT USE, maintained for compatibility
 
  uint32_t unused3; // DO NOT USE, maintained for compatibility
 
#endif
 
  soinfo* next;
 
......
 
 public:
 
  link_map link_map_head;
 
  bool constructors_called;
 
  // When you read a virtual address from the ELF file, add this
 
  // value to get the corresponding address in the process' address space.
 
  ElfW(Addr) load_bias;
 
#if !defined(__LP64__)
 
  bool has_text_relocations;
 
#endif
 
  bool has_DT_SYMBOLIC;

只要计算一下就可以拿到 soinfo->next 和 soinfo->load_bias 这两个关键信息了.

首先第一个问题是 soinfo 链表的 head在哪里? 同样以安卓15为例, head 其实是 linker 里的一个静态变量.

1
2
3
static uint8_t __libdl_info_buf[sizeof(soinfo)] __attribute__((aligned(8)));
 
static soinfo* __libdl_info = nullptr;

虽然我们不方便访问目标进程的 maps 但是我们可以安全访问 zygote 的 maps. 而 App 是从 zygote fork出来的, 继承了一样的地址空间. 所以我们直接读 zygote 的 maps 然后解析 bionic 的 elf 拿到 __libdl_info_buf 地址. 接着根据这个地址在目标 App 进程里直接 process_vm_readv 读取即可.

思路是对的, 但是实践的时候我们会遇到一个很大的问题, 就是在不同的安卓版本中, soinfo 的结构是有较大差异的. 如果不能抹平这个思路就毫无用处了.

这里再介绍一个暴力扫搜 soinfo 成员结构的方法.

首先另起一个检测进程, 在这个检测进程中调用 dl_iterate_phdr 拿到头两个 so 的 一些信息, 主要是

1
2
3
const char* strtab;
 
ElfW(Sym)* symtab;

因为这两个成员在所有的版本的soinfo中都是相邻的, 所以紧接着在这个检测进程中, 遍历 maps 中每个可读的区域, 直接搜

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
static
 
int get_strtab_symtab(struct dl_phdr_info* info, size_t size, void* ptr)
 
{
 
    auto* data = reinterpret_cast<Data*>(ptr);
 
 
 
    if (data->index != data->target) {
 
        data->index += 1;
 
        return 0; // continue
 
    }
 
 
 
#if DEBUG_LOG
 
        std::cout << "name    " << info->dlpi_name << std::endl;
 
#endif
 
 
 
    ElfW(Dyn)* dynamic{nullptr};
 
    ElfW(Addr) bias{0};
 
    bool first_phdr{true};
 
 
 
    for(size_t i = 0; i < info->dlpi_phnum; ++i) {
 
        if (info->dlpi_phdr[i].p_type == PT_LOAD and first_phdr) {
 
            first_phdr = false;
 
            bias = info->dlpi_addr - info->dlpi_phdr[i].p_vaddr;
 
        }
 
        if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) {
 
            dynamic = reinterpret_cast<ElfW(Dyn)*>(info->dlpi_phdr[i].p_vaddr + bias);
 
        }
 
    }
 
 
 
    for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
 
        switch(d->d_tag) {
 
            case DT_SYMTAB:
 
                data->symtab = reinterpret_cast<ElfW(Sym)*>(bias + d->d_un.d_ptr);
 
                break;
 
            case DT_STRTAB:
 
                data->strtab = reinterpret_cast<const char*>(bias + d->d_un.d_ptr);
 
                break;
 
        }
 
    }
 
 
 
    data->base = info->dlpi_addr;
 
    data->name = info->dlpi_name;
 
 
 
#if DEBUG_LOG
 
    std::cout << "strtab  " << (void*)data->strtab << std::endl;
 
    std::cout << "symtab  " << (void*)data->symtab << std::endl;
 
    std::cout << "phdr    " << info->dlpi_phdr << std::endl;
 
    std::cout << "dynamic " << dynamic << std::endl;
 
    std::cout << "bias    " << (void*)bias << std::endl;
 
#endif
 
    return 1;
 
}
 
 
 
static
 
Data get_soinfo_next_ptr(size_t target)
 
{
 
    Data data { nullptr, nullptr, target, 0 };
 
 
 
    dl_iterate_phdr(get_strtab_symtab, &data);
 
 
 
    FILE* fp = fopen("/proc/self/maps", "re");
 
    if (fp == nullptr) {
 
        return { 0, 0, 0, 0, 0, {}, 0 };
 
    }
 
 
 
    char line[BUFSIZ];
 
 
 
    while(fgets(line, sizeof(line), fp) != nullptr) {
 
        uintptr_t begin;
 
        uintptr_t end;
 
        char r,w;
 
 
 
        if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %c%c", &begin, &end, &r, &w) == 4) {
 
            if (r == 'r') {
 
                void* ptr = memmem(reinterpret_cast<void*>(begin), end - begin, &data, sizeof(void*) * 2);
 
                if (ptr != nullptr and ptr != &data) {
 
#if DEBUG_LOG && 0
 
                    printf("%s", line);
 
                    for (int i = 0; i < 8; ++i) {
 
                        std::cout << (reinterpret_cast<void**>(ptr)[-i+1]) << std::endl;
 
                    }
 
#endif
 
                    data.next_ptr = &reinterpret_cast<void**>(ptr)[-2];
 
                    break;
 
                }
 
            }
 
        }
 
    }
 
    fclose(fp);
 
    return data;
 
}

由此可以拿到两个 soinfo 的所在区域了. 实际上头两个 soinfo 在地址空间中是距离相当远的, 一个在静态数据区, 一个在堆里, 理论上直接根据地址范围在第一个soinfo 直接可以搜到第二个soinfo的首地址. 给出的代码里我是偷懒直接假设 next 成员是在strtab 前面两个指针处两,在较新的版本中这样假设是可以的.

水字数到这里也够体面了, 接下来直接提供代码大家自行自己看吧.

最后于 1天前 被vrolife编辑 ,原因: 改为markdown
上传的附件:
收藏
免费 6
支持
分享
最新回复 (12)
雪    币: 1947
活跃值: (1902)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
拿安卓每个版本的soinfo  ,然后switch 哈哈哈
1天前
0
雪    币: 2886
活跃值: (5410)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
3
谁来执行这个代码,如果,你是app进程,并没有读取zygote的权限啊,如果是root进程,读哪里都一样,我对你这个思路的使用方式更感兴趣
1天前
0
雪    币: 2211
活跃值: (10179)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
"只要计算一下就可以拿到 soinfo->next 和 soinfo->load_bias 这两个关键信息了",小白懵了,这个具体是怎么计算的,可以给个例子嘛
1天前
0
雪    币: 2237
活跃值: (2110)
能力值: (RANK:400 )
在线值:
发帖
回帖
粉丝
5
全版本适配及稳定性是个大问题啊
1天前
0
雪    币: 1112
活跃值: (3151)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6

solist 是个坑,不如这样

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
#include <linux/module.h>
#include <linux/sched.h> /* For task_struct */
#include <linux/pid.h>   /* For find_get_pid */
#include <linux/mm.h>    /* For mm_struct and vm_area_struct */
 
void print_memory_maps(struct task_struct *task)
{
    struct mm_struct *mm;
    struct vm_area_struct *vma;
 
    if (!task || !task->mm)
        return;
 
    mm = task->mm;
 
    down_read(&mm->mmap_sem); // Lock the memory map
    for (vma = mm->mmap; vma; vma = vma->vm_next) {
        printk(KERN_INFO "Address: %lx-%lx, Flags: %08lx\n",
               (unsigned long)vma->vm_start,
               (unsigned long)vma->vm_end,
               (unsigned long)vma->vm_flags);
    }
    up_read(&mm->mmap_sem); // Unlock the memory map
}
 
static int __init my_module_init(void)
{
    struct pid *pid;
    struct task_struct *task;
 
    pid = find_get_pid(1234); // Replace 1234 with the target PID
    if (pid) {
        task = pid_task(pid, PIDTYPE_PID);
        if (task) {
            print_memory_maps(task);
            put_task_struct(task);
        }
        put_pid(pid);
    }
 
    return 0;
}
 
static void __exit my_module_exit(void)
{
    // Cleanup code here
}
 
module_init(my_module_init);
module_exit(my_module_exit);
 
MODULE_LICENSE("GPL");
23小时前
1
雪    币: 1112
活跃值: (3151)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
正向开发没必要踩这个坑。逆向开发有 root 怎么样都行。
23小时前
0
雪    币: 440
活跃值: (1283)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
Thehepta 谁来执行这个代码,如果,你是app进程,并没有读取zygote的权限啊,如果是root进程,读哪里都一样,我对你这个思路的使用方式更感兴趣
主要是这样过inotify方便一点
22小时前
0
雪    币: 440
活跃值: (1283)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
9
Amun solist 是个坑,不如这样 ```C #include #include /* For task_struct */ #include /* For find_get_pid * ...
这样遇到不同的内核版本也完蛋, 只能弄payload module 我试过这个, 很累
22小时前
0
雪    币: 440
活跃值: (1283)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
你瞒我瞒 "只要计算一下就可以拿到 soinfo->next 和 soinfo->load_bias 这两个关键信息了",小白懵了,这个具体是怎么计算的,可以给个例子嘛
你可以不算的, 代码里的做法就是暴力搜这两个成员的偏移量
22小时前
0
雪    币: 8844
活跃值: (6080)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
11
感谢大佬分享技术
22小时前
0
雪    币: 5
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12

可是这样只能过读maps的检测,获取到地址后vmread读内存还是会被inotify mem检测吧

最后于 17小时前 被mb_fycbgpri编辑 ,原因:
18小时前
0
雪    币: 639
活跃值: (4091)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
13
mb_fycbgpri 可是这样只能过读maps的检测,获取到地址后vmread读内存还是会被inotify mem检测吧
inotify 是针对文件操作的 vmread读内存不会触发的 
16小时前
0
游客
登录 | 注册 方可回帖
返回
//