读取数据
读取数据的前提是知道目标app的pid,在上一篇中已经做到了动态获取pid了。
现在通过syscall的方式读取数据。这里用一个飞机大战来学习。
系统调用syscall读取内存
Linux提供了syscall的api来读取,通过__NR_process_vm_readv调用号。
__NR_process_vm_readv 是一个系统调用号,在Linux中,它对应的系统调用是 process_vm_readv。
process_vm_readv 是一个非常强大的系统调用,它能够在不同的进程之间直接传输内存。这意味着一个进程可以直接读取另一个进程的内存区域,而不需要进行任何的数据复制。这对于那些需要处理大量数据并且希望尽可能地减少系统开销的应用程序来说非常有用。
具体来说,process_vm_readv 需要5个参数:
第一个参数是要读取数据的进程的PID。
第二个参数是一个指向 struct iovec 的指针,它描述了要读取的内存区域。
第三个参数是第二个参数中 struct iovec 的数量。
第四个参数同样是一个指向 struct iovec 的指针,但它描述的是数据读取到哪里。
第五个参数是第四个参数中 struct iovec 的数量。
读取内存实践
首先上面syscall提到的pid我们已经拿到了,第二个参数要读取的内存区域,这个怎么理解呢,app申请了一大块内存,某个数据会存在这一块内存中的某个位置,这个位置可以理解为他的地址,假设我们知道了地址,就可以去读取了,读取出来的数据和我们看到的是不是一致,如果一致就表示我们代码和想法都可行的。
这里用一个飞机大战来学习。
需要配置的工具是gg,游戏开始之后,gg搜索0,消灭敌人之后,再次搜索分数,再次消灭敌人,改善搜索分数。最后剩下的和显示分数对应的就是目标值了。分数左边就是存储分数的地址。
这是分数的数据。
下面是gg的搜索数据:
13168330就是地址,不过这个地址是16进制的,应该是0x13168330。
写代码的时候记得加上0x就行。
开始使用syscall读取这个地址。上面函数的描述需要的数据我们已经具备了。
pid,读取的地址,直到了地址还不够,地址上面读多少?也就是要知道我们读的数据他占用多大,就好比你用挖掘机挖地,雇主说你在门口开始(这个就是地址),向前挖8米,挖多了就会破坏别人的菜园。这个8米就是读多少。
在我的手机上面Android 12,皮鞋3,这个系统和处理器64位的,读取D类型是8位。你当然也可以试试4位,跑一跑就行。
所以读取的代码如下:
int read_dword(int pid, long long addr) {
struct iovec local, remote;
// 读取到数据保存到这个data中
int data;
local.iov_base = &data;
// 长度8位
local.iov_len = 8;
// 读取模板地址
remote.iov_base = (void *) addr;
// 读取长度8位
remote.iov_len = 8;
// 下面模板
long int call_result = syscall(__NR_process_vm_readv,
pid,
&local,
1,
&remote,
1,
0);
return data;
}
通过动态获取pid,和gg上面提供的地址:
long long addr = 0x13168330;
int sun = read_dword(pid, addr);
printf("分数: %d\n", sun);
读取到的分数和游戏显示一致。
杀死游戏,重新开一局。重新搜索分数。得到分数对应的地址:13f4d4c0
第一次读取后:
多消灭敌人之后再次读取:
这样可以确认代码是可行的。读取其他数据类型同意的,后面遇到会给出代码。
#include <stdio.h>
#include <unistd.h>
#include <string>
#include <dirent.h>
#include <sys/uio.h>
#include <sys/syscall.h>
/**
* 传递包名
* @param name 目标包名
* @return 返回app的pid
*/
pid_t get_game_pid(char *name) {
FILE *fp;
// 储存拿到的pid
pid_t pid;
// 命令拼接
char cmd[0x100] = "pidof %s";
char exec[128] = {0};
// 把pidof 和包名拼接在一起组成命令
sprintf(exec, cmd, name);
// popen执行命令,等同于终端执行
fp = popen(exec, "r");
// 拿到结构后储存到pid变量中
fscanf(fp, "%d", &pid);
pclose(fp);
return pid;
}
int getPID(const char *packageName) {
// 默认pid是-1,如果返回-1就是读取失败
int id = -1;
DIR *dir;
FILE *fp;
char filename[64];
char cmdline[64];
struct dirent *entry;
// 打开proc目录
dir = opendir("/proc");
while ((entry = readdir(dir)) != NULL) {
// 目录名字就是pid,转int
id = atoi(entry->d_name);
if (id != 0) {
// 把目录和cmdline拼接一起读取
sprintf(filename, "/proc/%d/cmdline", id);
fp = fopen(filename, "r");
if (fp) {
fgets(cmdline, sizeof(cmdline), fp);
fclose(fp);
// 判断传递进来的包名和cmdline中字符串是否一样
if (strcmp(packageName, cmdline) == 0) {
// 如果一致,上面目录就是pid了
return id;
}
}
}
}
closedir(dir);
return -1;
}
int read_dword(int pid, long long addr) {
struct iovec local, remote;
// 读取到数据保存到这个data中
int data;
local.iov_base = &data;
// 长度8位
local.iov_len = 8;
// 读取模板地址
remote.iov_base = (void *) addr;
// 读取长度8位
remote.iov_len = 8;
// 下面模板
long int call_result = syscall(__NR_process_vm_readv,
pid,
&local,
1,
&remote,
1,
0);
return data;
}
int main() {
printf("读取游戏的pid\n");
pid_t game_pid = get_game_pid("com.herocraft.game.free.mig29");
printf("pid: %d\n", game_pid);
int pid = getPID("com.herocraft.game.free.mig29");
printf("pid_proc: %d\n", pid);
long long addr = 0x13f4d4c0;
int sun = read_dword(pid, addr);
printf("分数: %d\n", sun);
return 0;
}
总结
1、利用上一篇动态获取pid来配合syscall读取内存数据。
2、使用gg拿到数据的地址。
3、使用syscall来读取地址,对比真实的数据。
4、重启游戏后重复操作,验证数据。