发表于 2020-12-21 19:49

申請會員ID: tnsc4502【申请通过】

1. 申请人ID: tnsc4502

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

(github登入後畫面)


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

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

(RZ論壇登入後畫面)



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

(部落格後台)


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

Hmily 发表于 2020-12-22 09:45

请认真阅读神器内容,把技术文章直接发布到帖子中申请。

发表于 2020-12-22 21:04

好的版主,我會重發,但想請問一下能否幫我遮蔽下email,畢竟是常用信箱,不想公開,謝謝

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)
        if (v10 <= 2111004)
        {
                if (v10 == 2111004)
                        goto LABEL_807;
                if (v10 <= 2101002)
                {
                        if (v10 != 2101002)
                        {
                                if (v10 == 1321012)
                                        goto ActiveAttackSkillAddr;
                         ....
        }

對應的彙編代碼
Address1:cmp   edi, 142834h //1321012
Address2:jz      ActiveAttackSkillAddr
Address3:cmp   edi, 1E8869h
Address4:jle   ....

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

譬如我們想新增 1321013 這個攻擊技能,我們期望的邏輯代碼是這樣:
        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函數,處理我們要自定義的技能:

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 //如果技能不存在於以上的列表中,跳回原本的彙編處,讓客戶端自行處理

                JumpToAttackSkillAddr:
                        jmp
                }
        }


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

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

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

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

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



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; //存放所有的動作


接著我們看看s_aCharacterActionData是如何被初始化的,查找一下xref,我們看到一個名為dynamic_initializer_for__s_aCharacterActionData__的函數(一樣是根據比對出來的函數名)
auto& sWalkBSTR = g_stringPool->GetBSTR(e_StringPool_WalkAction);
s_aCharacterActionData = ActionData(0, 0, sWalkBSTR);
...
auto& sDarkImpaleBSTR = g_stringPool->GetBSTR(e_StringPool_DarkImpale);
s_aCharacterActionData = 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到此陣列:

const int g_nMaxCharacterActionCount = 313;
const int g_nCustomCharacterActionCount = 400;
ActionData aCustomeActionArray;

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

注意到一件事情,我們只重建一般動作的部分,坐騎動作與變身動作保留預設,代碼部分不影響,因為他們的基址是 &s_aCharacterActionData 與&s_aCharacterActionData,而我們只要重新導向基指為&s_aCharacterActionData ( = 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的函數)
int get_action_code_from_name(Ztl_bstr_t bsName)
{
        int nRet = 0;
        auto pIter = s_aCharacterActionData; //將這邊改為 aCustomeActionArray
        while(pIter < &s_aCharacterActionData) //這邊改為 aCustomeActionArray
        {
                if(pIter->pBSTR == bsName.m_Data)
                        return nRet;
                ++nRet;
        }
        return -1;
}

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


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

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

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


在ActionInfo::ActionInfo初始化所有aaMorph之後,我們重建一個外部的aaTamingAction:
       

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


剩餘的工作很簡單,找出aaTamingAction的xref,並將其導向我們外部的陣列即可
//找出原先為這樣的代碼
auto& action3 = aiAction.aaTamingAction;
//大概會是這樣的彙編:
lea edx,

//修改為這樣
auto& action4 = pExternalArray;
mov esi,
lea edx,

//記得保存esi( = this指標)

Hmily 发表于 2020-12-25 11:16

I D:tnsc4502

申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。

tnsc4502 发表于 2020-12-25 14:07

來報到了,謝謝版主批准
页: [1]
查看完整版本: 申請會員ID: tnsc4502【申请通过】