作者:无聊的菜鸟
时间:2009年3月
声明:“知道不?其实懒惰才是人类进步的源动力!”

    山口山,大家都知道的,这里就不介绍了。
    先说下为什么我会写下这些。首先吧,我不会写的很详细,至少不会直接给出Patch之后的代码。因为拿来主义总让我觉得变扭;其次,不写出来吧,总觉得憋着荒……
    山口上自某个补丁之后,加入了一些额外的代码,外在的表现是减少了#132错误(违规访问内存)。但是,实际效果是私服自己制作的自定义物品(就是不是暴雪开放的装备)的图标全部显示为问号(图片路径为INV_MISC_QestionMark),也无法直接在背包中右击装备,同时无法自己对自己的自定义装备进行附魔,以及左键点击拿起这些物品后图标显示为问号(图片路径同样为INV_MISC_QestionMark)。而这些问题在官方物品上是不存在的。
    当然,你会说也许私服传递的数据不正确才导致这样的。这句话有道理,在补丁和检验的过程中,不同的私服服务器端传递下来的数据的正确性差异很大。但是这并不是主要的,因为这些自定义装备在那个补丁之前同样没有这些问题,而现在,即使有这这些问题,这些物品依然可以通过拖放的方法将其装备在正确的位置,并且装备在角色身上之后,在角色信息页面中图标显示的也是正确的。所以我们得出的结论是,客户端中有限制代码。
--------------------------无辜的分割线---------------------------
插件部分(Lua):
    其实最开始的时候,我是去google了解决方法,结果下下来了一个插件ItemPatch,记得好像是在zgwow.cn下的,这个插件里面Hook了GetContainerItemInfo和GetActionTexture两个API,然后自行处理的这个API中出现INV_MISC_QestionMark的情况,具体往下看注释,顺带扫扫盲:

代码:
-- Item's Patch For 2.4.2 Core by W.S :P -- 由W.S为山口山2.4.2版本编写。

WOW_GetContainerItemInfo = GetContainerItemInfo;  --Hook
function GetContainerItemInfo(index, id)
  local texture, itemCount, locked, quality, readable;  --声明本地变量
  texture, itemCount, locked, quality, readable = WOW_GetContainerItemInfo(index, id);
  if( texture and string.find(texture,"INV_Misc_QuestionMark") ) then
    -- DEFAULT_CHAT_FRAME:AddMessage("GetContainerItemInfo Item "..itemid);
    local itemlink = GetContainerItemLink(index, id);
  --调用API_GetContainerItemLink获取这个显示为问号的物品的物品链接。
    local itemid = 0;
    if( itemlink ) then
      _, _, itemid = string.find(itemlink, "Hitem:(%d+):");
  --在物品链接字符串中查找物品ID。实际为"Hitem:"字符串之后的数字。
      texture = string.gsub(texture,"INV_Misc_QuestionMark", My_GetItemTexture(itemid) );
   --调用下一级函数
      -- DEFAULT_CHAT_FRAME:AddMessage( itemlink.." - ID: "..itemid.." - Texture: "..texture );
    end
  end
  return texture, itemCount, locked, quality, readable;
end
--说到这里,其实在后面IDA山口山的时候,我们能发现其实有直接实现的GetItemID函数,
--但是这个函数没有被导出为API……而且嘛,过程其实是一样的……
function My_GetItemTexture( itemid )
  if MY_ITEM_TEXTURE[ itemid ] ~= nil and MY_ITEM_TEXTURE[ itemid ] ~= "" then
   --枚举下面数组中的ID对比,典型的查表法。
    return MY_ITEM_TEXTURE[ itemid ];
  end
  return "INV_Misc_QuestionMark";
end

MY_ITEM_TEXTURE = {};
MY_ITEM_TEXTURE = 
{
-- ["物品id"] = "INV_XXX_XXX",
["1"] = "INV_Qiraj_JewelGlyphed",
["2"] = "INV_Qiraj_JewelGlyphed",
......中间省略XX千字节.....
["6051190"] = "INV_Scroll_04",
["1401019"] = "INV_Trinket_HonorHold",

}
     这个插件代码不多,但是由于是查表法,最后插件体积高达604KB……

------------------------------------熟悉的分割线--------------------------------
改进插件:
    大家都看见了,枚举起来很麻烦,每次自己做了新物品都要修改这个插件的数组,不然就是一个大大的问号。作为用户而言,也很麻烦,时不时地要来更新一次。而且更大的缺点是,这个玩意不通用的,把它用在其它私服上之后,你的1号物品是不是和他的1号物品一样呢?这个问题很有研究意义啊。
    在我对山口山的API手册研究了之后(google到的),我觉得其实可以不用枚举法。再Hook了GetActionTexture、GetContainerItemInfo、GetSpellTexture、GetInventoryItemTexture之后,界面上明显的地方已经没有问号了,不过自定义物品的使用、自身附魔以及拿在鼠标上依然是问号的问题并没有得到解决。我还是举个修改之后的例子:
    (什么?你想要全部的?这个么,嘛嘛。不要想了。我故意的……)

代码:
function GetContainerItemInfo(index, id)  --我们依然以这个函数为例
  local texture, itemCount, locked, quality, readable = WOW_GetContainerItemInfo(index, id);

  if( texture and string.find(texture,"INV_Misc_QuestionMark") ) then
    local itemlink = GetContainerItemLink(index, id);
    local itemid = 0;
    if( itemlink ) then --如果获得的物品链接不是空串
      _, _, itemid = string.find(itemlink, "Hitem:(%d+):"); --获取itemID
      _, _, _, _, _, _, _, _, __, texture = GetItemInfo(itemid);  --使用API_GetItemInfo来获取路径
    end
  end
  return texture, itemCount, locked, quality, readable;  --返回需要的东西
end
    这里我们就没有使用枚举,因为想下也会知道,自定义物品在NPC窗口中出售,在身上装备了之后,他的图标是可以正确显示的,所以API中肯定有能返回正确图标的函数。

--------------------------------好熟悉的分割线----------------------------------------
山口山部分:
    下面我们该到主程序的修改部分了,因为插件虽然好,但是有些东西却不能用插件来实现。例如:当你使用山口山登录英国的私服(NaxpServer.com),看见一片的服务器想选择一个,然后山口山告诉你:“你正在使用不同语言版本的山口山服务器”。其实这个检测也是在插件中实现的,调用的API是IsInvalidLocale。不过这个函数你却无法用插件Hook。为什么呢?人家是BLZ自己写的插件,加载的比你早,在你的插件被读取编译之前,这个检测就已经过了。而且BLZ的插件你还不能修改,改了他就不给你启动山口山。
    然后这里需要介绍下山口山的API导出表:
    用OD加载山口山之后,打开内存视图,找到.rdata区段,然后再这个区段中搜索你要的API的名字,比如IsInvalidLocale,找到:
代码:
0093D370  49 73 49 6E 76 61 6C 69 64 4C 6F 63 61 6C 65 00  IsInvalidLocale.
    然后再内存视图中找到.data区段,在其中搜索这个字符串指针0093d370,找到:
代码:
00FC1D68  70 D3 93 00                                      p.@
    在把视图转为长型ASCII数据地址,如下:
代码:
00FC1D40  0093D3F0  鹩?    ASCII "SortRealms"
00FC1D44  00476D70  pmG.  WoW.00476D70
00FC1D48  0093D3DC  苡?    ASCII "GetSelectedCategory"
00FC1D4C  00476E40  @nG.  WoW.00476E40
00FC1D50  0093D3C0  烙?    ASCII "RealmListDialogCancelled"
00FC1D54  00475E10  ^G.  WoW.00475E10
00FC1D58  0093D39C  ?    ASCII "IsInvalidTournamentRealmCategory"
00FC1D5C  00476B10  kG.  WoW.00476B10
00FC1D60  0093D380  .   ASCII "IsTournamentRealmCategory"
00FC1D64  00476BC0  G.   WoW.00476BC0
00FC1D68  0093D370  p.   ASCII "IsInvalidLocale"  <<--这里这里
00FC1D6C  00476C40  @lG.  WoW.00476C40                 <<--这个就是IsInvaliLocale的实现代码的地址
00FC1D70  00930B40  @?   WoW.00930B40      <<--空字符串指针,这一段表结束的标记
00FC1D74  00000000  ....
    所以要找一个API的实现也就是这样子找的。
    然后通过查阅API手册,及对可疑函数的确认之后,最后我们把问题锁定在了UseContainerItem上。
    有人说插件HooK不行么?这个API又不是启动时调用的。话这么说没错,可是这个API的类型是protected,代表这个API只能由BLZ的插件来调用。。不支持用户来重写这个函数,而且当你接触了这个API的实现之后,你更不会这么想。
    这个API一共9个分支,通过下断对比自定义物品和标准物品的使用问题,可以得知自定义物品的装备问题是出在第8个分支上:

代码:
0053BEA6  |> \8B7D FC       mov     edi, [local.1]                   ; case 8
0053BEA9  |.  8B57 08       mov     edx, dword ptr [edi+8]
0053BEAC  |.  33C9          xor     ecx, ecx
0053BEAE  |.  51            push    ecx
0053BEAF  |.  51            push    ecx
0053BEB0  |.  51            push    ecx
0053BEB1  |.  894D E8       mov     [local.6], ecx
0053BEB4  |.  894D EC       mov     [local.5], ecx
0053BEB7  |.  8B42 0C       mov     eax, dword ptr [edx+C]
0053BEBA  |.  8D4D E8       lea     ecx, [local.6]
0053BEBD  |.  51            push    ecx
0053BEBE  |.  50            push    eax
0053BEBF  |.  B9 60F31A01   mov     ecx, 011AF360
0053BEC4  |.  E8 17FD0800   call    005CBBE0
0053BEC9  |.  8BCF          mov     ecx, edi
0053BECB  |.  8BF0          mov     esi, eax
0053BECD  |.  E8 CE311100   call    0064F0A0      ;!!非零则可以装备
0053BED2  |.  85C0          test    eax, eax
0053BED4  |.  74 50         je      short 0053BF26
0053BED6  |.  85F6          test    esi, esi
0053BED8  |.  74 09         je      short 0053BEE3
0053BEDA  |.  83BE AC010000>cmp     dword ptr [esi+1AC], 0
0053BEE1  |.  75 43         jnz     short 0053BF26
0053BEE3  |>  833D 78680A01>cmp     dword ptr [10A6878], 0
0053BEEA  |.  75 1B         jnz     short 0053BF07
0053BEEC  |.  8B55 F4       mov     edx, [local.3]
0053BEEF  |.  8B82 08010000 mov     eax, dword ptr [edx+108]
0053BEF5  |.  8B88 D0000000 mov     ecx, dword ptr [eax+D0]
0053BEFB  |.  C1E9 14       shr     ecx, 14
0053BEFE  |.  F6C1 01       test    cl, 1
0053BF01  |.^ 0F84 6AFEFFFF je      0053BD71
0053BF07  |>  8B55 F8       mov     edx, [local.2]
0053BF0A  |.  8B43 0C       mov     eax, dword ptr [ebx+C]
0053BF0D  |.  8B4B 08       mov     ecx, dword ptr [ebx+8]
0053BF10  |.  6A 00         push    0
0053BF12  |.  52            push    edx
0053BF13  |.  50            push    eax
0053BF14  |.  51            push    ecx
0053BF15  |.  8B4D F4       mov     ecx, [local.3]
0053BF18  |.  E8 33A30F00   call    00636250
0053BF1D  |.  5F            pop     edi
0053BF1E  |.  5B            pop     ebx
0053BF1F  |.  33C0          xor     eax, eax
0053BF21  |.  5E            pop     esi
0053BF22  |.  8BE5          mov     esp, ebp
0053BF24  |.  5D            pop     ebp
0053BF25  |.  C3            retn
    然后我们继续来看0064F0A0:
代码:
本地调用来自 004AB0C8, 004AB12F, 004AE0A0, 0053BECD, 0058248E, 006313DA, 0064DD14, 00650A14, 00650BD3, 007713A0

0064F0A0  /$  8B41 08       mov     eax, dword ptr [ecx+8]
0064F0A3  |.  8B40 0C       mov     eax, dword ptr [eax+C]  ;此时eax=itemID
0064F0A6  |.  8B0D 9C01FD00 mov     ecx, dword ptr [FD019C]  ;[FD019C]=11h
0064F0AC  |.  3BC1          cmp     eax, ecx
0064F0AE  |.  7C 1B         jl      short 0064F0CB
0064F0B0  |.  3B05 9801FD00 cmp     eax, dword ptr [FD0198]  ;[FD0198]=AFEDh
0064F0B6  |.  7F 13         jg      short 0064F0CB
0064F0B8  |.  2BC1          sub     eax, ecx
0064F0BA  |.  8B0D AC01FD00 mov     ecx, dword ptr [FD01AC]  ;[00FD01AC]=06371F00h
0064F0C0  |.  8B0481        mov     eax, dword ptr [ecx+eax*4]
0064F0C3  |.  85C0          test    eax, eax
0064F0C5  |.  74 04         je      short 0064F0CB
0064F0C7  |.  8B40 18       mov     eax, dword ptr [eax+18]
0064F0CA  |.  C3            retn
0064F0CB  |>  33C0          xor     eax, eax
0064F0CD  \.  C3            retn
    到这里,根据现有资料,11h(17)表示为客户端数据库中最小的itemid,而AFEDh(45037)则是最大的itemID,这里做出了判断,如果传递近来的物品的ID不再客户端所包含的物品ID范围内则视为未知物品返回0,如果在范围内,则查表1获得一个非零数据返回,最后经验证,此数据为物品的equipSlot,即装备位置:其取值可能为如下之一:
代码:
INVTYPE_2HWEAPON - Two-Hand  双手武器
INVTYPE_AMMO - Ammo 弹药
INVTYPE_BAG - Bag      背包
INVTYPE_BODY - Shirt   衬衣
INVTYPE_CHEST - Chest  胸部
INVTYPE_CLOAK - Back   背部披风
INVTYPE_FEET - Feet     脚
INVTYPE_FINGER - Finger 手指戒指
INVTYPE_HAND - Hands  手
INVTYPE_HEAD - Head   头
INVTYPE_HOLDABLE - Held In Off-hand  副手物品
INVTYPE_LEGS - Legs     腿部
INVTYPE_NECK - Neck    颈部
INVTYPE_QUIVER - Quiver     箭袋
INVTYPE_RANGED - Ranged   远程射击武器
INVTYPE_RANGEDRIGHT - Ranged  魔杖
INVTYPE_RELIC - Relic               圣物
INVTYPE_ROBE - Chest              胸2
INVTYPE_SHIELD - Off Hand       盾
INVTYPE_SHOULDER - Shoulder  肩
INVTYPE_TABARD - Tabard        战袍
INVTYPE_THROWN - Thrown     投掷
INVTYPE_TRINKET - Trinket       饰品
INVTYPE_WAIST - Waist            腰部
INVTYPE_WEAPON - One-Hand   单手
INVTYPE_WEAPONMAINHAND - Main Hand  主手武器
INVTYPE_WEAPONOFFHAND - Off Hand      副手武器
INVTYPE_WRIST - Wrist            手腕
    所以这个函数的名称推测为GetItemEquipSlotForClientItem。而我们需要的则是把这个函数修改成适合所有物品的。
    前面插件中我们注意到GetItemInfo函数是能获取到正确的物品信息,也就是说,这个函数是没有有效物品检查的,通过分析这个函数,我们注意到011AF368的表可以查询到正确的物品信息,所以我们仿写其中的查询方式,即可完美解决这个问题。同时装备附魔的问题也一并解决了,猜测这2个限制公用了这个判断函数

---------------------------------哎,休息休息------------------------
    然后继续对UseContainerItem的分析中,发现鼠标拿起物品之后显示的问号则是发生在第三个分支上:
代码:
0053B702  |> \F687 CC030000>test    byte ptr [edi+3CC], 1
0053B709  |.  75 78         jnz     short 0053B783
0053B70B  |.  8B45 FC       mov     eax, [local.1]
0053B70E  |.  8B4E 0C       mov     ecx, dword ptr [esi+C]
0053B711  |.  8B56 08       mov     edx, dword ptr [esi+8]
0053B714  |.  8B75 E0       mov     esi, [local.8]
0053B717  |.  6A 01         push    1
0053B719  |.  6A 00         push    0
0053B71B  |.  6A 01         push    1
0053B71D  |.  50            push    eax
0053B71E  |.  51            push    ecx
0053B71F  |.  52            push    edx
0053B720  |.  56            push    esi
0053B721  |.  53            push    ebx
0053B722  |.  E8 A928F7FF   call    004ADFD0  
0053B727  |.  56            push    esi
0053B728  |.  53            push    ebx
0053B729  |.  E8 128EF6FF   call    004A4540
0053B72E  |.  83C4 28       add     esp, 28
0053B731  |.  5B            pop     ebx
0053B732  |.  5F            pop     edi
0053B733  |.  33C0          xor     eax, eax
0053B735  |.  5E            pop     esi
0053B736  |.  8BE5          mov     esp, ebp
0053B738  |.  5D            pop     ebp
0053B739  |.  C3            retn
    对这一段代码的跟入发现限制代码位于00652290:
代码:
本地调用来自 004DBCAF, 004DE933, 0050CFB4, 0050D091, 0053A58A, 0053B098, 00548062, 00583E0D

00652290  /$  8B41 08       mov     eax, dword ptr [ecx+8]
00652293  |.  8B40 0C       mov     eax, dword ptr [eax+C]
00652296  |.  8B0D 9C01FD00 mov     ecx, dword ptr [FD019C]
0065229C  |.  3BC1          cmp     eax, ecx
0065229E  |.  7C 24         jl      short 006522C4
006522A0  |.  3B05 9801FD00 cmp     eax, dword ptr [FD0198]
006522A6  |.  7F 1C         jg      short 006522C4
006522A8  |.  2BC1          sub     eax, ecx
006522AA  |.  8B0D AC01FD00 mov     ecx, dword ptr [FD01AC]
006522B0  |.  8B0481        mov     eax, dword ptr [ecx+eax*4]
006522B3  |.  85C0          test    eax, eax
006522B5  |.  74 0D         je      short 006522C4
006522B7  |.  8B40 14       mov     eax, dword ptr [eax+14]  ;!!
006522BA  |.  50            push    eax
006522BB  |.  E8 D0FEFFFF   call    00652190
006522C0  |.  83C4 04       add     esp, 4
006522C3  |.  C3            retn
006522C4  |>  33C0          xor     eax, eax
006522C6  |.  50            push    eax
006522C7  |.  E8 C4FEFFFF   call    00652190
006522CC  |.  83C4 04       add     esp, 4
006522CF  \.  C3            retn
    和前面一段代码不同的是这边!!标准的006522B7行,获取的成员偏移变了,下面还多了一个对00652190函数的调用。通过跟踪对比发现,+14h的成员是Icon的索引号。所以推测这个函数名应该是GetItemIconForClientItem。了解这个之后,我们同样参照前面的代码只要修改下在最后的成员偏移,这个限制也被成功解除。同时也解决了包裹中物品的显示问题。

    到现在插件中只需要保留对GetActionTexture和GetInventoryItemTexture这2个函数的处理即可。其他2个处理可以被删掉。至于这2个函数,当然也可以用patch来解决,但是由于种种原因,能简单点何必那么复杂?

----------------Keep going, going, going, and going.---------------------
    此时这个修改好的山口山主程序在WLK客户端上已经没有问题,但是处于偷懒的愿望,我并不想在布下3.05的客户端,所以我直接复制到了3.05客户端重执行,OK。然后登录的时候我得到了一个提示:“您的账号已经开启了《巫妖王之怒》的权限,但是您所使用的客户端不包括《巫妖王之怒》的内容。如果您想要在这台电脑上进行游戏,就必须安装《巫妖王之怒》客户端。请访问www.wowchina.com下载客户端。”
    无奈的再次开启OD,然后定位到GetClientExpansionLevel,然后对第一个call指向的函数004021A0下断,再登录,很快就能定位相应的判断代码了,这边我就不写了。

收工,好长好长,太膜拜我自己了……