当前位置: 首页 > news >正文

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上一切皆文件,只要有权限,读写其他程序的内存很简单。

  1. sudo权限,强制读写/proc/ID/mem
  2. 自身为内核空间,直接读写mem
  3. 能获取到proc的mem的物理地址,读写物理内存
  4. 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上的获取基地址方法:

  1. 启动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

  1. 通过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

  1. 计算基地址偏移

BaseAddr = gamebox 基地址 + 0x121E + 0x2E22

  1. 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,外挂 【重磅】

目标&#xff1a; C写一个Demo版本的游戏由浅入深&#xff0c;了解外挂原理Linux/Android下实现内存读取ptrace实现内存修改&#xff08;依赖第三方库&#xff09; 先准备一个C写的小游戏 #include <stdio.h> #include <string.h>struct Role {float pos_x; // …...

XUbuntu22.04之Typora快捷键Ctrl+5不生效问题(二百二十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…...

GRE_MGRE综合实验

目录 1、R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;其所有地址均配为公有IP地址。 IP配置 配置公网全网通 2、&#xff08;1&#xff09;R1和R5间使用PPP的PAP认证&#xff0c;R5为主认证方。 PAP认证 &#xff08;2&#xff09;R2与R5之间使用ppp的CHAP认证&am…...

把组合损失中的权重设置为可学习参数

目前的需求是&#xff1a;有一个模型&#xff0c;准备使用组合损失&#xff0c;其中有2个或者多个损失函数。准备对其进行加权并线性叠加。但想让这些权重进行自我学习&#xff0c;更新迭代成最优加权组合。 目录 1、构建组合损失类 2、调用组合损失类 3、为其构建优化器 …...

用Bat启动jar程序

前情提要&#xff1a;在使用冰蝎、哥斯拉等一些列工具时&#xff08;PS&#xff1a;一系列需要利用Java环境并打开的jar&#xff09;&#xff0c;我就在想能不能写一段代码点一下&#xff0c;就能打开程序而不用去输入命令 echo off echo 程序启动中... start javaw -noverif…...

网站维护页404源码

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

jmeter链路压测

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

香港服务器怎么看是CN2 GT线路还是CN2 GIA线路?

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

CrossOver软件2024免费 最新版本详细介绍 CrossOver软件好用吗 Mac电脑玩Windows游戏

CrossOver是一款由CodeWeavers公司开发的软件&#xff0c;它可以在Mac和Linux等操作系统上运行Windows软件&#xff0c;而无需在计算机上安装Windows操作系统。这款软件的核心技术是Wine&#xff0c;它是一种在Linux和macOS等操作系统上运行Windows应用程序的开源软件。 Cross…...

harbor api v2.0

harbor api v2.0 v2.0 v2.0 “harbor api v2.0”与原来区别较大&#xff0c;此处harbor也做了https。另外&#xff0c;通过接口拿到的数据也是只能默认1页10个&#xff0c;所以脚本根据实际情况一页页的抓取数据 脚本主要用于统计repo、image&#xff0c;以及所有镜像的tag数&…...

Vue 表单数据双向绑定 v-mode

每一个Vue项目&#xff0c;每一个系统&#xff0c;肯定涉及到表单的双向数据绑定问题&#xff0c;这一部分是 vue 的重中之重&#xff0c;不是因为知识点复杂&#xff0c;而是因为只要参与 vue 项目的开发&#xff0c;那么就必不可少。 单项绑定 &#xff1a;数据变&#xff0…...

tab切换组件,可横向自适应滑动

示例图&#xff1a; 注&#xff1a;需要引入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提供&#xff0c;启动本地Ability通过featureAbility中的startAbility接口实现。 表1 featureAbility接口说明 接口名接口描述startAbility(parameter: StartAbilityParameter)启动Ability。startAbilityForRes…...

BaseDao封装增删改查

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

Redis入门到实战-第十三弹

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

深度学习InputStreamReader类

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

2023年后端面试总结

备注&#xff1a;这篇文章是我在2023年年初在自己的网站上写的&#xff0c;最近在迁移技术文章&#xff0c;我感觉这个也是和咱程序员相关&#xff0c;所以今天就决定把它迁移过来。 .......................................................................分割线..........…...

axios实现前后端通信报错Unsupported Media

使用axios向SpringBoot的后端使用post请求发送数据&#xff0c;发现报错Unsupported Media&#xff0c;最终解决方案如下&#xff1a; 检查变量名字是否一样&#xff0c;即前端传给后端的json数据键名要与后端接收的对象的成员变量名字一致检查Content-Type&#xff0c;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, …...

Nginx-记

Nginx是一个高性能的web服务器和反向代理服务器&#xff0c;用于HTTP、HTTPS、SMTP、POP3和IMAP协议。因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。 &#xff08;1&#xff09;更快 这表现在两个方面&#xff1a;一方面&#xff0c;在正常情况下&…...

JS面试题:call,apply,bind区别

1. 共同点 三者共同点都是改变函数内部this指向的方法 2. call用法 ini 复制代码 var a 2; var b 2; function func() { console.log(this.a, this.b) } let obj { a: 1, b: 1 } func.call(obj) func.call() 输出结果&#xff1a; 复制代码 1 1 2 2 解析&#xff1…...

Charles抓包配置代理手机连接

Charles下载地址&#xff1a; Charles_100519.zip官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘123云盘为您提供Charles_100519.zip最新版正式版官方版绿色版下载,Charles_100519.zip安卓版手机版apk免费下载安装到手机,支持电脑端一键快捷安装https://www.123pan.com…...

NA555、NE555、SA555和SE555系列精密定时器

这份文件是关于德州仪器&#xff08;Texas Instruments&#xff09;生产的NA555、NE555、SA555和SE555系列精密定时器&#xff08;Precision Timers&#xff09;的数据手册。以下是该文件的核心内容概述&#xff1a; 产品特性&#xff1a; 德州仪器的NA555、NE555、SA555和SE55…...

黑马鸿蒙笔记2

1.图片设置&#xff1a; 1 加载网络图片&#xff0c;申请权限。 申请权限&#xff1a;entry - src - resources - module.json5 2 加载本地图片 ,两种加载方式 API 鼠标悬停在Image&#xff0c; 点击show in API Reference interpolation&#xff1a;看起来更加清晰 resou…...

微信小程序uniapp+vue3+ts+pinia的环境搭建

一.创建uniapp项目 通过vue-cli创建 npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project二.安装依赖&#xff1a; 1.pnpm i 2.运行项目&#xff1a; 将package.json的 "dev:mp-weixin": "uni -p mp-weixin",改为 "serve": "u…...

MongoDB聚合运算符:$let

文章目录 MongoDB聚合运算符&#xff1a;$let语法使用举例 MongoDB聚合运算符&#xff1a;$let $let聚合运算符绑定用于表示计算的变量&#xff0c;并返回表达式的结果。 语法 {$let:{vars: { <var1>: <expression>, ... },in: <expression>} }vars 用于在…...

HarmonyOS像素转换-如何使用像素单位设置组件的尺寸。

1 卡片介绍 基于像素单位&#xff0c;展示了像素单位的基本知识与像素转换API的使用。 2 标题 像素转换&#xff08;ArkTS&#xff09; 3 介绍 本篇Codelab介绍像素单位的基本知识与像素单位转换API的使用。通过像素转换案例&#xff0c;向开发者讲解了如何使用像素单位设…...

【前端面试3+1】05v-if和v-show的区别、v-if和v-for能同时使用吗、Vuex是什么?【合并两个有序链表】

一、v-if和v-show的区别 v-if 和 v-show 是 Vue.js 中用来控制元素显示与隐藏的指令。 1.v-if&#xff1a; v-if 是根据表达式的真假值来决定是否渲染元素。当表达式为真时&#xff0c;元素会被渲染到 DOM 中&#xff1b;当表达式为假时&#xff0c;元素不会被渲染到 DOM 中。每…...

Unity WebRequest 变得简单

作者简介: 高科,先后在 IBM PlatformComputing从事网格计算,淘米网,网易从事游戏服务器开发,拥有丰富的C++,go等语言开发经验,mysql,mongo,redis等数据库,设计模式和网络库开发经验,对战棋类,回合制,moba类页游,手游有丰富的架构设计和开发经验。 (谢谢你的关注…...

三好街做网站公司/无锡seo关键词排名

谁能告诉我怎么用php搭建论坛啊转载于:https://blog.51cto.com/6168443/1167976...

网站空间下载/网络服务电话

【实例简介】Android使用websocket 进行通讯&#xff0c;推送消息给客户端&#xff0c;实现即时通讯。【实例截图】【核心代码】153473androidWebsocketDemo└── androidWebsocketDemo-master├── README.md├── app│ ├── build.gradle│ ├── libs│ │ └…...

wap免费网站/企业官网定制设计

2019独角兽企业重金招聘Python工程师标准>>> 一、索引基础&#xff1a; MongoDB的索引几乎与传统的关系型数据库一模一样&#xff0c;这其中也包括一些基本的优化技巧。下面是创建索引的命令&#xff1a; > db.test.ensureIndex({"username":1})…...

王野天图片/欧美seo查询

FPGA图像加速解决方案来了参考文章&#xff1a; &#xff08;1&#xff09;FPGA图像加速解决方案来了 &#xff08;2&#xff09;https://www.cnblogs.com/alifpga/p/9285759.html 备忘一下。...

做网红用哪个网站/必应搜索引擎网站

16年元旦顶着大风来北京面试&#xff0c;一天6轮&#xff0c;在回去的高铁上感觉面试可能挂了&#xff0c;一个礼拜后收到了Offer。由于自身各方面的原因&#xff0c;18年元旦&#xff0c;选择离开微软。It is hard to say LEAVE, 这里整理了一下我在微软的两年经历&#xff0c…...

浏览国外服务器的网站/百度推广电话销售好做吗

root用户删除文件提示&#xff1a;Operation not permitted http://blog.csdn.net/evanbai/article/details/6187578转载于:https://www.cnblogs.com/diyunpeng/p/7648537.html...