我在做嵌入式系统大作业的时候,遇到了一些问题,使我暂时放弃了原来制作一个借助电脑算力的同声传译器的计划,为了快些交差,我准备制作一个桌面小副屏,让网易云音乐的歌词可以通过串口显示在STM32F407单片机的显示屏上,为了解决网易云音乐没有实时歌词api接口的问题,我决定使用读取内存的方式获得网易云的歌词。
通过检索资料,我发现了“三十岁开始学编程的大叔”大佬写的一篇古早文章(原文在此:“原文链接”),觉得非常满足我的需求,但是经过试验,由于其代码年代久远,原本的地址,操作系统位数等的变动,程序已经不能正常使用,于是我重复了他的操作,自学了一下CE,重新获得了网易云音乐歌词程序歌词在内存中的地址与复数个偏移量,修改了下代码中的小bug,成功的盘活了这个程序,想到与我有相似需求的人可能不在少数,就准备发上来。
第一步,寻找网易云音乐歌词所在的地址
先找出一首英文歌曲,在CE中搜索正在显示的歌词的首字母,一般是大写。
点击新的扫描,我们会获得复数个(数以万计)数值为首字母的地址,这个时候我们不要慌,我们可以让歌曲(子弹)飞一会儿,在随机的新歌词出现时,暂停歌曲,再次搜索新的歌词的首字母。经过多次重复,最终我们会得到一个(没错,就一个)地址。
恭喜各位,现在你已经学会了如何获得了一个字母的地址(现在去高考吧),但这是远远不够的,将这个地址添加到工作区里。
我们需要获得基地址的地址和多层指针偏移(通常不止一个偏移,但一般也不会太多。网易云老贼有4个偏移)而且网易云的歌词地址还是会动的,一个一个找肯定是要命的。
经过我的实验和多方资料查证,我们最方便的方法是:在CE中对地址右键,点击对这个地址进行指针扫描,设置最大偏移量2078,层数>=4, 64位,开始扫描。
扫描后我们会再次看到复数个基地址和复数^2个偏移,这时候我们要确定谁是我们需要的,持续运行网易云音乐,发现第一个地址对应的数据是会变动的,当第一个地址变动后,第二第三个地址会依次变动成新的值,这里我也不清楚这样的原因,暂时认为是废弃的指针残存在内存中吧!期待其他大佬的解答。
根据这一行的基地址和偏移量,我们修改前人的代码。
我看过新的代码后,认为有以下几个需要着重注意的点:
1. 原作者代码成与32位时代,现在我们的电脑都是64位的,新地址会溢出,所以需要改long位longlong。
2. 原作者代码成时,网易云是双层指针,新版有4个,所以我们需要增加几个偏移。
3. 原作者代码成时,没有使用bytes转utf8的库,为了标准化我们补全这一部分。
4. 原作者代码成时,认为字节的倒置是应用故意为之,其实不然,这是由utf-16-le 小端序排列导致的,使用标准会自动解决这一问题。
修改后的代码如下:
# 2022-10-15 by jd3096 vx:jd3096
# 2024-12-26 rewrite by 五月
import pymem
import time
import win32process
from win32con import PROCESS_ALL_ACCESS
import win32api
import ctypes
from win32gui import FindWindow
def GetProcssID(address, bufflength):
kernel32 = ctypes.windll.kernel32
hwnd = FindWindow("DesktopLyrics", u"桌面歌词") # 获取窗口句柄
_, pid = win32process.GetWindowThreadProcessId(hwnd) # 获取窗口ID
hProcess = win32api.OpenProcess(PROCESS_ALL_ACCESS, False, pid) # 获取进程句柄
# 使用 c_ulonglong 代替 c_ulong 来处理64位地址
addr = ctypes.c_ulonglong()
# 使用 ctypes.c_void_p 来处理64位指针
ReadProcessMemory = kernel32.ReadProcessMemory
ReadProcessMemory.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
ReadProcessMemory.restype = ctypes.c_bool
# 使用 ctypes.c_void_p 来处理64位地址
bytes_read = ctypes.c_size_t()
result = ReadProcessMemory(int(hProcess), ctypes.c_void_p(address), ctypes.byref(addr), bufflength, ctypes.byref(bytes_read))
if not result:
raise ctypes.WinError()
win32api.CloseHandle(hProcess) # 关闭句柄
return addr.value
def Get_moduladdr(dll): #找到dll的内存基址
modules = list(Game.list_modules())
for module in modules:
if module.name == dll:
Moduladdr = module.lpBaseOfDll
return Moduladdr
def get_add(): #从基址加偏移量反复三次得到实际内存地址
Char_Modlue = Get_moduladdr("cloudmusic.dll")
# 使用 ctypes.c_ulonglong 来处理64位地址
addr = ctypes.c_ulonglong(GetProcssID(Char_Modlue + 0x019D9410, 8)).value
ret = addr + 0xE0
ret2 = ctypes.c_ulonglong(GetProcssID(ret, 8)).value
ret3 = ret2 + 0x8
ret4 = ctypes.c_ulonglong(GetProcssID(ret3, 8)).value
ret5 = ret4 + 0x120
ret6 = ctypes.c_ulonglong(GetProcssID(ret5, 8)).value
ret7 = ret6 + 0x8
ret8 = ctypes.c_ulonglong(GetProcssID(ret7, 8)).value
print(hex(ret8))
return ret8
Game = pymem.Pymem("cloudmusic.exe")
lyrics_addr=get_add()#实际内存地址
lyrics_len=200
last_lyrics=b''
def b2u(b):
"""将 bytes 转换为 Unicode 字符串"""
try:
# 尝试使用 UTF-16-LE 解码,因为你的数据似乎是以这种格式存储的
return b.decode('utf-16-le')
except UnicodeDecodeError:
# 如果 UTF-16-LE 解码失败,尝试其他编码
try:
return b.decode('utf-8')
except UnicodeDecodeError:
# 如果所有尝试都失败,返回一个错误消息或空字符串
return "无法解码的字节序列"
def get_lyrics():
global last_lyrics
raw_bytes = Game.read_bytes(lyrics_addr, lyrics_len)
use_bytes = raw_bytes.split(b'\x00\x00')[0]
if len(use_bytes) % 2 == 1:
use_bytes += b'\x00'
if last_lyrics != use_bytes:
last_lyrics = use_bytes
return b2u(use_bytes)
else:
return None
while True:
lyrics = get_lyrics()
if lyrics is not None:
print("原始歌词:", lyrics)
# 获取 UTF-8 编码
utf8_encoded = lyrics.encode('utf-8')
print("UTF-8 编码:", utf8_encoded)
# 如果你想看十六进制表示
print("UTF-8 编码 (十六进制):", utf8_encoded.hex())
time.sleep(0.1)
最终效果如下:
在此,特别鸣谢“30岁开始学编程的大叔”提供的非常好的基础代码。