吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3131|回复: 6
收起左侧

[会员申请] 申請會員ID: tnsc4502【申请通过】

[复制链接]
吾爱游客  发表于 2020-12-21 19:49
1. 申请人ID: tnsc4502

您好,小弟来自台湾,主要从事逆向工程与C++编程,在网路上较常分享的作品主要是枫之谷(冒险岛)相关,最主要作品为
1 根据逆向分析所彷製的官方版伺服器 :https://github.com/tnsc4502/mnwvs077

(github登入後畫面)
image.png

2 根据逆向分析研究后破解游戏客户端使其支持自定义游戏技能增添
http://forum.ragezone.com/f921/2019-adding-custom-skills-1157481

3 根据逆向分析研究后破解游戏客户端使其支持自定义角色动作增添
http://forum.ragezone.com/f921/2019-adding-character-actions-1157618

(RZ論壇登入後畫面)
image.png


4 自己有一系列的C++教学文章供大学学弟妹参考用(自己曾经是TA)
https://blog.maplenight.net/?cat=6

(部落格後台)
image.png

******抱歉由於系統一直提示我包含非法字符, 所以才一直發文, 造成不便請見諒******

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Hmily 发表于 2020-12-22 09:45
请认真阅读神器内容,把技术文章直接发布到帖子中申请。
吾爱游客  发表于 2020-12-22 21:04
好的版主,我會重發,但想請問一下能否幫我遮蔽下email,畢竟是常用信箱,不想公開,謝謝

点评

不用重发,直接跟帖回复即可,如果申请通过或者不申请了可以删除邮箱,不然我不知道你邮箱没法给你继续处理。  详情 回复 发表于 2020-12-23 10:15
Hmily 发表于 2020-12-23 10:15
游客 111.252.16.x 发表于 2020-12-22 21:04
好的版主,我會重發,但想請問一下能否幫我遮蔽下email,畢竟是常用信箱,不想公開,謝謝

不用重发,直接跟帖回复即可,如果申请通过或者不申请了可以删除邮箱,不然我不知道你邮箱没法给你继续处理。
吾爱游客  发表于 2020-12-23 16:30
感謝樓主,那請容許我已冒險島新增技能的相關內容進行講解。

第一部分,真正的新增技能

說到新增技能,很多人以為直接在Skill.wz(wz = 冒險島的資源格式)新增技能即可,然而實際咝袝r會發現點擊技能毫無反應,而這點與伺服器端是無關係的。
曾經有人在RZ論壇發表過一項新增技能的方法,實際讓他是藉助精靈遊俠(双弩精灵)連續技能來達成的,也就是施放了一個原先就有的技能後,接續施放新增的技能,這雖然可行,但是實務上使用起來非常彆扭,而且舊版冒險島不支持。

為了要解決新增技能無法使用的問題,打開MapleStory.exe,進行逆向分析,找到UserLocal::DoActiveSkill,這個函數名可根據官方洩漏的debug版客戶端與自己的客戶端進行特徵交叉比對,或者找尋攻擊技能ID找到對應的函數。

找到該函數之後,我們稍微往下翻,大概可以看到類似的內容(我是使用台版v147,對應接近國際版v117)
[C] 纯文本查看 复制代码
	if (v10 <= 2111004)
	{
		if (v10 == 2111004)
			goto LABEL_807;
		if (v10 <= 2101002)
		{
			if (v10 != 2101002)
			{
				if (v10 == 1321012)
					goto ActiveAttackSkillAddr;
                         ....
	}


對應的彙編代碼
[Asm] 纯文本查看 复制代码
Address1:  cmp     edi, 142834h //1321012
Address2:  jz      ActiveAttackSkillAddr
Address3:  cmp     edi, 1E8869h
Address4:  jle     ....


經過比對我們知道技能 1321012 是一個主動攻擊的技能,而查看其他主動技能附近的代碼,都是跳到此label,因此我們可以斷定ActiveAttackSkillAddr就是接著處理攻擊技能的位置,現在思路變得明顯,我們只要找個地方,插入我們自己新增的技能ID,使其跳到攻擊技能處理的位置,或者buff技能的位置即可,下面提供一個簡單的範例:

譬如我們想新增 1321013 這個攻擊技能,我們期望的邏輯代碼是這樣:
[Asm] 纯文本查看 复制代码
	if (v10 <= 2111004)
	{
		if (v10 == 2111004)
			goto LABEL_807;
		if (v10 <= 2101002)
		{
			if (v10 != 2101002)
			{
				if (v10 == 1321012 || v10 == 1321013) // << 我們需要在此住達成一個類似 or 的結果
					goto ActiveAttackSkillAddr;
                         ....
	}


我們可以利用dll注入的方式來完成,或者可利用CE等修改器,為了方便起見我這邊直接展示用C++來patch的代碼,首先新增一個DispatchActiveSkill函數,處理我們要自定義的技能:

[Asm] 纯文本查看 复制代码
 const static int addrActiveAttackSkillAddr = ActiveAttackSkillAddr;
const static int addrOriJumpBack = Address3; //從補丁位置跳回原本要執行的下一行
	void __declspec(naked) DispatchActiveSkill()
	{
		__asm
		{
			cmp edi, 0x142834 //1321012 
			jz JumpToAttackSkillAddr
			cmp edi, 0x142835 //1321013
			jz JumpToAttackSkillAddr
			jmp [addrOriJumpBack] //如果技能不存在於以上的列表中,跳回原本的彙編處,讓客戶端自行處理

		JumpToAttackSkillAddr: 
			jmp[addrActiveAttackSkillAddr ]
		}
	}


接著我們在上述展示的彙編中,把原先的代碼改為轉跳到我們自定義的函數:
[Asm] 纯文本查看 复制代码
Address1:  jmp DispatchActiveSkill
Address2:  ?? ?? // 這邊的代碼可能會損毀,是正常的,因為彙編指令常不不同
Address3:  cmp     edi, 1E8869h //此處回到客戶端原先的代碼
Address4:  jle     ....


注意到這個土法煉鋼的方法,在神之子職業尚未加入前的客戶端會有工作量稍大的問題,原因在於舊版客戶端處理技能貌似是使用if條件式(反正根據彙編出來的結果是這樣),因此會有技能ID分散的問題,在新版的客戶端中我們在彙編會看到變成一系列的switch-case,比較好處理,也有可能是不同編譯器優化的結果。

第二部分,新增技能對應的動作
第一部分中我們已經完成了技能新增,但有一些技能有自己的動作動畫,如果沒有新增對應的動作,會發現角色使用技能時,會站在原地一動也不動的施放技能。

動作的動畫png存在於Character.wz中,但此處依然存在一個問題,雖然我們可以把動作檔案加入到Character.wz中,但是施放技能後會發現情況不變,原因跟新增技能差不多,也就是官方在代碼中將這些功能寫死的,無法自行新增。

首先為了方便我們之後的patch工程,我們先大概看一下會使用到的幾個結構與全局變數:

[C++] 纯文本查看 复制代码

struct _bstr_t { //這東西也能參照原本win32裡面的_bstr_t,但為了保持與客戶端絕對一致,我這邊還是展示逆向工程版
	void *pStr;
	int n1 = 0, nRefCounter = 1;
}

struct ActionData
{
	_bstr_t * pBSTR
	int n1 = 0,
		n2 = 1,
		n3 = 0,
		n4 = 0,
		n5 = 0; //n5是一個指向 ActionData::Piece 的指針,也可以改為void *pPiece
};

const int g_nMaxCharacterActionCount = 313; //在我的版本中,玩家能用的動作數量為312, 多一個1作為nullptr臨界
const int g_nMaxTamingMobActionCount = 313; //對應使用坐騎時,能展現的動作數量
const int g_nMaxMorphActionCount = 58; //玩家使用變身技能(或道具)時,能展現的動作數量

ActionData s_aCharacterActionData[g_nMaxCharacterActionCount + g_nMaxTamingMobActionCount  + g_nMaxMorphActionCount]; //存放所有的動作


接著我們看看s_aCharacterActionData是如何被初始化的,查找一下xref,我們看到一個名為dynamic_initializer_for__s_aCharacterActionData__的函數(一樣是根據比對出來的函數名)
[C++] 纯文本查看 复制代码
auto& sWalkBSTR = g_stringPool->GetBSTR(e_StringPool_WalkAction);
s_aCharacterActionData[e_ActionData_walk] = ActionData(0, 0, sWalkBSTR);
...
auto& sDarkImpaleBSTR = g_stringPool->GetBSTR(e_StringPool_DarkImpale);
s_aCharacterActionData[e_ActionData_darkImpale] = ActionData(0, 0, sDarkImpaleBSTR );
... 以此類推 ... 


其中g_stringPool是客戶端存放字串的單例字串管理員,在這邊的話e_StringPool_WalkAction對應的字串其實就是 "walk",而sDarkImpaleBSTR就是 "darkImpale",這都可以在Character.wz/0002000.img 找到對應的節點,在這邊我們知道了一件事情,官方初始化所有動作的時候是一一創建,而非走訪資源檔裡的所有動作節點來初始化。

現在對大的問題在於s_aCharacterActionData是一個靜態陣列,也就是說他長度固定,我們不能隨意拓展(原因請自行參考PE文件格式),也許能用其他的程式來修改PE文件進而擴大陣列空間,但需要

1. 可完美執行的脫殼後客戶端
2. 完整移除CRC檢測的客戶端 (由於整個文件改動,無法使用鏡像bypass法)
3. 在該陣列以後的變數定址需要重新修正(我不確定擴展靜態陣列空間的程式能不能完美做到這點)

因此我們採用另一種較為彈性的方法,就是重新導向所有指向s_aCharacterActionData的代碼,在此我們依然採用dll注入的方式,首先我們先新增一個自定義動作的陣列,並且複製已經初始化的ActionData到此陣列:

[C++] 纯文本查看 复制代码
const int g_nMaxCharacterActionCount = 313;
const int g_nCustomCharacterActionCount = 400;
ActionData aCustomeActionArray[g_nCustomCharacterActionCount];


在動作資源載入後任意區域插入  memcpy(aCustomActionArray, s_aCharacterActionData, sizeof(ActionData) * g_nMaxCharacterActionCount);  
接著我們只需要按照預設的動作建構方式來建構物件(直接呼叫ActionData建構子即可)並塞入我們自訂義陣列即可。

注意到一件事情,我們只重建一般動作的部分,坐騎動作與變身動作保留預設,代碼部分不影響,因為他們的基址是 &s_aCharacterActionData[g_nMaxCharacterActionCount] 與  &s_aCharacterActionData[g_nMaxCharacterActionCount + g_nMaxTamingMobActionCount],而我們只要重新導向基指為&s_aCharacterActionData[0] ( = s_aCharacterActionData本身) 的部分,要查詢所有引用,可以採用兩種方式

1. 使用IDA查詢xref,除了找到的xrefs之外,要另外自行搜尋引用到 s_aCharacterActionData + 4、s_aCharacterActionData + 8、s_aCharacterActionData + 0x0C、 + s_aCharacterActionData + 0x10 的地址

2. 使用ollydbg設置訪問斷點,一樣分別設在s_aCharacterActionData + 0、s_aCharacterActionData + 4、s_aCharacterActionData + 8、s_aCharacterActionData + 0x0C、 + s_aCharacterActionData + 0x10 使用這種方式,要確保客戶端操作的時候能訪問到所有地址,建議搭配(1)。

接著我們修改 get_action_code_from_name (這個是根據動作名稱反查動作在陣列中index的函數)
[Asm] 纯文本查看 复制代码
int get_action_code_from_name(Ztl_bstr_t bsName)
{
	int nRet = 0;
	auto pIter = s_aCharacterActionData; //將這邊改為 aCustomeActionArray
	while(pIter < &s_aCharacterActionData[g_nMaxCharacterActionCount ]) //這邊改為 aCustomeActionArray[g_nCustomCharacterActionCount]
 	{
		if(pIter->pBSTR == bsName.m_Data)
			return nRet;
		++nRet;
	}
	return -1;
}


接著反查所有使用到 g_nMaxCharacterActionCount 的函數,將其修改為你自訂義的動作總數量 + 1,譬如我新增了兩個動作那就是 312(預設) + 2 + 1 (這點我在我自己原本的文章中打錯了),舉例在CActionMan::Init中初始化所有技能對應的index函數,就使用到了這個全局變數:
[Asm] 纯文本查看 复制代码
int nAction = 0;
do
{
	auto& actionData = s_aCharacterActionData[nAction];
	if(actionData[2])
	{
		....
		get_action_code_from_name(...)
		....
	}
	....
} while(nAction < g_nMaxCharacterActionCount); //修改此處


再來就是最麻煩的部分了,遊戲中有些地方不直接使用ActionData,而是利用ActionInfo保存並存取這些數據:
[C++] 纯文本查看 复制代码
struct ActionInfo
{
	int someIntegers[6]; //在台版v13x~v15x中這邊固定為6個ints,每個版本可能不同
	CharacterInfoFrameEntry* aaAction[g_nMaxCharacterActionCount]; //this + 36
	TamingMobActionFrameEntry* aaTamingAction[g_nMaxTamingMobActionCount]; //this + 36 + 313(aaAction) * 4(指針大小)
	MorphActionFrameEntry* aaMorph[g_nMaxMorphActionCount]; //this + 36 + (313 * 4 = aaAction) + (313 * 4 = aaTamingAction)
}


由於aaAction是member data,我們無法直接的將其導向為自定義的陣列,加上存放ActionInfo的地方基本都是一般變數而非指標,所以要重新導向引用ActionInfo的代碼也變得方常困難,因此我在這邊示範另一種方法,就是犧牲aaTamingAction的空間,將aaAction的範圍拓展到 aaAction + aaTamingAction 大小,並且重導所有 aaTamingAction 的引用(因為它的引用量較少,因此能減少工作量),因此邏輯上我們對ActionInfo的看法變成:
[C++] 纯文本查看 复制代码
struct ActionInfo
{
	int someIntegers[6]; //在台版v13x~v15x中這邊固定為6個ints,每個版本可能不同
	CharacterInfoFrameEntry* aaAction[g_nMaxCharacterActionCount * 2]; //this + 36
	MorphActionFrameEntry* aaMorph[g_nMaxMorphActionCount]; //this + 36 + (313 * 4) + (313 * 4)
}


(注意,這只是邏輯上看法的改變,實際大小不變)
實際上引用aaAction以及aaMorphAction的代碼沒有改變,而且我們也能引用下標超過312的aaAction:
[C++] 纯文本查看 复制代码
auto& aiAction = pAvatar->m_aiAction[nIndex]; //aiAction = ActionInfoauto& action1 = aiAction.aaAction[499]; //沒問題,引用上限 = 313 + 312
auto& action2 = aiAction.aaMorphAction[10]; //沒問題


在ActionInfo::ActionInfo初始化所有aaMorph之後,我們重建一個外部的aaTamingAction:
[Asm] 纯文本查看 复制代码
	

//在aaMorph建構完畢後直接patch這段代碼
pExternalArray = new char(sizeof(void*) * 313);
	memcpy(pExternalArray, aaTamingAction, sizeof(void*) * 313);


剩餘的工作很簡單,找出aaTamingAction的xref,並將其導向我們外部的陣列即可
[Asm] 纯文本查看 复制代码
//找出原先為這樣的代碼
auto& action3 = aiAction.aaTamingAction[300]; 
//大概會是這樣的彙編:
lea edx, [esi + ecx * 4 + aaTamingAction的偏移地址]

//修改為這樣
auto& action4 = pExternalArray[300];
mov esi, [pExternalArray]
lea edx, [esi + ecx * 4]

//記得保存esi( = this指標)
Hmily 发表于 2020-12-25 11:16
I D:tnsc4502

申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。
tnsc4502 发表于 2020-12-25 14:07
來報到了,謝謝版主批准
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-14 14:36

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表