分享

从C#到Python手把手教你用Python实现内存扫描获取指定字符串

 小小明代码实体 2022-10-28 发布于广东

📢作者: 小小明-代码实体

📢博客主页:https://blog.csdn.net/as604049322

📢欢迎点赞 👍 收藏 ⭐留言 📝 欢迎讨论!

背景

之前有个叫《羊XX羊》的游戏火了一阵子,在网友们玩的不亦乐乎的时候,各路程序员们纷纷发布自己的刷通关数的方案。

我们回顾一下所使用过的技术方案:

  1. 获取该游戏的源码后修改到去广告并设置道具无限,重新打包小程序包。用户使用自己的编译的小程序进行游戏。
  2. 抓包分析通关所发出的请求包,然后直接post提交通关请求。
  3. 使用CE修改内存达到作弊玩游戏通关的效果。
  4. 抓包分析地图请求包,使用工具替换响应,让客户端只需要通过第一关就认为已通关。
  5. 基于第二种方案分析游戏的源码,获取请求包重要参数的生成算法,自己生成token并提交通关请求。

这些方案中第五种可能是使用了不能明说的手段分析了服务端的源码,由于可以直接生成token就可以方便别人傻瓜式操作,于是很多人基本这种开发了这种帮助刷通关数的网站。本人朋友圈个别通关数几十万甚至上百万的人,可能就是使用了这种网站。

更多的技术博主分享的方案是第二种,就是两步先获取token,然后再使用工具或代码发送请求。几乎所有的博主分享的方案都是自己抓包,区别只是抓包所使用工具软件不一样,发包有的写代码有的使用现成的工具,有的则使用别人写好的软件。

所以获取token这步是最重要的,一些小白们也想刷通关数,但是由于看了文章也没有学会抓包,仍然不会操作。无数小白们希望大佬提供全自动的软件,做到傻瓜式操作。只可惜抓包很难自动化,必须一定的手动。所以个人当时也没有能力开发出全自动的软件,只是提供了第3和4这两种方案的操作教程。而第5种方案实在是不可言说,本人决不能亲自进行这种操作去分析。

不过最近我在github上又发现了一种高级的方案:https://github.com/SwaggyMacro/YangLeGeYang

它通过内存扫描自动获取了token,从而实现了全自动化,任何人都可以使用该软件轻易的刷。

扫描内存获取token的代码是:https://github.com/SwaggyMacro/YangLeGeYang/blob/master/SheepSheep/WcToken.cs

这个游戏本身已经过时,我们已经没有继续玩的兴趣,但是我们可以基于该游戏涉及的该项目,研究扫描内存,获取内存中的字符串的方法。

这是一个C#项目,其中的代码直接翻译为python并不能直接使用,涉及非常多的坑。下面我们就是基于该项目研究使用Python扫描内存的方法。

分析C#源码并翻译实现

DLL函数定义

首先我们定义出我们需要的核心函数。

C#定义:

[DllImport("kernel32.dll")]
private static extern uint GetLastError();
[DllImport("kernel32.dll")]
private static extern int OpenProcess(int dwDesiredAccess, int bInheritHandle, int dwProcessId);
[DllImport("Kernel32.dll", SetLastError = true)]
private static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, int dwLength);
[DllImport("Kernel32.dll")]
public static extern bool ReadProcessMemory(IntPtr handle, int address, byte[] data, int size, byte[] read);

Python定义:

from ctypes import *

kernel32 = cdll.LoadLibrary("kernel32.dll")
GetLastError = kernel32.GetLastError
OpenProcess = kernel32.OpenProcess
VirtualQueryEx = kernel32.VirtualQueryEx
ReadProcessMemory = kernel32.ReadProcessMemory

这几个函数的参数列表可以查看:

  • https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
  • https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
  • https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualqueryex
  • https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory

其中GetLastError可以在其他kernel32函数调用失败时获取错误码,错误码列表可以查看:

https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes

有博主翻译的中文错误码大全:https://blog.csdn.net/sunjiaoya/article/details/125873124

获取进程PID

然后我们针对C#入口代码,使用Python进行测试。

C#获取微信小程序进程PID代码:

Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
	if (process.ProcessName.Equals("WeChatAppEx")) {

	}
}

用python实现:

import psutil

for proc in psutil.process_iter(attrs=['name', 'pid']):
    if "WeChatAppEx" in proc.name():
        print(proc.pid)

可以看到打印结果与任务管理器一致。

image-20221028112717437

内存搜索函数测试

C#代码:

private struct MEMORY_BASIC_INFORMATION
{
	public int BaseAddress;
	public int AllocationBase;
	public int AllocationProtect;
	public int RegionSize;
	public int State;
	public int Protect;
	public int lType;
}

private static List<int> MemorySearch(IntPtr HWND, byte[] content)
{
	int IpAddr = 0x000000;
	MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION();
	while (VirtualQueryEx(HWND, (IntPtr)IpAddr, out mbi, 28) != 0)
	{
		...
		IpAddr = IpAddr + mbi.RegionSize;
	}
	return foundList;
}

使用python测试一下:

import win32con

DWORD = c_ulong


class MEMORY_BASIC_INFORMATION(Structure):
    _fields_ = [
        ("BaseAddress", DWORD),
        ("AllocationBase", DWORD),
        ("AllocationProtect", DWORD),
        ("RegionSize", DWORD),
        ("State", DWORD),
        ("Protect", DWORD),
        ("Type", DWORD),
    ]


process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, proc.pid)
mbi = MEMORY_BASIC_INFORMATION()
IpAddr = 0x0
t = VirtualQueryEx(process, IpAddr, byref(mbi), 28)
print(t, GetLastError())
print(mbi.BaseAddress, mbi.AllocationBase, mbi.AllocationProtect,
      mbi.RegionSize, mbi.State, mbi.Protect, mbi.Type)

结果:

0 24
0 0 0 0 0 0 0

在之前,我用原始翻译代码这么测试时,就是死活不行,VirtualQueryEx始终返回零。后面我想到使用GetLastError查看错误码,是24,表示程序发出命令,但命令长度不正确。

基于此,我就有了继续分析的思路,推测可能是系统位数的不同,原始的C#代码适用于32位的编程环境中,而64位环境下MEMORY_BASIC_INFORMATION的结构可能不一样。

基于这个假设进行搜索,我甚至在Stack Overflow论坛上看到有人遇到与我一样的问题:

https:///questions/36319464/virtualqueryex-with-process-query-information-gives-error-24/74215819

但是无人回答,我最终找出问题所在后,已注册账号并回答了该问题。

结论就是确实验证了我的假设,我们可以通过微软官方文档看到32位系统与64位系统不同的定义:

https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information

定义如下:

typedef struct _MEMORY_BASIC_INFORMATION32 {
    DWORD BaseAddress;
    DWORD AllocationBase;
    DWORD AllocationProtect;
    DWORD RegionSize;
    DWORD State;
    DWORD Protect;
    DWORD Type;
} MEMORY_BASIC_INFORMATION32, *PMEMORY_BASIC_INFORMATION32;

typedef struct DECLSPEC_ALIGN(16) _MEMORY_BASIC_INFORMATION64 {
    ULONGLONG BaseAddress;
    ULONGLONG AllocationBase;
    DWORD     AllocationProtect;
    DWORD     __alignment1;
    ULONGLONG RegionSize;
    DWORD     State;
    DWORD     Protect;
    DWORD     Type;
    DWORD     __alignment2;
} MEMORY_BASIC_INFORMATION64, *PMEMORY_BASIC_INFORMATION64;

而28正是原本MEMORY_BASIC_INFORMATION定义的长度,可以使用sizeof(mbi)获取。

于是重新编码测试:

import win32con

DWORD = c_ulong
ULONGLONG = c_ulonglong


class MEMORY_BASIC_INFORMATION32(Structure):
    _fields_ = [
        ("BaseAddress", DWORD),
        ("AllocationBase", DWORD),
        ("AllocationProtect", DWORD),
        ("RegionSize", DWORD),
        ("State", DWORD),
        ("Protect", DWORD),
        ("Type", DWORD),
    ]


class MEMORY_BASIC_INFORMATION64(Structure):
    _fields_ = [
        ("BaseAddress", ULONGLONG),
        ("AllocationBase", ULONGLONG),
        ("AllocationProtect", DWORD),
        ("__alignment1", DWORD),
        ("RegionSize", ULONGLONG),
        ("State", DWORD),
        ("Protect", DWORD),
        ("Type", DWORD),
        ("__alignment2", DWORD),
    ]


process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, proc.pid)
mbi = MEMORY_BASIC_INFORMATION64()
IpAddr = 0x0
t = VirtualQueryEx(process, IpAddr, byref(mbi), sizeof(mbi))
print(t, GetLastError())
print(mbi.BaseAddress, mbi.AllocationBase, mbi.AllocationProtect,
      mbi.RegionSize, mbi.State, mbi.Protect, mbi.Type)
48 0
0 0 0 6094848 65536 1 0

这次VirtualQueryEx的返回值终于不再是0,错误码返回了0表示无错误。

然后我们可以尝试从第一个可读区域中读取字节:

process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, proc.pid)
mbi = MEMORY_BASIC_INFORMATION64()
IpAddr = 0x0
while(VirtualQueryEx(process, IpAddr, byref(mbi), sizeof(mbi)) != 0):
    if mbi.Protect in (1, 16, 512):
        IpAddr = IpAddr + mbi.RegionSize
        continue
    print(
        f"RegionSize={mbi.RegionSize}, State={mbi.State}, Protect={mbi.Protect}, Type={mbi.Type}")
    data_type = c_char * mbi.RegionSize
    readByte = data_type()
    bytesRead = c_ulong(0)
    position = 0
    isRead = ReadProcessMemory(process, IpAddr, byref(
        readByte), mbi.RegionSize, byref(bytesRead))
    if isRead:
        break
    IpAddr = IpAddr + mbi.RegionSize
result = readByte.raw
result
RegionSize=4096, State=4096, Protect=2, Type=16777216

结果中能够清晰的看到一段This program cannot be run in DOS mode.的字符串:

image-20221028134400908

字节搜索测试

C#代码:

public static int IndexOf(byte[] array, byte[] pattern, int startOffset = 0)
{
	int success = 0;
	for (int i = startOffset; i < array.Length; i++)
	{
		if (array[i] == pattern[success])
		{
			success++;
		}
		else
		{
			success = 0;
		}
		if (pattern.Length == success)
		{
			return i - pattern.Length + 1;
		}
	}
	return -1;
}

原始的C#代码使用for循环逐字节查找,我一开始也编写了对应的Python函数测试,结果由于python的每次读取都极度耗时,应是导致每次测试扫描都耗时达到1分钟左右。但实际上我们再Python中可以直接使用字节自带的查找方法find。

C#代码打算查找所有具备的目标字符串的地址,但实际上我们只需要找到第一个目标字符串即可。

text = "This program cannot be run in DOS mode."
result.find(text.encode("u8"))
78

find方法在找不到目标字节数组时也会返回-1。

完善内存字符串搜索方法

编写的方法如下:

def MemorySearch(address, text, returnSize=1024):
    content = text.encode("u8")
    process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, address)
    mbi = MEMORY_BASIC_INFORMATION64()
    IpAddr = 0x0
    while VirtualQueryEx(process, IpAddr, byref(mbi), sizeof(mbi)) != 0:
        if mbi.Protect not in (1, 16, 512):
            data_type = c_char * mbi.RegionSize
            readByte = data_type()
            bytesRead = c_ulong(0)
            isRead = ReadProcessMemory(process, IpAddr, byref(
                readByte), mbi.RegionSize, byref(bytesRead))
            if isRead:
                position = readByte.raw.find(content)
                if position != -1:
                    token = readByte.raw[position +
                                         len(content):position+returnSize].decode("u8", "ignore")
                    return token
        IpAddr = IpAddr + mbi.RegionSize

搜索第一个This字符串,并获取从This开头的48个字节组成的字符串:

text = "This"
MemorySearch(proc.pid, text, 48)
'program cannot be run in DOS mode.$\x00\x00PE\x00\x00L\x01'

可以看到已经顺利的获取对应字符串。

完善Python代码

那么针对某游戏我们可以针对什么字符串获取到token呢?

我们可以先手动使用CE测试一下:

image-20221028151836493

可以看到搜索\",\"token\":\"字符串即可获取对应的token。

最终完整Python代码为:

import psutil
from ctypes import *
import win32con

DWORD = c_ulong
ULONGLONG = c_ulonglong
kernel32 = cdll.LoadLibrary("kernel32.dll")
GetLastError = kernel32.GetLastError
OpenProcess = kernel32.OpenProcess
VirtualQueryEx = kernel32.VirtualQueryEx
ReadProcessMemory = kernel32.ReadProcessMemory


class MEMORY_BASIC_INFORMATION64(Structure):
    _fields_ = [
        ("BaseAddress", ULONGLONG),
        ("AllocationBase", ULONGLONG),
        ("AllocationProtect", DWORD),
        ("__alignment1", DWORD),
        ("RegionSize", ULONGLONG),
        ("State", DWORD),
        ("Protect", DWORD),
        ("Type", DWORD),
        ("__alignment2", DWORD),
    ]


def MemorySearch(address, text, returnSize=1024):
    content = text.encode("u8")
    process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, address)
    mbi = MEMORY_BASIC_INFORMATION64()
    IpAddr = 0x0
    max_RegionSize = 0.5*2**30
    while VirtualQueryEx(process, IpAddr, byref(mbi), sizeof(mbi)) != 0:
        if mbi.Protect not in (1, 16, 512):
            data_type = c_char * mbi.RegionSize
            readByte = data_type()
            bytesRead = c_ulong(0)
            isRead = ReadProcessMemory(process, IpAddr, byref(
                readByte), mbi.RegionSize, byref(bytesRead))
            if isRead:
                position = readByte.raw.find(content)
                if position != -1:
                    token = readByte.raw[position +
                                         len(content):position+returnSize].decode("u8", "ignore")
                    return token
        IpAddr = IpAddr + mbi.RegionSize
        if mbi.RegionSize > max_RegionSize:
            # 不扫描0.5GB以上的区域
            break


def MemorySearchToken(address):
    token = MemorySearch(address, r"\",\"token\":\"")
    if token:
        return token[:token.find(r"\"")]


for proc in psutil.process_iter(attrs=['name', 'pid']):
    if "WeChatAppEx" in proc.name():
        print(proc.pid)
        token = MemorySearchToken(proc.pid)
        if token:
            print(token)
            break

测试结果:

image-20221028153205732

可以看到在遍历到第二个小程序进程时就找到了该游戏的进程,并顺利获取对应的token。整个过程耗时不超过2秒,问题顺利解决。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多