通过破解游戏学习Frida基础知识
最近我看到
LiveOverflow开始播放一系列关于如何“破解“一款游戏的视频,破解该游戏是2015年Ghost in the Shellcode CTF比赛中的一项挑战。在观看了前两、三个视频之后,我决定使用同一款游戏来做一些关于
Frida的介绍,并且向你展示这个了不起的项目是可以助你一臂之力的。
因此,在本文中,我们要构思一个可以帮助我们破解游戏的作弊方法。读者要注意的如下:
- 使用Frida hook函数
- 读内存
- 写内存
- 在我们的hook中调用二进制函数
- 处理类和结构
首先,请参考此链接配置服务器。如果你已准备好服务器和客户端,那我们就开玩!:)
0x00第一步:信息收集!
第一步是启动客户端,注册新玩家,并探索游戏世界。在你花了几分钟在地图上移动并查看HUD(比如魔法值,生命值,物品等)之后,现在是时候继续下一步,在终端上做一些小动作了。游戏运行时,执行ps-aux并检查客户端二进制文件的名称:PwnAdventure3-Linux-Shipping。它是一个带符号的动态链接二进制文件(使用file查看),因此很有可能我们关心的“核心”位于一个共享对象中。 我们可以使用ldd列出该二进制文件使用的所有共享对象:
mothra@kaiju:~/holydays|⇒ ldd $(locate PwnAdventure3-Linux-Shipping)
linux-vdso.so.1 (0x00007fff5e6c2000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f5af4631000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5af442d000)
libGameLogic.so => /home/mothra/PwnAdventure3_Data/PwnAdventure3/PwnAdventure3/Binaries/Linux/libGameLogic.so (0x00007f5af3f61000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f5af3d59000)
libopenal.so.1 => /home/mothra/PwnAdventure3_Data/PwnAdventure3/PwnAdventure3/Binaries/Linux/../../../Engine/Binaries/Linux/libopenal.so.1 (0x00007f5af3b02000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5af3801000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f5af34f6000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f5af32e0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5af2f35000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5af484e000)
libssl.so.1.0.0 => /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0 (0x00007f5af2cd4000)
libcrypto.so.1.0.0 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (0x00007f5af28d7000)
看起来“libGameLogic.so”是我们的目标。由于游戏是用C++编写的,我们必须处理name mangling(名字修饰)。为了转储所有导出函数并做名字解析,我们执行一个使用Frida和cxxfilt的小脚本: # Extract exports & demangle it
import frida
import cxxfilt
session = frida.attach("PwnAdventure3-Linux-Shipping")
script = session.create_script("""
var exports = Module.enumerateExportsSync("libGameLogic.so");
for (i = 0; i < exports.length; i++) {
send(exports[i].name);
}
""");
def on_message(message, data):
print message["payload"] + " - " + cxxfilt.demangle(message["payload"])
script.on('message', on_message)
script.load()
我们在做什么?我们将自己附加到正在运行的游戏进程(PwnAdventure3-Linux-Shipping),然后创建一个包含主逻辑的脚本(JavaScript)。从该JavaScript代码段我们可以访问Frida API,所有magic都会实现:):使用Module.enumerateExportsSync(libname)我们检索一个包含所有导出函数的数组,然后遍历数组并使用send()传递信息给Python脚本。在Python脚本中,我们只需调用cxxfilt.demangle()来解析名字。
现在我们有了一堆有用的信息,可以从中搜索。例如,让我们搜索与速度(speed)相关的方法:
mothra@kaiju:~/holydays|⇒ python demangle-exports.py > demangled.txt
mothra@kaiju:~/holydays|⇒ cat demangled.txt | grep -i speed
_ZN6Player12GetJumpSpeedEv - Player::GetJumpSpeed()
_ZThn168_N6Player12GetJumpSpeedEv - non-virtual thunk to Player::GetJumpSpeed()
_ZN6Player15GetWalkingSpeedEv - Player::GetWalkingSpeed()
_ZThn168_N6Player15GetWalkingSpeedEv - non-virtual thunk to Player::GetWalkingSpeed()
0x01 作弊
通常我们不需要一直作弊。也许我们只想增加我们的移动速度以便做长距离移动,但在建筑物内我们想要正常的速度,或者将自己传送到另一个位置我们则需要传递坐标参数。最好的解决方法是使用游戏的聊天功能。 在我们导出的数据中搜索“chat”字样:
mothra@kaiju:~/holydays|⇒ cat demangled.txt | grep -i chat
_ZN6Player11ReceiveChatEPS_RKSs - Player::ReceiveChat(Player*, std::string const&)
_ZN11ClientWorld4ChatEP6PlayerRKSs - ClientWorld::Chat(Player*, std::string const&)
_ZN10LocalWorld13SendChatEventEP6PlayerRKSs - LocalWorld::SendChatEvent(Player*, std::string const&)
_ZN20GameServerConnection11OnChatEventEP6Player - GameServerConnection::OnChatEvent(Player*)
_ZN11ServerWorld4ChatEP6PlayerRKSs - ServerWorld::Chat(Player*, std::string const&)
_ZN11ServerWorld13SendChatEventEP6PlayerRKSs - ServerWorld::SendChatEvent(Player*, std::string const&)
_ZN13ClientHandler4ChatEv - ClientHandler::Chat()
_ZN11ClientWorld13SendChatEventEP6PlayerRKSs - ClientWorld::SendChatEvent(Player*, std::string const&)
_ZN6Player4ChatEPKc - Player::Chat(char const*)
_ZN20GameServerConnection4ChatERKSs - GameServerConnection::Chat(std::string const&)
_ZN6Player11PerformChatERKSs - Player::PerformChat(std::string const&)
_ZThn168_N6Player4ChatEPKc - non-virtual thunk to Player::Chat(char const*)
_ZN10LocalWorld4ChatEP6PlayerRKSs - LocalWorld::Chat(Player*, std::string const&)
那个Player::Chat(char const*)看起来很有趣,因为它收到一个指向字符串的指针(也许是我们的聊天消息?)。为了确认,我们hook它并将该字符串的内容输出为log:
# Log chat
import frida
import sys
session = frida.attach("PwnAdventure3-Linux-Shipping")
script = session.create_script("""
//Find "Player::Chat"
var chat = Module.findExportByName("libGameLogic.so", "_ZN6Player4ChatEPKc");
console.log("Player::Chat() at address: " + chat);
Interceptor.attach(chat, {
onEnter: function (args) { // 0 => this; 1 => cont char* (our text)
var chatMsg = Memory.readCString(args[1]);
console.log("[Chat]: " + chatMsg);
}
});
""")
script.load()
sys.stdin.read()
通过Module.findExportByName(libname, function),我们得到Player::Chat的地址,然后我们将该地址传递给Interceptor,以便“附加”我们的hook。现在我们可以控制两个事件:onEnter和onLeave(按字面意思理解就行)。在onEnter里面我们可以看到参数(记住第一个参数是
this,所以第二个参数是我们指向字符串的指针)。最后我们只需要用Memory.readCString(pointer)读取内存来获取字符串。执行之并输入聊天内容:
mothra@kaiju:~/holydays|⇒ python log-chat.py
Player::Chat() at address: 0x7f4ca4d4d850
[Chat]: This Works
此时,我们可以在游戏聊天中键入并解析命令以触发我们的作弊行为。噢,等等,什么行为呢?继续阅读!
0x02 速度!
我们要做的第一件事就是加快移动速度。如前所述,二进制文件有符号。让我们转储与Player类相关的符号:
gdb -p $(pidof PwnAdventure3-Linux-Shipping) --batch -ex "ptype Player" -ex "quit" > Player.class
搜索speed:
mothra@kaiju:~/holydays|⇒ cat Player.class | grep -i speed
float m_walkingSpeed;
float m_jumpSpeed;
virtual float GetWalkingSpeed();
virtual float GetJumpSpeed();
有趣的是,我们找到了m_walkingSpeed(float),看起来像是移动动作的基准速度,还找到一个名为“GetWalkingSpeed()”的方法(如果我们用解析数据做交叉检查)对应于_ZN6Player15GetWalkingSpeedEv - Player::GetWalkingSpeed()。我们可以hook GetWalkingSpeed所以每次调用时m_walkingSpeed的值都会被我们想要的速度值覆盖。
mothra@kaiju:~/holydays|⇒ gdb -p $(pidof PwnAdventure3-Linux-Shipping) --batch
\ -ex "b _ZN6Player15GetWalkingSpeedEv" --ex "c" --ex "print &this->m_walkingSpeed"
\ -ex "print this" -ex "print (int)\$1-(int)\$2" -ex "quit" 2>/dev/null | awk '/\$3/ {print $3 }'
736
所以我们的步骤是:1、hook ___ZN6Player15GetWalkingSpeedEv;2、获取onEnter的this指针并添加736(偏移量)来获取m_walkingSpeed的内存地址; 3、用我们想要的速度值覆盖该浮点数(不大于9999)。
// Find Player::GetWalkingSpeed()
var walkSpeed = Module.findExportByName("libGameLogic.so", "_ZN6Player15GetWalkingSpeedEv");
console.log("Player::GetWalkingSpeed() at address: " + walkSpeed);
// Check Speed
Interceptor.attach(walkSpeed,
{
// Get Player * this location
onEnter: function (args) {
console.log("Player at address: " + args[0]);
this.walkingSpeedAddr = ptr(args[0]).add(736) // Offset m_walkingSpeed
console.log("WalkingSpeed at address: " + this.walkingSpeedAddr);
},
留意我们是如何获取walkingSpeedAddr中m_walkingSpeed的内存地址的,以便我们在onLeave事件中访问此值:
// Get the return value and write the new value
onLeave: function (retval) {
console.log("Walking Speed: " + Memory.readFloat(this.walkingSpeedAddr));
Memory.writeFloat(this.walkingSpeedAddr, 9999);
}
});
正如我们之前对readCString所做的那样,现在我们使用Memory.readFloat来读取原始速度值(200)并将其记录在命令行终端中。最后,我们将移动速度设为浮点数(9999)。运行游戏并在地图上移动。疯狂的速度!
由于我们有获取聊天消息的例行程序,我们可以配合使用 !wspeed_on NUMBER和!wspeed_off来调节速度:
script = session.create_script("""
// Global Values
var Player = {
m_walkingSpeed : 200,
};
// Cheat status
var cheatStatus = {
walkingSpeed : 0,
};
// Chat Helper
function chatHelper(msg) {
var token = msg.split(" ");
if (token[0] === "!wspeed_on") {
Player.m_walkingSpeed = parseInt(token[1]);
cheatStatus.walkingSpeed = 1;
console.log("[CHEAT]: Walking Speed Enabled (" + token[1] + ")");
}
if (token[0] === "!wspeed_off") {
Player.m_walkingSpeed = 200;
cheatStatus.walkingSpeed = 0;
console.log("[CHEAT]: Walking Speed Disabled (200)");
}
}
//Find "Player::Chat"
var chat = Module.findExportByName("libGameLogic.so", "_ZN6Player4ChatEPKc");
console.log("Player::Chat() at address: " + chat);
// Add our logger
Interceptor.attach(chat, {
onEnter: function (args) { // 0 => this; 1 => cont char* (our text)
var chatMsg = Memory.readCString(args[1]);
console.log("[Chat]: " + chatMsg);
chatHelper(chatMsg);
}
});
// Find Player::GetWalkingSpeed()
var walkSpeed = Module.findExportByName("libGameLogic.so", "_ZN6Player15GetWalkingSpeedEv");
console.log("Player::GetWalkingSpeed() at address: " + walkSpeed);
// Check Speed
Interceptor.attach(walkSpeed,
{
// Get Player * this location
onEnter: function (args) {
//console.log("Player at address: " + args[0]);
this.walkingSpeedAddr = ptr(args[0]).add(736) // Offset m_walkingSpeed
//console.log("WalkingSpeed at address: " + this.walkingSpeedAddr);
},
// Get the return value and write the new speed
onLeave: function (retval) {
if (Memory.readFloat(this.walkingSpeedAddr) != Player.m_walkingSpeed && cheatStatus.walkingSpeed == 0) {
Memory.writeFloat(this.walkingSpeedAddr, 200);
}
if (cheatStatus.walkingSpeed == 1) {
Memory.writeFloat(this.walkingSpeedAddr, Player.m_walkingSpeed);
}
}
});
""")
0x03 传送
快速移动有助于探索更大的地图区域,但在地图的其他地点闪现的能力更酷。再次搜索我们的demangled函数:
mothra@kaiju:~/holydays|⇒ cat demangled.txt| grep -i position
_ZN11ServerWorld23SendActorPositionEventsEP6Player - ServerWorld::SendActorPositionEvents(Player*)
_ZN6Player25ShouldSendPositionUpdatesEv - Player::ShouldSendPositionUpdates()
_ZN5Actor28ShouldReceivePositionUpdatesEv - Actor::ShouldReceivePositionUpdates()
_ZN7AIActor25ShouldSendPositionUpdatesEv - AIActor::ShouldSendPositionUpdates()
_ZN5Actor28SetRemotePositionAndRotationERK7Vector3RK8Rotation - Actor::SetRemotePositionAndRotation(Vector3 const&, Rotation const&)
_ZN20GameServerConnection26OnPositionAndVelocityEventEP6Player - GameServerConnection::OnPositionAndVelocityEvent(Player*)
_ZN5Actor11GetPositionEv - Actor::GetPosition()
_ZN4Drop25ShouldSendPositionUpdatesEv - Drop::ShouldSendPositionUpdates()
_ZN6Player28ShouldReceivePositionUpdatesEv - Player::ShouldReceivePositionUpdates()
_ZN10LocalWorld23SendActorPositionEventsEP6Player - LocalWorld::SendActorPositionEvents(Player*)
_ZN6Player15GetLookPositionEv - Player::GetLookPosition()
_ZN20GameServerConnection15OnPositionEventEP6Player - GameServerConnection::OnPositionEvent(Player*)
_ZN5Actor15GetLookPositionEv - Actor::GetLookPosition()
_ZN11ClientWorld23SendActorPositionEventsEP6Player - ClientWorld::SendActorPositionEvents(Player*)
_ZN20GameServerConnection21OnPlayerPositionEventEP6Player - GameServerConnection::OnPlayerPositionEvent(Player*)
_ZN5Actor21GetProjectilePositionEv - Actor::GetProjectilePosition()
_ZN10Projectile25ShouldSendPositionUpdatesEv - Projectile::ShouldSendPositionUpdates()
_ZN5Actor25ShouldSendPositionUpdatesEv - Actor::ShouldSendPositionUpdates()
_ZN5Actor25InterpolateRemotePositionEf - Actor::InterpolateRemotePosition(float)
_ZN7AIActor28ShouldReceivePositionUpdatesEv - AIActor::ShouldReceivePositionUpdates()
_ZN5Actor11SetPositionERK7Vector3 - Actor::SetPosition(Vector3 const&)
一个令人激动的SetPosition出现了!其中有一个Vector3参数,是x,y和z轴的坐标,所以这个SetPosition是我们传送的关键。
在Frida中,我们可以通过NativeFunction调用二进制文件内的函数。我们需要知道:
我们需要将指针传递给this和我们的Vector3。第一点要求很容易解决:当我们调用“!tp”命令时,只需从聊天的hook中将其取出。为了解决第二个要求,我们用Frida分配一个小缓冲区,其中我们将用x,y和z的信息写入浮点数,然后将指针和此缓冲区传递给SetPosition。
//Teleport
var setPositionAddr = Module.findExportByName("libGameLogic.so", "_ZN5Actor11SetPositionERK7Vector3");
var setPosition = new NativeFunction(setPositionAddr, 'void', ['pointer', 'pointer']);
var Vector3 = Memory.alloc(16);
function teleport(thisReference, x, y, z) {
Memory.writeFloat(Vector3, x);
Memory.writeFloat(ptr(Vector3).add(4), y);
Memory.writeFloat(ptr(Vector3).add(8), z);
setPosition(thisReference, Vector3);
}
通过调用Memory.alloc(SIZE)可以很容易分配缓冲区。 然后通过Memory.writeFloat,我们输入所需坐标(x, y, z)的值,最后我们调用该函数。整个脚本,包括聊天解析,应该如下所示:
// Global Values
var Player = {
m_walkingSpeed : 200,
};
// Cheat status
var cheatStatus = {
walkingSpeed : 0,
};
//Teleport
var setPositionAddr = Module.findExportByName("libGameLogic.so", "_ZN5Actor11SetPositionERK7Vector3");
var setPosition = new NativeFunction(setPositionAddr, 'void', ['pointer', 'pointer']);
var Vector3 = Memory.alloc(16);
function teleport(thisReference, x, y, z) {
Memory.writeFloat(Vector3, x);
Memory.writeFloat(ptr(Vector3).add(4), y);
Memory.writeFloat(ptr(Vector3).add(8), z);
setPosition(thisReference, Vector3);
}
// Chat Helper
function chatHelper(msg, thisReference) {
var token = msg.split(" ");
if (token[0] === "!wspeed_on") {
Player.m_walkingSpeed = parseInt(token[1]);
cheatStatus.walkingSpeed = 1;
console.log("[CHEAT]: Walking Speed Enabled (" + token[1] + ")");
}
if (token[0] === "!wspeed_off") {
Player.m_walkingSpeed = 200;
cheatStatus.walkingSpeed = 0;
console.log("[CHEAT]: Walking Speed Disabled (200)");
}
if (token[0] === "!tp") {
console.log("[CHEAT]: Teleporting to " + token[1] + " " + token[2] + " "+ token[3]);
teleport(thisReference, parseInt(token[1]), parseInt(token[2]), parseInt(token[3]));
}
}
//Find "Player::Chat"
var chat = Module.findExportByName("libGameLogic.so", "_ZN6Player4ChatEPKc");
console.log("Player::Chat() at address: " + chat);
// Add our logger
Interceptor.attach(chat, {
onEnter: function (args) { // 0 => this; 1 => cont char* (our text)
var chatMsg = Memory.readCString(args[1]);
console.log("[Chat]: " + chatMsg);
chatHelper(chatMsg, args[0]);
}
});
// Find Player::GetWalkingSpeed()
var walkSpeed = Module.findExportByName("libGameLogic.so", "_ZN6Player15GetWalkingSpeedEv");
console.log("Player::GetWalkingSpeed() at address: " + walkSpeed);
// Check Speed
Interceptor.attach(walkSpeed,
{
// Get Player * this location
onEnter: function (args) {
//console.log("Player at address: " + args[0]);
this.walkingSpeedAddr = ptr(args[0]).add(736) // Offset m_walkingSpeed
//console.log("WalkingSpeed at address: " + this.walkingSpeedAddr);
},
// Get the return value
onLeave: function (retval) {
if (Memory.readFloat(this.walkingSpeedAddr) != Player.m_walkingSpeed && cheatStatus.walkingSpeed == 0) {
Memory.writeFloat(this.walkingSpeedAddr, 200);
}
if (cheatStatus.walkingSpeed == 1) {
Memory.writeFloat(this.walkingSpeedAddr, Player.m_walkingSpeed);
}
}
});
0x04 从天而降的魔法
更新于2018年7月8日:服务端会做魔法值检测,所以我这里失败了。事与愿违,我们只设置了HUD中的值:(
如果你走向太阳的方向(在海上),你会在某处找到一个有奶牛的岛屿,之后会触发一个任务。在不做手脚的情况下,这个岛上的一个NPC会给你一个武器。 这个武器会消耗魔法,我们不喜欢浪费魔法(即使会快速再生)。我们希望魔法值始终处于最大值!
mothra@kaiju:~/holydays|⇒ cat Player.class| grep -i mana
int32_t m_mana;
float m_manaRegenTimer;
virtual int32_t GetMana();
virtual bool UseMana(int32_t);
void PerformSetMana(int32_t);
魔法值与上文的移动速度情况类似,因此我们的操作方式也是相同的。计算m_mana的偏移量,然后hook GetMana将魔法值覆盖为100:
mothra@kaiju:~/holydays|⇒ gdb -p $(pidof PwnAdventure3-Linux-Shipping) --batch
\ -ex "set verbose off" -ex "b _ZN6Player15GetWalkingSpeedEv" --ex "c" --ex "print &this->m_mana"
\ -ex "print this" -ex "print (int)\$1-(int)\$2" -ex "quit" 2>/dev/null | awk '/\$3/ {print $3 }'
544
Hook GetMana:
var getMana = Module.findExportByName("libGameLogic.so", "_ZN6Player7GetManaEv");
console.log("Player::GetMana at address: " + getMana);
Interceptor.attach(getMana,
{
onEnter: function (args) {
if (cheatStatus.infiniteMana == 1) {
m_manaAddr = ptr(args[0]).add(544) // Offset m_mana
Memory.writeInt(m_manaAddr, 100);
}
}
}
);
火力全开,魔法值不再减少。
0x05 写在最后
如果您发现拼写错误或其他错误,请与我联系:P。
原文链接:https://x-c3ll.github.io/posts/Frida-Pwn-Adventure-3/
编译:看雪翻译小组 SpearMint
[培训]内核驱动高级班,冲击BAT一流互联网大厂工
作,每周日13:00-18:00直播授课
最后于 2018-8-13 19:49
被SpearMint编辑
,原因: