别摸鱼啦,说的就是你,学习编程从入门到放弃。掌握编程思维方式,远胜于死记硬背编程语法,不能再迷路了。
如何准确地读写内存数据, 想必是伙伴们比较关注的一个话题,尤其是某些对游戏外挂感兴趣的, 说到这里鸡贼的伙伴估计已经捕捉到了关键信息。本篇文章主要讲解如何读写内存数据,涉及到的进程、内存等操作系统级,只进行简要说明。底层原理的剖析,择期讲解,敬请关注。
操作系统进程模块
主流的操作系统windows、linux、类unix、mac等,都有进程模块,进程模块又是kernel和操作系统的核心模块之一,理解起来比较复杂,下面图示简要的说明。
图一 进程内存空间示意图
读写进程A "0X000A" 地址的内存数据,存在两种不同的情况:
进程A内部: 进程A内部就比较简单了,同一个进程中的所有线程共享进程的资源,但每个线程拥有独立的堆栈空间。
其他进程:进程B要想读写进程A内存,有多种方式,比如进程间通讯等,这些比较间接,实际上就是进程A内部读写,然后返回数据给进程B。 这里给出三种方式, 直接读写进程A的内存, 等的就是你
不同进程之间内存读写三种方式 ptrace、proc、syscall
1. ptrace
Linux中man ptrace可以查看相关文档
DESCRIPTION
The ptrace() system call provides a means by which one process (the "tracer") may observe and control the execution of another
process (the "tracee"), and examine and change the tracee's memory and registers. It is primarily used to implement breakpoint
debugging and system call tracing.
A tracee first needs to be attached to the tracer. Attachment and subsequent commands are per thread: in a multithreaded process,
every thread can be individually attached to a (potentially different) tracer, or left not attached and thus not debugged. ptrace开发文档,翻译过来就是,
ptrace()系统调用提供了一种方法,通过该方法,一个进程可以observe和control另一个进程的执行,检查和更改跟踪对象的内存和寄存器。它主要用于实现断点调试和系统调用跟踪。
ptrace 一次读写一个字的长度(32位操作系统 4字节、 64位操作系统 8字节)
,对于多字节数据,根据需要进行多次读写。
看到这里就明白了,ptrace可以observe和control其他线程的内存和寄存器,这是重点, 知道了用途,还需要知道用法
A tracee first needs to be attached to the tracer
The tracer will be notified at its next call to waitpid
The value of request determines the action to be performed:
...
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
Read a word at the address addr in the tracee's memory, returning the word as the result of the ptrace() call. Linux does not
have separate text and data address spaces, so these two requests are currently equivalent. (data is ignored; but see NOTES.)
PTRACE_POKETEXT, PTRACE_POKEDATA
Copy the word data to the address addr in the tracee's memory. As for PTRACE_PEEKTEXT and PTRACE_PEEKDATA, these two requests
are currently equivalent.
...
对上述官方文档进行了整理,翻译过来就是:
首先需要attached到另外的线程,然后调用 waitpid 来等待被跟踪进程的状态改变,最后根据 request 执行操作。
ptrace 函数原型如下
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
纸上得来终觉浅,绝知此事要躬行。知道了理论知识,下面就来学习ptrace如何读写内存。
ptrace 示例代码
进程A 提供要读写的内存
#include <iostream>
#include <unistd.h>
int main()
{
char *p = "hello word";
printf("p:%p\n", p);
printf("pid:%d\n", getpid());
while(true);
return 0;
}
进程B 读写内存数据代码
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_SIZE 1024
#define READONLY 1
union {
long long memDataInt;
char memDataBuff[MAX_SIZE];
} d {
.memDataInt = 0
};
static long ReadWProcAddrMem(const int pid, unsigned long long address, int flag)
{
if (pid == 0 || address == 0) return -1;
int waitPidStatus = 0;
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
fprintf(stderr, "Failed to attach process %d: %s\n", pid, strerror(errno));
return d.memDataInt = -1;
}
/* pause progress */
if (waitpid(pid, &waitPidStatus, 0) == -1 || !WIFSTOPPED(waitPidStatus)) {
fprintf(stderr, "Failed to wait process %d: %s\n", pid, strerror(errno));
goto DETACH_AND_RET;
}
errno = 0;
if (flag & READONLY) /* read memory */
d.memDataInt = ptrace(PTRACE_PEEKDATA, pid, address, 0);
else /* write memory */
d.memDataInt = ptrace(PTRACE_POKEDATA, pid, address, 8888);
if (errno != 0)
perror("peek | poke");
else
printf("read/write pid:%d memory:0x%02llX success [memDataInt:%lld][memDataBuff:%s]\n", pid, address, d.memDataInt, d.memDataBuff);
DETACH_AND_RET:
ptrace(PTRACE_DETACH, pid, NULL, NULL); /* free ptrace */
return d.memDataInt;
}
int main(int argc, char* argv[])
{
if(argc != 3) {
printf("Usage: %s <pid> <addr>\n", argv[0]);
exit(1);
}
pid_t pid = atoi(argv[1]);
unsigned long long addr = strtol(argv[2], NULL, 16);
ReadWProcAddrMem(pid, addr, 1);
}
2. proc
安卓系统是基于Linux kernel 实现的。 /proc 目录是一个虚拟文件系统,提供了对内核和运行中进程的访问。该目录下包含了许多以数字命名的子目录,每个子目录对应一个正在运行的进程。以下是 /proc 目录下常见的一些重要文件和子目录:
-
/proc/[pid]:每个运行中进程对应的子目录,其中 [pid] 是进程的 PID(进程标识符)
-
/proc/[pid]/maps(进程内存映射信息)
-
/proc/[pid]/mem(进程内存数据)
-
/proc/cpuinfo:包含了有关 CPU 的信息,如型号、频率、缓存等。
-
/proc/meminfo:提供了关于系统内存(RAM)使用情况的信息,如总内存、可用内存、缓存等。
-
/proc/loadavg:显示了系统的平均负载情况,通常包含 1 分钟、5 分钟和 15 分钟的负载平均值。
-
/proc/filesystems:列出了当前系统支持的文件系统类型。
-
/proc/version:显示了当前运行的内核版本信息。
-
/proc/cmdline:包含了内核启动时传递给内核的参数信息。
-
/proc/net:包含了与网络相关的信息,如 tcp, udp, icmp 等。
-
/proc/sys:包含了许多内核参数和系统设置的配置文件,可以通过读写这些文件来修改内核参数。
-
/proc/sysrq-trigger:通过向该文件写入特定字符,可以触发 SysRq 功能。
-
/proc/self:一个符号链接,指向当前进程自身的 /proc/[pid] 目录。
铁子们,是不是已经悟了,咱直接读取 /proc/[pid]/mem 获取进程内存数据,小编直接上代码。
proc示例代码
进程A 提供要读写的内存
#include <iostream>
#include <unistd.h>
int main()
{
char *p = "hello word";
printf("p:%p\n", p);
printf("pid:%d\n", getpid());
while(true);
return 0;
}
进程B 读取内存数据代码(只读)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 128
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <pid> <address>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
off_t addr = strtol(argv[2], NULL, 16);
char path[256] = {0};
sprintf(path, "/proc/%d/mem", pid);
int fd = open(path, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
if (lseek(fd, addr, SEEK_SET) == -1) {
perror("lseek");
close(fd);
return 1;
}
char buf[BUF_SIZE];
ssize_t nread = read(fd, buf, BUF_SIZE);
if (nread == -1) {
perror("read");
close(fd);
return 1;
}
for (int i = 0; i < nread; i++) {
printf("%c ", (unsigned char)buf[i]);
}
printf("\n");
close(fd);
return 0;
}
3. syscall
linux syscall是系统调用接口,提供了用户态和内核态的交互,syscall如何实现和调用,后续会有专题进行讲解。
syscall() 函数是一个用于调用 Linux kernel系统调用的函数。它的函数声明如下:参数 number 表示要调用的系统调用号,也就是内核中对应的系统调用函数。剩余的参数是可变参数,也就是具体的系统调用的参数。
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
long syscall(long number, ...);
#include <sys/uio.h>
ssize_t process_vm_readv(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
ssize_t process_vm_writev(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
syscall 读写进程内存数据
__NR_process_vm_read 是一个在 Linux 内核中定义的系统调用号
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#define BUFF_SIZE 128
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("Usage: %s <pid> <address>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
off_t addr = strtol(argv[2], NULL, 16);
char buffer[BUFF_SIZE] = {0};
struct iovec *local = (struct iovec*)malloc(sizeof(struct iovec));
struct iovec *remote = (struct iovec*)malloc(sizeof(struct iovec));
local->iov_base = buffer;
local->iov_len = BUFF_SIZE;
remote->iov_base = (void *)addr;
remote->iov_len = BUFF_SIZE;
/* process_vm_xxx */
long int res;
if ((res = syscall(__NR_process_vm_readv,
pid,
local,
1,
remote,
1,
0)) == -1) {
fprintf(stderr, "Failed to syscall %d: %s\n", __NR_process_vm_readv, strerror(errno));
goto FREE;
}
for (int i = 0; i < BUFF_SIZE; i++) {
printf("%c ", (unsigned char)buffer[i]);
}
FREE:
free(local); free(remote);
return 0;
}
写在文末,安卓系统是基于linux kernel研发的,上述几种方式,可以读写修改安卓手游内存哦,仅供学习交流, 不提供游戏外挂。
创作不易,动动发财的小手点个关注再走呗