安卓逆向 不碰/proc/pid/maps拿到目标App地址分布思路分享

admin 2024年11月15日10:14:19评论22 views字数 3823阅读12分44秒阅读模式

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

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

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

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

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

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 里的一个静态变量.

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 的 一些信息, 主要是

const char* strtab;ElfW(Sym)* symtab;

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

staticint 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;}staticData 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 前面两个指针处两,在较新的版本中这样假设是可以的.

· 今 日 推 荐 ·

扫描二维码下载学习视频

本文内容来自网络,如有侵权请联系删除

安卓逆向  不碰/proc/pid/maps拿到目标App地址分布思路分享

原文始发于微信公众号(逆向有你):安卓逆向 -- 不碰/proc/pid/maps拿到目标App地址分布思路分享

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月15日10:14:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓逆向 不碰/proc/pid/maps拿到目标App地址分布思路分享https://cn-sec.com/archives/3396359.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息