Rust 实战练习 - 8. 内存,ASM,外挂 【重磅】
目标:
- C写一个Demo版本的游戏
- 由浅入深,了解外挂原理
- Linux/Android下实现内存读取
- ptrace实现内存修改(依赖第三方库)
先准备一个C写的小游戏
#include <stdio.h>
#include <string.h>struct Role
{float pos_x; // 位置xfloat pos_y;float pos_z;int level; // 等级long money; // 金钱
};struct Account
{char name[20]; // 名字long ID; // IDstruct Role *role; // 多个角色信息
};void decMoney(struct Role* r){r->money -= rand()%100;
}void main(){printf("GameBoy tester!\r\n");struct Account ac;strcpy(ac.name, "我是张三啊");ac.ID = 20240318;// rolesstruct Role *rl = (struct Role*)malloc(sizeof(struct Role)*4);ac.role = rl;for(int i=0;i<4;i++){rl[i].level = i*2+10;rl[i].money = 5000;rl[i].pos_x = i*1000;rl[i].pos_y = i*1100;rl[i].pos_z = i*1200;}while (1){printf("\r\nInput [l,m,p,c] to change value:");char input = getchar();switch (input){case 'l':for(int i=0;i<4;i++){rl[i].level += rand()%50/10;}break;case 'm':for(int i=0;i<4;i++){rl[i].money += rand()%20;}break;case 'p':for(int i=0;i<4;i++){rl[i].pos_x += rand()%100 * 0.1;rl[i].pos_y += rand()%20 * 0.2;rl[i].pos_z += rand()%10;}break;case 'c':for(int i=0;i<4;i++){decMoney(&rl[i]);}break;default:break;}// printprintf("\r\nAccount: [%ld] => %s\r\n", ac.ID, ac.name);for(int i=0;i<4;i++){printf("Role[%d] => Level:%d, Money: %ld, Pos:[%.2f,%.2f,%.2f]\r\n", i, rl[i].level, rl[i].money, rl[i].pos_x,rl[i].pos_y,rl[i].pos_z);}printf("Cheat: Account [0x%lX], ID [0x%lX], Name [0x%lX], role [%lX]\r\n", &ac, &(ac.ID), ac.name, &(ac.role));printf("Cheat: Role [0x%lX], pos_x [0x%lX], level [0x%lX], money [%lX]\r\n", rl, &(rl[0].pos_x), &(rl[0].level), &(rl[0].money));printf("Cheat: decMoney [0x%lX], main: {0x%lX}\r\n",decMoney, main); }
}// gcc -o gamebox main.c
// ./gamebox
Input [l,m,p,c] to change value:
只要输入 l/m/p
/c 就可以随机改变其中的等级,金钱,位置信息。其中输入c会调用函数,随机减少角色的金钱。
Rust修改gamebox
有个出名的技术叫hook,还有一个技术叫修改内存。这也就是简单的游戏外挂范围的技术。这里我们使用rust去模拟一下,修改上面用C写的gamebox。
在Linux下如何搜索内存找到我们需要的数据,不在这里讨论,我们根据gamebox提供的地址,直接定位。一定要自己找,可以使用 PINCE。(类似CE)
备注:ceserver + wine + CE GUI 可以在Linux下进行搜索。
首先,反推几个重要信息的地址关系: (假设字节对齐)
[BaseAddr+0] => name 首地址, char[20]
[BaseAddr+20] => ID, long
[BaseAddr+20+8] => Role的首地址一个, Role size=3*4+4+8=24Role = [BaseAddr+20+8]
[[BaseAddr+20+8]+24*i +0] => Role.pos_x, float
[[BaseAddr+20+8]+24*i +4] => Role.pos_y, float
[[BaseAddr+20+8]+24*i +8] => Role.pos_z, float
[[BaseAddr+20+8]+24*i +12] => Role.level, int
[[BaseAddr+20+8]+24*i +16] => Role.money, long
虽然这个地址信息每次启动时会变化,但是他们的关系应该是固定的。
实际上,字节没有对齐,和我们上面预期有差异:
BaseAddr = 0x7FFEE5125640
[BaseAddr+0] => name 首地址, char[20] => 0x7FFEE5125640 实际占用是0x18=24
[BaseAddr+24] => ID, long => 0x7FFEE5125658
[BaseAddr+24+8] => Role的首地址一个, Role size=3*4+4+8=24 => 0x7FFEE5125660 Role = [BaseAddr+24+8] = [0x7FFEE5125660] = 0x5585F6BFE6B0
[[BaseAddr+24+8]+24*i +0] => Role.pos_x, float => 0x5585F6BFE6B0
[[BaseAddr+24+8]+24*i +4] => Role.pos_y, float
[[BaseAddr+24+8]+24*i +8] => Role.pos_z, float
[[BaseAddr+24+8]+24*i +12] => Role.level, int => 0x5585F6BFE6BC
[[BaseAddr+24+8]+24*i +16] => Role.money, long => 0x5585F6BFE6C0
准备
https://blog.csdn.net/guojin08/article/details/9454467
https://blog.csdn.net/hhhlizhao/article/details/77930009
https://zhuanlan.zhihu.com/p/348171413
https://zhuanlan.zhihu.com/p/674139021
https://www.52pojie.cn/thread-1355860-1-1.html
Linux上一切皆文件,只要有权限,读写其他程序的内存很简单。
- sudo权限,强制读写
/proc/ID/mem
- 自身为内核空间,直接读写mem
- 能获取到proc的mem的物理地址,读写物理内存
- dbg相关函数
如果想要注入,修改代码,需要使用ptrace一类的方式。
https://www.52pojie.cn/thread-1568457-1-1.html
Linux和Android的方法都是类似的.
简易内存修改
1.直接读取内存固定位置
读文件方式实现
andy@andy-pc:/proc/22093$ cat maps
5593869d4000-5593869d5000 r--p 00000000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
5593869d5000-5593869d6000 r-xp 00001000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
5593869d6000-5593869d7000 r--p 00002000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
5593869d7000-5593869d8000 r--p 00002000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
5593869d8000-5593869d9000 rw-p 00003000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
559386d8f000-559386db0000 rw-p 00000000 00:00 0 [heap]
7f914db19000-7f914db1c000 rw-p 00000000 00:00 0
7f914db1c000-7f914db44000 r--p 00000000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914db44000-7f914dcd9000 r-xp 00028000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914dcd9000-7f914dd31000 r--p 001bd000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914dd31000-7f914dd32000 ---p 00215000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914dd32000-7f914dd36000 r--p 00215000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914dd36000-7f914dd38000 rw-p 00219000 08:12 12848718 /usr/lib/x86_64-linux-gnu/libc.so.6
7f914dd38000-7f914dd45000 rw-p 00000000 00:00 0
7f914dd5e000-7f914dd60000 rw-p 00000000 00:00 0
7f914dd60000-7f914dd62000 r--p 00000000 08:12 12848709 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f914dd62000-7f914dd8c000 r-xp 00002000 08:12 12848709 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f914dd8c000-7f914dd97000 r--p 0002c000 08:12 12848709 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f914dd98000-7f914dd9a000 r--p 00037000 08:12 12848709 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f914dd9a000-7f914dd9c000 rw-p 00039000 08:12 12848709 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffed5856000-7ffed5877000 rw-p 00000000 00:00 0 [stack]
7ffed596e000-7ffed5972000 r--p 00000000 00:00 0 [vvar]
7ffed5972000-7ffed5974000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]# gamebox
Account: [20240318] => 我是张三啊
Role[0] => Level:14, Money: 4926, Pos:[9.00,3.80,3.00]
Role[1] => Level:15, Money: 4915, Pos:[1002.60,1100.00,1206.00]
Role[2] => Level:17, Money: 4925, Pos:[2007.20,2203.20,2401.00]
Role[3] => Level:20, Money: 4992, Pos:[3006.80,3301.40,3609.00]
Cheat: Account [0x7FFF90D18540], ID [0x7FFF90D18558], Name [0x7FFF90D18540], role [7FFF90D18560]
Cheat: Role [0x558CE6B4F6B0], pos_x [0x558CE6B4F6B0], level [0x558CE6B4F6BC], money [558CE6B4F6C0]
Cheat: decMoney [0x558CE63E11C9]
根据地址信息,数据主要存在 heap和stack上,函数存在代码段。
use std::{ffi::{CStr, OsString},fs::File,io::Read,os::unix::fs::FileExt,path, u8,
};// 64 bit program
fn main() {let pid = find_id_by_name("gamebox");println!("gamebox pid: {:?}", pid);if pid.len() > 0 {let memf = format!("/proc/{}/mem", pid.first().unwrap());println!("gamebox mem: {}", memf);// 需要root权限if let Ok(f) = &File::open(memf) {let base_addr:u64 = 0x7FFF90D18540;// accountlet id = read_at_mem(f, base_addr + 24, 8);let name = read_at_mem(f, base_addr + 0, 20);let role_addr = read_at_mem(f, base_addr + 24 + 8, 8);let name2 = unsafe { CStr::from_ptr(name.as_ptr() as *const i8) };let role_addr2 = u64::from_le_bytes(role_addr.try_into().unwrap());println!("Account => ID: {}, Name: {}, role addr: 0x{:#X}",u64::from_le_bytes(id.try_into().unwrap()),name2.to_str().unwrap(),role_addr2,);// rolefor i in 0..4 {let level = read_at_mem(f, role_addr2 + 24*i + 12, 4);let money = read_at_mem(f, role_addr2 + 24*i + 16, 8);let pos_x = read_at_mem(f, role_addr2 + 24*i + 0, 4);let pos_y = read_at_mem(f, role_addr2 + 24*i + 4, 4);let pos_z = read_at_mem(f, role_addr2 + 24*i + 8, 4);println!(" Role[{}] => Level: {}, Money: {}, Pos: [{:.2}, {:.2}, {:.2}]",i,u32::from_le_bytes(level.try_into().unwrap()),u64::from_le_bytes(money.try_into().unwrap()),f32::from_le_bytes(pos_x.try_into().unwrap()),f32::from_le_bytes(pos_y.try_into().unwrap()),f32::from_le_bytes(pos_z.try_into().unwrap()),);}} else {println!("need root permission!");}}
}// 读取指定大小的内存数据
fn read_at_mem(f: &File, addr: u64, sz: usize) -> Vec<u8> {let mut buf = Vec::new();buf.resize(sz, 0);f.read_at(&mut buf, addr).unwrap();return buf;
}// 忽略大小写进行进程查找
fn find_id_by_name(name: &str) -> Vec<String> {// 遍历和读取 /proc/xxx/commlet root = path::Path::new("/proc/");let mut pids: Vec<_> = Vec::new();for sub in root.read_dir().unwrap() {if let Ok(id_dir) = sub {let fp = id_dir.path();if fp.is_dir() {let comm = fp.join("comm");if comm.exists() {let txt = file_read_content(comm.to_str().unwrap());// println!("try file: {:?}, {}", comm, txt);if txt.to_ascii_lowercase() == name.to_ascii_lowercase() {let pid = OsString::from(fp.file_name().unwrap()).into_string().unwrap();pids.push(pid);}}}}}pids
}fn file_read_content(filepath: &str) -> String {let mut txt = String::new();if let Ok(mut f) = File::open(filepath) {f.read_to_string(&mut txt).unwrap();}// 默认内容有换行txt = txt.trim().to_string();return txt;
}
2.寻找基地址,真正外挂
基地址的本质是全局变量,所以我们原本的代码无法实现。
修改一下, 重新编译:
struct Account ac;void main(){printf("GameBoy tester!\r\n"); // struct Account ac; // 移动这个到全局变量strcpy(ac.name, "我是张三啊");ac.ID = 20240318;...
}
另外,程序每次加载到内存中并不一定是固定地址的。
- 在Windows系统中,xp,win7时代是这样,每次固定加载在固定的地址上,所以每次程序运行的地址都是固定的。
- 到了后来,windows系统和Linux都使用了动态基地址的flag, 每次的初始地址不固定,所以寻找基地址变得麻烦。
思路:
- 程序加载到内存时,有导出表或者maps信息,可以看到code的初始地址。
- main函数相对程序初始地址一般是固定的,与第一步地址一起计算可以得到main的动态地址。
- 程序全局变量等的地址,相对main是固定的,所以就可以动态计算出来当前变量的地址。(即动态的基地址)
Linux上的获取基地址方法:
- 启动gamebox得到全局变量偏移信息
Account: [20240318] => 我是张三啊
Role[0] => Level:10, Money: 5000, Pos:[0.00,0.00,0.00]
Role[1] => Level:12, Money: 5000, Pos:[1000.00,1100.00,1200.00]
Role[2] => Level:14, Money: 5000, Pos:[2000.00,2200.00,2400.00]
Role[3] => Level:16, Money: 5000, Pos:[3000.00,3300.00,3600.00]
Cheat: Account [0x55AF86AE7040], ID [0x55AF86AE7058], Name [0x55AF86AE7040], role [55AF86AE7060]
Cheat: Role [0x55AF87D676B0], pos_x [0x55AF87D676B0], level [0x55AF87D676BC], money [55AF87D676C0]
Cheat: decMoney [0x55AF86AE41C9], main: {0x55AF86AE421E}
所以,Account Offset = Account[0x55AF86AE7040] - main[0x55AF86AE421E] = 0x2E22
- 通过gamebox进程maps信息获取main偏移
55af86ae3000-55af86ae4000 r--p 00000000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
55af86ae4000-55af86ae5000 r-xp 00001000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
55af86ae5000-55af86ae6000 r--p 00002000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
55af86ae6000-55af86ae7000 r--p 00002000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
55af86ae7000-55af86ae8000 rw-p 00003000 08:12 6160449 /home/andy/Work/rs_prj/d8/C/gamebox
55af87d67000-55af87d88000 rw-p 00000000 00:00 0 [heap]
所以,main Offset = main[0x55AF86AE421E] - gamebox[0x55af86ae3000] = 0x121E
- 计算基地址偏移
BaseAddr = gamebox 基地址 + 0x121E + 0x2E22
- Code
use std::{ffi::{CStr, OsString},fs::File,io::{BufRead, BufReader, Cursor, Read},os::unix::fs::FileExt,path, u8,
};// 64 bit program
fn main() {let pids = find_id_by_name("gamebox");println!("gamebox pid: {:?}", pids);if pids.len() > 0 {let pid = pids.first().unwrap();let memf = format!("/proc/{}/mem", pid);let mut base_addr:u64 = process_get_base_addr(pid);if base_addr == 0 {println!("gamebox find base addr failed: {}", pid);return;}base_addr += 0x121E + 0x2E22; // Account ac 全局变量偏移println!("gamebox mem: {}, base addr: {:X}", memf, base_addr);// 需要root权限if let Ok(f) = &File::open(memf) { // accountlet id = read_at_mem(f, base_addr + 24, 8);let name = read_at_mem(f, base_addr + 0, 20);let role_addr = read_at_mem(f, base_addr + 24 + 8, 8);let name2 = unsafe { CStr::from_ptr(name.as_ptr() as *const i8) };let role_addr2 = u64::from_le_bytes(role_addr.try_into().unwrap());println!("Account => ID: {}, Name: {}, role addr: 0x{:#X}",u64::from_le_bytes(id.try_into().unwrap()),name2.to_str().unwrap(),role_addr2,);// rolefor i in 0..4 {let level = read_at_mem(f, role_addr2 + 24*i + 12, 4);let money = read_at_mem(f, role_addr2 + 24*i + 16, 8);let pos_x = read_at_mem(f, role_addr2 + 24*i + 0, 4);let pos_y = read_at_mem(f, role_addr2 + 24*i + 4, 4);let pos_z = read_at_mem(f, role_addr2 + 24*i + 8, 4);println!("\tRole[{}] => Level: {}, Money: {}, Pos: [{:.2}, {:.2}, {:.2}]",i,u32::from_le_bytes(level.try_into().unwrap()),u64::from_le_bytes(money.try_into().unwrap()),f32::from_le_bytes(pos_x.try_into().unwrap()),f32::from_le_bytes(pos_y.try_into().unwrap()),f32::from_le_bytes(pos_z.try_into().unwrap()),);}} else {println!("need root permission!");}}
}fn read_at_mem(f: &File, addr: u64, sz: usize) -> Vec<u8> {let mut buf = Vec::new();buf.resize(sz, 0);f.read_at(&mut buf, addr).unwrap();return buf;
}// 忽略大小写进行进程查找
fn find_id_by_name(name: &str) -> Vec<String> {// 遍历和读取 /proc/xxx/commlet root = path::Path::new("/proc/");let mut pids: Vec<_> = Vec::new();for sub in root.read_dir().unwrap() {if let Ok(id_dir) = sub {let fp = id_dir.path();if fp.is_dir() {let comm = fp.join("comm");if comm.exists() {let txt = file_read_content(comm.to_str().unwrap());// println!("try file: {:?}, {}", comm, txt);if txt.to_ascii_lowercase() == name.to_ascii_lowercase() {let pid = OsString::from(fp.file_name().unwrap()).into_string().unwrap();pids.push(pid);}}}}}pids
}fn file_read_content(filepath: &str) -> String {let mut txt = String::new();if let Ok(mut f) = File::open(filepath) {f.read_to_string(&mut txt).unwrap();}// 默认内容有换行txt = txt.trim().to_string();return txt;
}// 通过 /proc/pid/maps 查找 program 地址
fn process_get_base_addr(pid:&str) -> u64{let mut addr = 0u64;if let Ok(f) = File::open(format!("/proc/{}/maps", pid)){let mut txt = String::new();let mut br = BufReader::new(f);br.read_line(&mut txt).unwrap();//55af86ae3000-55af86ae4000 r--p 00000000 08:12 6160449 /xx/gamebox if let Some(pos) = txt.find('-') {addr = u64::from_str_radix(&txt[..pos], 16).unwrap();}}return addr;
}
cargo build
sudo ./target/debug/d8gamebox pid: ["7586"]
gamebox mem: /proc/7586/mem, base addr: 55AF86AE7040
Account => ID: 20240318, Name: 我是张三啊, role addr: 0x0x55AF87D676B0Role[0] => Level: 10, Money: 5000, Pos: [0.00, 0.00, 0.00]Role[1] => Level: 12, Money: 5000, Pos: [1000.00, 1100.00, 1200.00]Role[2] => Level: 14, Money: 5000, Pos: [2000.00, 2200.00, 2400.00]Role[3] => Level: 16, Money: 5000, Pos: [3000.00, 3300.00, 3600.00]
然后,尝试关闭gamebox,然后再重启,继续使用程序读取一下,依然有效。
到此,一个只读取gamebox的外挂做好了。
3.修改程序调用逻辑
如果,我们想把代码中decMoney
函数的减少钱的逻辑,改成增加钱的逻辑怎么办?
- root权限,强制改写 decMoney 函数的汇编代码,可以实现简单功能
- root权限,在内存中新写一个 函数, 替代decMoney,实现任意功能。这个难度很高。
我们做一个简单的版本:
// decMoney 函数汇编代码
.text:00000000000011C9 public decMoney
.text:00000000000011C9 decMoney proc near ; CODE XREF: main+4E3↓p
.text:00000000000011C9 ; DATA XREF: main+68E↓o
.text:00000000000011C9
.text:00000000000011C9 var_8= qword ptr -8
.text:00000000000011C9
.text:00000000000011C9 ; __unwind {
.text:00000000000011C9 F3 0F 1E FA endbr64
.text:00000000000011CD 55 push rbp
.text:00000000000011CE 48 89 E5 mov rbp, rsp
.text:00000000000011D1 48 83 EC 10 sub rsp, 10h
.text:00000000000011D5 48 89 7D F8 mov [rbp+var_8], rdi
.text:00000000000011D9 B8 00 00 00 00 mov eax, 0
.text:00000000000011DE E8 ED FE FF FF call _rand
.text:00000000000011DE
.text:00000000000011E3 48 63 D0 movsxd rdx, eax
.text:00000000000011E6 48 69 D2 1F 85 EB 51 imul rdx, 51EB851Fh
.text:00000000000011ED 48 C1 EA 20 shr rdx, 20h
.text:00000000000011F1 C1 FA 05 sar edx, 5
.text:00000000000011F4 89 C1 mov ecx, eax
.text:00000000000011F6 C1 F9 1F sar ecx, 1Fh
.text:00000000000011F9 29 CA sub edx, ecx
.text:00000000000011FB 6B CA 64 imul ecx, edx, 64h ; 'd'
.text:00000000000011FE 29 C8 sub eax, ecx
.text:0000000000001200 89 C2 mov edx, eax
.text:0000000000001202 48 8B 45 F8 mov rax, [rbp+var_8]
.text:0000000000001206 48 8B 40 10 mov rax, [rax+10h]
.text:000000000000120A 48 63 CA movsxd rcx, edx
.text:000000000000120D 48 29 C8 sub rax, rcx ; 核心是这里减法处理
.text:0000000000001210 48 89 C2 mov rdx, rax ; v1
.text:0000000000001213 48 8B 45 F8 mov rax, [rbp+var_8]
.text:0000000000001217 48 89 50 10 mov [rax+10h], rdx ; 最终v1赋值
.text:000000000000121B 90 nop
.text:000000000000121C C9 leave
.text:000000000000121D C3 retn
.text:000000000000121D ; } // starts at 11C9
.text:000000000000121D
.text:000000000000121D decMoney endp// 反汇编的C代码
__int64 __fastcall decMoney(__int64 a1)
{__int64 v1; // rdx__int64 result; // raxv1 = *(_QWORD *)(a1 + 16) - rand() % 100; // 需要修改这里的减法操作result = a1;*(_QWORD *)(a1 + 16) = v1;return result;
}
函数 DecMoney 的偏移为 00000000000011C9,sub rax, rcx
的偏移为 000000000000120D。offset=0x44.
; https://shell-storm.org/online/Online-Assembler-and-Disassembler/
sub rax, rcx ; 48 29 c8
add rax, rcx ; 48 01 c8
所以,修改 函数地址加偏移 0x44 + 1
的值, 从 29 改为 01 就可以啦。
fn_ofset = 0x55AF86AE41C9 − 0x55AF86AE421E = -0x55
说明函数地址在main函数地址之前。
fn_addr = program + 0x121E - 0x55
理论代码如下:
use std::{ffi::{CStr, OsString},fs::File,io::{self, BufRead, BufReader, Cursor, Read},os::unix::fs::FileExt,path, u8,
};// 64 bit program
fn main() {let pids = find_id_by_name("gamebox");println!("gamebox pid: {:?}", pids);if pids.len() > 0 {let pid = pids.first().unwrap();let memf = format!("/proc/{}/mem", pid);let mut fn_addr = 0u64;let mut base_addr:u64 = process_get_base_addr(pid);if base_addr == 0 {println!("gamebox find base addr failed: {}", pid);return;}fn_addr = base_addr + 0x121E - 0x55; // decMoney 地址base_addr += 0x121E + 0x2E22; // Account ac 全局变量偏移println!("gamebox mem: {}, base addr: {:X}", memf, base_addr);// 需要root权限if let Ok(f) = &File::options().write(true).open(memf) { // accountlet id = read_at_mem(f, base_addr + 24, 8);let name = read_at_mem(f, base_addr + 0, 20);let role_addr = read_at_mem(f, base_addr + 24 + 8, 8);let name2 = unsafe { CStr::from_ptr(name.as_ptr() as *const i8) };let role_addr2 = u64::from_le_bytes(role_addr.try_into().unwrap());println!("Account => ID: {}, Name: {}, role addr: 0x{:#X}",u64::from_le_bytes(id.try_into().unwrap()),name2.to_str().unwrap(),role_addr2,);// rolefor i in 0..4 {let level = read_at_mem(f, role_addr2 + 24*i + 12, 4);let money = read_at_mem(f, role_addr2 + 24*i + 16, 8);let pos_x = read_at_mem(f, role_addr2 + 24*i + 0, 4);let pos_y = read_at_mem(f, role_addr2 + 24*i + 4, 4);let pos_z = read_at_mem(f, role_addr2 + 24*i + 8, 4);println!("\tRole[{}] => Level: {}, Money: {}, Pos: [{:.2}, {:.2}, {:.2}]",i,u32::from_le_bytes(level.try_into().unwrap()),u64::from_le_bytes(money.try_into().unwrap()),f32::from_le_bytes(pos_x.try_into().unwrap()),f32::from_le_bytes(pos_y.try_into().unwrap()),f32::from_le_bytes(pos_z.try_into().unwrap()),);}let mut buf = String::new();print!("Input any key to modify decMoney function ....");io::stdin().read_line(&mut buf).unwrap();// modify function decMoneylet asm_addr = fn_addr + 0x45; // sub rax, rcx => add rax, rcxlet asm = [0x01u8,];f.write_at(&asm, asm_addr).unwrap();} else {println!("need root permission!");}}
}fn read_at_mem(f: &File, addr: u64, sz: usize) -> Vec<u8> {let mut buf = Vec::new();buf.resize(sz, 0);f.read_at(&mut buf, addr).unwrap();return buf;
}// 忽略大小写进行进程查找
fn find_id_by_name(name: &str) -> Vec<String> {// 遍历和读取 /proc/xxx/commlet root = path::Path::new("/proc/");let mut pids: Vec<_> = Vec::new();for sub in root.read_dir().unwrap() {if let Ok(id_dir) = sub {let fp = id_dir.path();if fp.is_dir() {let comm = fp.join("comm");if comm.exists() {let txt = file_read_content(comm.to_str().unwrap());// println!("try file: {:?}, {}", comm, txt);if txt.to_ascii_lowercase() == name.to_ascii_lowercase() {let pid = OsString::from(fp.file_name().unwrap()).into_string().unwrap();pids.push(pid);}}}}}pids
}fn file_read_content(filepath: &str) -> String {let mut txt = String::new();if let Ok(mut f) = File::open(filepath) {f.read_to_string(&mut txt).unwrap();}// 默认内容有换行txt = txt.trim().to_string();return txt;
}// 通过 /proc/pid/maps 查找 program 地址
fn process_get_base_addr(pid:&str) -> u64{let mut addr = 0u64;if let Ok(f) = File::open(format!("/proc/{}/maps", pid)){let mut txt = String::new();let mut br = BufReader::new(f);br.read_line(&mut txt).unwrap();//55af86ae3000-55af86ae4000 r--p 00000000 08:12 6160449 /xx/gamebox if let Some(pos) = txt.find('-') {addr = u64::from_str_radix(&txt[..pos], 16).unwrap();}}return addr;
}
看起来似乎可行,实际执行发现,即便是root权限也无法直接修改其他进程的内存信息。
其他思路有:
- ptrace 接口,dbg方式修改
- 内核空间修改
- 能找到对应的物理内存地址,修改物理内存
上面几种方法,只有第一种难度最低,后续研究这种。
4.ptrace 版本
ptrace 提供了一种机制使得父进程可以观察和控制子进程的执行过程,ptrace 还可以检查和修改子进程的可执行文件在内存中的image及子进程所使用的寄存器中的值。通常来说,主要用于实现对进程插入断点和跟踪子进程的系统调用。
我们在/proc/xxx/mem
可以root读取,但是写入失败。但是ptrace可以实现这个写入功能。
long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);
/*
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个LONG长度数据,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个LONG长度数据。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个LONG长度数据。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个LONG长度数据,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH,追踪指定pid的进程
PTRACE_DETACH, 结束追踪
*/// ptrace Demo
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
int main(int argc, char* argv[])
{pid_t attack_pid = -1;long val = 66;if (argc < 2 || argv[1] <= 0){printf("usage: ./main pid(pid > 0)\n");return 0;}attack_pid = strtoul(argv[1], 0, 10);if (ptrace(PTRACE_ATTACH, attack_pid, NULL, NULL) < 0){printf("attach failed\n");return 0;}//读取数据printf("global1 %d\n", ptrace(PTRACE_PEEKDATA , attack_pid, (void*)0x804a028, NULL));printf("stack_var %d\n", ptrace(PTRACE_PEEKDATA , attack_pid, (void*)0xbfa4195c, NULL));//修改数据ptrace(PTRACE_POKEDATA , attack_pid, (void*)0x804a028, val);ptrace(PTRACE_POKEDATA , attack_pid, (void*)0xbfa4195c, val);ptrace (PTRACE_DETACH, attack_pid, NULL, NULL);waitpid(attack_pid, NULL, WUNTRACED);return 0;
}//main.c被改写的进程
#include <stdio.h>int global1 = 11; // int main(void)
{long stack_var = 10;char c = 'a';while(1){printf("global1 addrss 0x%lx, global1=%d\n", &global1, global1);printf("stack_var addrss 0x%lx, stack_var=%d\n", &stack_var, stack_var);scanf("%c", &c);getchar();if (c != 'c'){break;}}return 0;
}
https://dev59.com/unix/s3VD5IYBdhLWcg3wWaRh
完整的Rust版本的ptrace代码如下:
use std::{ffi::{c_void, CStr, OsString},fs::File,io::{self, BufRead, BufReader, Read, Write},os::unix::fs::FileExt,path, u8,
};use nix::{sys::{ptrace, wait::waitpid}, unistd::Pid};// 64 bit program
fn main() {let pids = find_id_by_name("gamebox");println!("gamebox pid: {:?}", pids);if pids.len() > 0 {let pid = pids.first().unwrap();let mut fn_addr = 0u64;let mut base_addr: u64 = process_get_base_addr(pid);if base_addr == 0 {println!("gamebox find base addr failed: {}", pid);return;}fn_addr = base_addr + 0x121E - 0x55; // decMoney 地址base_addr += 0x121E + 0x2E22; // Account ac 全局变量偏移println!("gamebox base addr: {:#X}, fn addr: {:#X}",base_addr, fn_addr);// 需要root权限let ppid = Pid::from_raw(i32::from_str_radix(pid, 10).unwrap());if let Ok(_) = ptrace::attach(ppid) {waitpid(ppid, None).unwrap();// accountlet id = ptrace_read_at(ppid, base_addr + 24, 8);let name = ptrace_read_at(ppid, base_addr + 0, 20);let role_addr = ptrace_read_at(ppid, base_addr + 24 + 8, 8);let name2 = unsafe { CStr::from_ptr(name.as_ptr() as *const i8) };let role_addr2 = u64::from_le_bytes(role_addr.try_into().unwrap());println!("Account => ID: {}, Name: {:?}, role addr: 0x{:#X}",u64::from_le_bytes(id.try_into().unwrap()),name2.to_str(),role_addr2,);// rolefor i in 0..4 {let level = ptrace_read_at(ppid, role_addr2 + 24 * i + 12, 4);let money = ptrace_read_at(ppid, role_addr2 + 24 * i + 16, 8);let pos_x = ptrace_read_at(ppid, role_addr2 + 24 * i + 0, 4);let pos_y = ptrace_read_at(ppid, role_addr2 + 24 * i + 4, 4);let pos_z = ptrace_read_at(ppid, role_addr2 + 24 * i + 8, 4);println!("\tRole[{}] => Level: {}, Money: {}, Pos: [{:.2}, {:.2}, {:.2}]",i,u32::from_le_bytes(level.try_into().unwrap()),u64::from_le_bytes(money.try_into().unwrap()),f32::from_le_bytes(pos_x.try_into().unwrap()),f32::from_le_bytes(pos_y.try_into().unwrap()),f32::from_le_bytes(pos_z.try_into().unwrap()),);}// modify function decMoneylet asm_addr = fn_addr + 0x44;// ptrace 每次写入也是 8 字节的数据, 所以先读取,然后再写入let mut old_asm = ptrace_read_at(ppid, asm_addr, 8);println!("old_asm: {:?}", old_asm);// 修改 sub rax, rcx => add rax, rcxold_asm[1] = 0x01;let new_val = u64::from_ne_bytes(old_asm.try_into().unwrap());unsafe { ptrace::write(ppid, asm_addr as *mut c_void, new_val as *mut c_void).unwrap();};let old_asm2 = ptrace_read_at(ppid, asm_addr, 8);println!("old_asm2: {:?}", old_asm2);ptrace::detach(ppid, None).unwrap();} else {println!("need root permission!");}}
}fn read_at_mem(f: &File, addr: u64, sz: usize) -> Vec<u8> {let mut buf = Vec::new();buf.resize(sz, 0);f.read_at(&mut buf, addr).unwrap();return buf;
}fn ptrace_read_at(ppid: Pid, addr: u64, sz: usize) -> Vec<u8> {let mut buf = Vec::new();buf.resize(sz, 0);// println!("Begin read {:#X}, sz={}", addr, sz);let mut sz2 = sz/8;if sz%8 != 0 {sz2 += 1;}for i in 0..sz2 {let paddr = (addr + (i*8) as u64) as *mut c_void;// ptrace 每次读取 8 个字节的数据let val = ptrace::read(ppid, paddr).unwrap();//println!("read: [{}] = {:#X}", addr + (i*8) as u64,val);let mut idx = i*8;for b in val.to_ne_bytes(){if idx < sz {buf[idx] = b;idx += 1;}else{break;} }}return buf;
}// 忽略大小写进行进程查找
fn find_id_by_name(name: &str) -> Vec<String> {// 遍历和读取 /proc/xxx/commlet root = path::Path::new("/proc/");let mut pids: Vec<_> = Vec::new();for sub in root.read_dir().unwrap() {if let Ok(id_dir) = sub {let fp = id_dir.path();if fp.is_dir() {let comm = fp.join("comm");if comm.exists() {let txt = file_read_content(comm.to_str().unwrap());// println!("try file: {:?}, {}", comm, txt);if txt.to_ascii_lowercase() == name.to_ascii_lowercase() {let pid = OsString::from(fp.file_name().unwrap()).into_string().unwrap();pids.push(pid);}}}}}pids
}fn file_read_content(filepath: &str) -> String {let mut txt = String::new();if let Ok(mut f) = File::open(filepath) {f.read_to_string(&mut txt).unwrap();}// 默认内容有换行txt = txt.trim().to_string();return txt;
}// 通过 /proc/pid/maps 查找 program 地址
fn process_get_base_addr(pid: &str) -> u64 {let mut addr = 0u64;if let Ok(f) = File::open(format!("/proc/{}/maps", pid)) {let mut txt = String::new();let mut br = BufReader::new(f);br.read_line(&mut txt).unwrap();//55af86ae3000-55af86ae4000 r--p 00000000 08:12 6160449 /xx/gameboxif let Some(pos) = txt.find('-') {addr = u64::from_str_radix(&txt[..pos], 16).unwrap();}}return addr;
}/*
gamebox pid: ["101595"]
gamebox base addr: 0x55698A849040, fn addr: 0x55698A8461C9
Account => ID: 20240318, Name: Ok("我是张三啊"), role addr: 0x0x55698B1156B0Role[0] => Level: 10, Money: 5000, Pos: [0.00, 0.00, 0.00]Role[1] => Level: 12, Money: 5000, Pos: [1000.00, 1100.00, 1200.00]Role[2] => Level: 14, Money: 5000, Pos: [2000.00, 2200.00, 2400.00]Role[3] => Level: 16, Money: 5000, Pos: [3000.00, 3300.00, 3600.00]
old_asm: [72, 41, 200, 72, 137, 194, 72, 139]
Input any key to modify decMoney function ....
old_asm2: [72, 1, 200, 72, 137, 194, 72, 139]
*/
proc
https://blog.csdn.net/murphy_ma123456/article/details/16117577
https://blog.csdn.net/m0_37315653/article/details/82693108
https://zhuanlan.zhihu.com/p/378388389
相关文章:
Rust 实战练习 - 8. 内存,ASM,外挂 【重磅】
目标: C写一个Demo版本的游戏由浅入深,了解外挂原理Linux/Android下实现内存读取ptrace实现内存修改(依赖第三方库) 先准备一个C写的小游戏 #include <stdio.h> #include <string.h>struct Role {float pos_x; // …...

XUbuntu22.04之Typora快捷键Ctrl+5不生效问题(二百二十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...

GRE_MGRE综合实验
目录 1、R5为ISP,只能进行IP地址配置,其所有地址均配为公有IP地址。 IP配置 配置公网全网通 2、(1)R1和R5间使用PPP的PAP认证,R5为主认证方。 PAP认证 (2)R2与R5之间使用ppp的CHAP认证&am…...

把组合损失中的权重设置为可学习参数
目前的需求是:有一个模型,准备使用组合损失,其中有2个或者多个损失函数。准备对其进行加权并线性叠加。但想让这些权重进行自我学习,更新迭代成最优加权组合。 目录 1、构建组合损失类 2、调用组合损失类 3、为其构建优化器 …...
用Bat启动jar程序
前情提要:在使用冰蝎、哥斯拉等一些列工具时(PS:一系列需要利用Java环境并打开的jar),我就在想能不能写一段代码点一下,就能打开程序而不用去输入命令 echo off echo 程序启动中... start javaw -noverif…...

网站维护页404源码
网站维护页404源码,布局简洁,上传即可使用。 网站维护页404源码...

jmeter链路压测
比如登录后返回token,业务打印上传的操作需要用到token 线程组中添加登录请求,并执行 1、添加登录并执行,查看结果 2、结果树中下拉选择正则表达式,将token参数和值复制粘贴到下方,将token值改为(.*?)࿰…...

香港服务器怎么看是CN2 GT线路还是CN2 GIA线路?
不知道有没有小伙伴们注意过,很多人在租用香港服务器的时候都习惯性选择 CN2 线路?仿佛香港服务器是否采用 CN2 线路成为个人企业选择香港服务器的一个标准。其实,香港服务器有CN2、优化直连(163)、BGP多线(包含了国际和国内线路),…...

CrossOver软件2024免费 最新版本详细介绍 CrossOver软件好用吗 Mac电脑玩Windows游戏
CrossOver是一款由CodeWeavers公司开发的软件,它可以在Mac和Linux等操作系统上运行Windows软件,而无需在计算机上安装Windows操作系统。这款软件的核心技术是Wine,它是一种在Linux和macOS等操作系统上运行Windows应用程序的开源软件。 Cross…...

harbor api v2.0
harbor api v2.0 v2.0 v2.0 “harbor api v2.0”与原来区别较大,此处harbor也做了https。另外,通过接口拿到的数据也是只能默认1页10个,所以脚本根据实际情况一页页的抓取数据 脚本主要用于统计repo、image,以及所有镜像的tag数&…...
Vue 表单数据双向绑定 v-mode
每一个Vue项目,每一个系统,肯定涉及到表单的双向数据绑定问题,这一部分是 vue 的重中之重,不是因为知识点复杂,而是因为只要参与 vue 项目的开发,那么就必不可少。 单项绑定 :数据变࿰…...

tab切换组件,可横向自适应滑动
示例图: 注:需要引入Jquery <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style>.tabs-box {width: 100%;height: auto;}.tab-header-box {display: flex;overflow: hidden…...
设计模式---单例模式
目录 一、五种单例模式的实现方式 1.饿汉模式 2.饿汉枚举类型 3.懒汉式 4.双检锁懒汉式 5.内部类懒汉式 二、JDK 中单例的体现 一、五种单例模式的实现方式 1.饿汉模式 public class Singleton1 implements Serializable {private Singleton1() {if (INSTANCE ! null) {thro…...

HarmonyOS 应用开发之启动/停止本地PageAbility
启动本地PageAbility PageAbility相关的能力通过featureAbility提供,启动本地Ability通过featureAbility中的startAbility接口实现。 表1 featureAbility接口说明 接口名接口描述startAbility(parameter: StartAbilityParameter)启动Ability。startAbilityForRes…...

BaseDao封装增删改查
文章目录 什么是BaseDao操作代码增删改查询单个数据查询多个数据 总结 什么是BaseDao BaseDao是: 数据库里负责增加,删除,修改,查询 具体来说是一种接口代码,公共方法的接口类。 在dao层新建basedao,其他dao层接口继承basedao 相…...

Redis入门到实战-第十三弹
Redis入门到实战 Redis中JSON数据类型常见操作官网地址Redis概述JSON常见操作更新计划 Redis中JSON数据类型常见操作 完整命令参考官网 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://redis.io/Redis概述 Redis是…...

深度学习InputStreamReader类
咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好…...

2023年后端面试总结
备注:这篇文章是我在2023年年初在自己的网站上写的,最近在迁移技术文章,我感觉这个也是和咱程序员相关,所以今天就决定把它迁移过来。 .......................................................................分割线..........…...
axios实现前后端通信报错Unsupported Media
使用axios向SpringBoot的后端使用post请求发送数据,发现报错Unsupported Media,最终解决方案如下: 检查变量名字是否一样,即前端传给后端的json数据键名要与后端接收的对象的成员变量名字一致检查Content-Type,post请…...
网络套接字补充——TCP网络编程
六、TCP网络编程 6.1IP地址字符串和整数之间的转换接口 //字符串转整数接口 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); int inet_pton(int af, const char *strptr, …...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
Java数值运算常见陷阱与规避方法
整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...
十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...