为什么部分机型Game Guardian没有Ca内存

1,713 阅读8分钟

前言

GameGuardian是Android平台最知名的内存浏览工具(下文简称GG),被广泛用于游戏反作弊测试、安全性测试、在游戏厂商允许的前提下制作MOD等。

GG涉及到各种Android/Linux平台的技术,比较值得学习研究。

从它官网的数据,下载量将近2亿,2012年推出了第一个版本,目前最新版本是2021年3月21发布的101.1,由于target版本过低,Android14就已经无法直接安装,目前看来是不打算再更新了。

GG把内存空间划分为不同的类型,比如"Ca"、"Xa"等。Android11正式版,在2020年9月8被推出之后,网络上逐渐出现了不少用户反馈GG找不到"Ca"内存,但是简单从互联网搜索后,并没发现有合理的回答。

image.png

image.png

在Android11之前,也有极小部分用户遇到了找不到"Ca"内存的情况。GG作者Enyby答复:

image.png

虚拟内存区域划分

在进程空间中,不同地址区间的内存有不同的来源,比如有些内存是直接映射的磁盘上某个文件,有些内存是通过C语言代码通过malloc函数申请得到的。

某个数据所在的内存地址,如果在代码中,是通过C语言调用libc的malloc函数申请的内存空间,那么每次启动进程,它都会在libc的malloc函数管理的区域中。GG依此把内存划分为多个区域,以便于可以更精确和快速的找到目标数据的内存地址。

为了方便程序员修复BUG和分析内存占用,Linux内核提供了获取虚拟内存信息的接口,主要是/proc/{pid}/maps/proc/{pid}/smaps两个文件。pmap命令也仅仅是对这个接口的封装,Android14源码目录/external/toybox/toys/other/pmap.c

// ...忽略
sprintf(toybuf, "/proc/%u/%smaps", pid, "s"+!FLAG(x));
if (!(fp = fopen(toybuf, "r"))) {
  error_msg("no %s", toybuf);
  continue;
}
// ...

mapssmaps具体内容包含了虚拟内存地址起止范围、权限、映射名称等。两个文件节选如下:

sunstone:/ $ cat /proc/self/maps
58e143c000-58e1468000 r--p 00000000 fd:08 690                            /system/bin/toybox
58e1468000-58e14b0000 r-xp 0002c000 fd:08 690                            /system/bin/toybox
58e14b0000-58e14b4000 r--p 00074000 fd:08 690                            /system/bin/toybox
58e14b4000-58e14b8000 rw-p 00077000 fd:08 690                            /system/bin/toybox
58e14b8000-58e14bc000 rw-p 00000000 00:00 0                              [anon:.bss]
7d6ac00000-7d6aec7000 ---p 00000000 00:00 0                              [anon:cfi shadow]
7d6aec7000-7d6aec8000 r--p 00000000 00:00 0                              [anon:cfi shadow]
7d6aec8000-7d6afef000 ---p 00000000 00:00 0                              [anon:cfi shadow]
7d6afef000-7d6aff0000 r--p 00000000 00:00 0                              [anon:cfi shadow]
7d6aff0000-7deac00000 ---p 00000000 00:00 0                              [anon:cfi shadow]
7deac00000-7deb400000 rw-p 00000000 00:00 0                              [anon:libc_malloc]
7deb575000-7deb578000 r--p 00000000 fd:08 2947                           /system/lib64/libnetd_client.so
7deb578000-7deb57d000 r-xp 00003000 fd:08 2947                           /system/lib64/libnetd_client.so
7deb57d000-7deb57e000 r--p 00008000 fd:08 2947                           /system/lib64/libnetd_client.so
7deb57e000-7deb57f000 rw-p 00008000 fd:08 2947                           /system/lib64/libnetd_client.so
7deb5b1000-7deb615000 rw-p 00000000 00:00 0                              [anon:linker_alloc]
7deb615000-7deb617000 r--p 00000000 fd:08 2709                           /system/lib64/libcgrouprc.so
7deb617000-7deb618000 r-xp 00002000 fd:08 2709                           /system/lib64/libcgrouprc.so
7deb618000-7deb619000 r--p 00003000 fd:08 2709                           /system/lib64/libcgrouprc.so
7deb619000-7deb61a000 rw-p 00003000 fd:08 2709                           /system/lib64/libcgrouprc.so
.. 省略
sunstone:/ $ cat /proc/self/smaps                                                                                                                            
64a3d37000-64a3d63000 r--p 00000000 fd:08 690                            /system/bin/toybox
Size:                176 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  64 kB
Pss:                  64 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:        64 kB
Private_Dirty:         0 kB
Referenced:           64 kB
Anonymous:             0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:        0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:		0
VmFlags: rd mr mw me dw 
64a3d63000-64a3dab000 r-xp 0002c000 fd:08 690                            /system/bin/toybox
Size:                288 kB
KernelPageSize:        4 kB
... 省略

Android的sysdump meminfo具体的获取内存占用的实现代码在/frameworks/base/core/jni/android_os_Debug.cpp,也是解析的/proc/{pid}/smaps。可以从这里看到如何根据内存名称对内存进行分类(留意这里[anon:libc_malloc][anon:scudo:):

uint32_t namesz = name.size();
if (base::StartsWith(name, "[heap]")) {
    which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:libc_malloc]")) {
    which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:scudo:")) {
    which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:GWP-ASan")) {
    which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[stack")) {
    which_heap = HEAP_STACK;
} else if (base::StartsWith(name, "[anon:stack_and_tls:")) {
    which_heap = HEAP_STACK;
} else if (base::EndsWith(name, ".so")) {
    which_heap = HEAP_SO;
    is_swappable = true;
} else if (base::EndsWith(name, ".jar")) {
    which_heap = HEAP_JAR;
    is_swappable = true;
} else if (base::EndsWith(name, ".apk")) {
    which_heap = HEAP_APK;
    is_swappable = true;
} else if (base::EndsWith(name, ".ttf")) {
    which_heap = HEAP_TTF;
    is_swappable = true;

而GG对内存的分类,也是基于maps文件的。

image.png

可以把GG拖到JADX,搜索相关字符串定位到代码:

a(arrayList, luaTable, "REGION_JAVA_HEAP", 2);
a(arrayList, luaTable, "REGION_C_HEAP", 1);
a(arrayList, luaTable, "REGION_C_ALLOC", 4);
a(arrayList, luaTable, "REGION_C_DATA", 8);
a(arrayList, luaTable, "REGION_C_BSS", 16);
a(arrayList, luaTable, "REGION_PPSSPP", 262144);
a(arrayList, luaTable, "REGION_ANONYMOUS", 32);
a(arrayList, luaTable, "REGION_JAVA", 65536);
a(arrayList, luaTable, "REGION_STACK", 64);
a(arrayList, luaTable, "REGION_ASHMEM", 524288);
a(arrayList, luaTable, "REGION_VIDEO", 1048576);
a(arrayList, luaTable, "REGION_OTHER", -2080896);
a(arrayList, luaTable, "REGION_BAD", 131072);
a(arrayList, luaTable, "REGION_CODE_APP", 16384);
a(arrayList, luaTable, "REGION_CODE_SYS", 32768);

GG的ELF文件位于/res/raw/目录中,其中ydw2文件拖到IDA,里面打开maps的其中一处函数sub_109E0如下:

  // ... 忽略
  strcpy(format, "0qspd0&e0nbqt");
  v1 = &format[1];
  v2 = 48;
  do
  {
    *(v1 - 1) = v2 - 1;
    v2 = *v1++;
  }
  while ( v2 );
  v3 = getpid();
  snprintf(v0, 0x10000uLL, format, v3);
  v4 = (unsigned int *)__errno();
  *v4 = 0;
  v5 = fopen(v0, "r");
  //... 忽略
  while ( fgets(v0, 0x10000, v6) )
  {
    // 忽略
    v12 = sscanf(v0, "%zx-%zx %s %zx %*s %*s %n", &v50, &v49, &v51, &v48, &v47);
    if ( v12 == 4 )
    //... 忽略

这里0qspd0&e0nbqt不知为何做要做加密,把这段伪代码跑一下可以确定就是/proc/%d/mapsimage.png

在这个函数中,间接调用了sub_10DC0,这正是GG划分内存区域的逻辑,部分伪代码如下:

 if ( haystack )
  {
    if ( strstr(haystack, "[anon:.bss]") )
    {
      if ( !byte_5DF410 )
      {
        byte_5DF410 = 1;
        sub_5C8D90(4LL, "android-daemon", "BSS: 2");
      }
      result = 16LL;
      if ( dword_5DD048 != 8 )
        result = 2048LL;
      dword_5DD048 = result;
      return result;
    }
    if ( !strncmp(haystack, "/system/", 8uLL) )
    {
      dword_5DD048 = 1024;
      return 1024LL;
    }
    if ( strstr(haystack, "/dev/zero") )
      goto LABEL_91;
    if ( strstr(haystack, "PPSSPP_RAM") )
    {
      dword_5DD048 = 0x40000;
      return 0x40000LL;
    }
    if ( !strstr(haystack, "system@")
      && !strstr(haystack, "gralloc")
      && strncmp(haystack, "[vdso]", 6uLL)
      && strncmp(haystack, "[vectors]", 9uLL)
      && (strncmp(haystack, "/dev/", 5uLL) || !strncmp(haystack, "/dev/ashmem", 0xBuLL)) )
    {
      if ( strstr(haystack, "dalvik") )
      {
        if ( (strstr(haystack, "eap")
           || strstr(haystack, "dalvik-alloc")
           || strstr(haystack, "dalvik-main")
           || strstr(haystack, "dalvik-large")
           || strstr(haystack, "dalvik-free"))
          && !strstr(haystack, "itmap")
          && !strstr(haystack, "ygote")
          && !strstr(haystack, "ard")
          && !strstr(haystack, "jit")
          && !strstr(haystack, "inear") )
        {
          dword_5DD048 = 2;
          result = 2LL;
        }
        else
        {
          dword_5DD048 = 0x10000;
          result = 0x10000LL;
        }
        return result;
      }
      if ( strstr(haystack, "/lib") && strstr(haystack, ".so") )
      {
        if ( byte_5E66B0[0] )
        {
          v7 = byte_5E66B0;
        }
        else
        {
          if ( strstr(haystack, "/data/") )
          {
LABEL_89:
            dword_5DD048 = 8;
            return 8LL;
          }
          v7 = "/mnt/";
        }
        if ( strstr(haystack, v7) )
          goto LABEL_89;
      }
      if ( strstr(haystack, "malloc") )
      {
LABEL_91:
        dword_5DD048 = 4;
        return 4LL;
      }
      if ( strstr(haystack, "[heap]") )
      {
        dword_5DD048 = 1;
        return 1LL;
      }
      if ( strstr(haystack, "[stack") )
      {
LABEL_95:
        dword_5DD048 = 64;
        return 64LL;
      }

这里的dword_5DD048正对应着刚刚贴出的Java代码的参数四,例如dword_5DD048 = 4;对应着 a(arrayList, luaTable, "REGION_C_ALLOC", 4);4,也就是说,GG所谓的Ca内存是通过查找/proc/{pid}/maps的最后一列是否包含malloc字符串而确定的。

内存分配器 & VMA NAME

C语言常用*alloc系列函数分配内存,malloc只是libc提供的接口,而具体的实现,取决于Android系统。

内存分配器通过mmap系统调用,向系统申请一大段内存空间,内存分配器管理这段空间,当malloc每次被调用时,会从内存分配器管理的空间中划分出一部分,返回给调用者。

在Android平台,Android5之后的版本使用jemalloc作为libc的内存分配器的实现。Android10的jemalloc路径是/external/jemalloc/。其中/external/jemalloc/src/pages.c有为虚拟内存地址设置名称的代码:

#if defined(__ANDROID__)
	if (ret != NULL) {
		/* Name this memory as being used by libc */
		prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ret,
		    size, "libc_malloc");
	}
#endif

这也就是GG通过名称中是否存在"malloc"而确定是否为"C++ Alloca"内存的依据。

但是在Android11开始,默认的内存分配器从jemalloc改为了scudo,Adnroid11源码路径/external/scudo/。sudo更安全但是会占用更多内存空间,所以在一些设备上仍然在使用jemalloc(我手边的红米note和一加都是如此)。

在Android平台,scudo内部通过common.hmap函数通过mmap向系统申请内存并设置VMA名称,实现位于linux.cpp

void *map(void *Addr, uptr Size, UNUSED const char *Name, uptr Flags,
          UNUSED MapPlatformData *Data) {
  int MmapFlags = MAP_PRIVATE | MAP_ANONYMOUS;
  int MmapProt;
  if (Flags & MAP_NOACCESS) {
    MmapFlags |= MAP_NORESERVE;
    MmapProt = PROT_NONE;
  } else {
    MmapProt = PROT_READ | PROT_WRITE;
#if defined(__aarch64__) && defined(ANDROID_EXPERIMENTAL_MTE)
    if (Flags & MAP_MEMTAG)
      MmapProt |= PROT_MTE;
#endif
  }
  if (Addr) {
    // Currently no scenario for a noaccess mapping with a fixed address.
    DCHECK_EQ(Flags & MAP_NOACCESS, 0);
    MmapFlags |= MAP_FIXED;
  }
  void *P = mmap(Addr, Size, MmapProt, MmapFlags, -1, 0);
  if (P == MAP_FAILED) {
    if (!(Flags & MAP_ALLOWNOMEM) || errno != ENOMEM)
      dieOnMapUnmapError(errno == ENOMEM);
    return nullptr;
  }
#if SCUDO_ANDROID
  if (!(Flags & MAP_NOACCESS))
    prctl(ANDROID_PR_SET_VMA, ANDROID_PR_SET_VMA_ANON_NAME, P, Size, Name);
#endif
  return P;
}

而Android11开始,一些设备找不到带有"malloc"字符串的内存,正是因为scudo设置VMA名称不含"malloc",而是"scudo:primary""scudo:secondary"...。

在Android系统中,使用PR_SET_VMA_ANON_NAME设置内存区域名称,可以理解为传入的是字符串的内存地址,而不是字符串本身,也就是说,直接修改这个地址的字符串,就立刻可以在/proc/{pid}/maps中看到。

解决思路

回归到最开始的GG没有Ca内存的话题,如果要解决,最简单的方式就是直接修改进程libc.so.rodata的"scudo:"开头的字符串,改为"malloc",大概30行C语言代码,至于具体代码,其实我个人用不到GG,所以先不写了。