2023-02-06 20:27:42 +02:00
|
|
|
import idaapi
|
|
|
|
import ida_ida
|
|
|
|
import ida_bytes
|
|
|
|
import ida_search
|
|
|
|
|
|
|
|
packet_names = {
|
2023-05-23 13:18:54 +01:00
|
|
|
2: 'Ping', # note: could be +1
|
|
|
|
3: 'Init', # note: could be +1
|
|
|
|
8: 'Logout', # note: could be +1
|
|
|
|
11: 'CFCancel', # note: could be +1
|
|
|
|
13: 'CFDutyInfo', # note: could be +1
|
|
|
|
14: 'CFNotify', # note: could be +1
|
|
|
|
18: 'CFPreferredRole', # note: could be +1
|
|
|
|
81: 'CrossWorldLinkshellList', # note: could be +1
|
|
|
|
89: 'FellowshipList', # note: could be +1
|
|
|
|
111: 'Playtime', # note: could be +1
|
|
|
|
112: 'CFRegistered', # note: could be +1
|
|
|
|
115: 'Chat', # note: could be +1
|
|
|
|
122: 'RSVData',
|
|
|
|
123: 'RSFData',
|
|
|
|
124: 'SocialMessage',
|
|
|
|
125: 'SocialMessage2',
|
|
|
|
127: 'SocialList',
|
|
|
|
128: 'SocialRequestResponse',
|
|
|
|
129: 'ExamineSearchInfo',
|
|
|
|
130: 'UpdateSearchInfo',
|
|
|
|
131: 'InitSearchInfo',
|
|
|
|
132: 'ExamineSearchComment',
|
|
|
|
135: 'ServerNoticeShort',
|
|
|
|
136: 'ServerNotice',
|
|
|
|
137: 'SetOnlineStatus',
|
|
|
|
138: 'LogMessage',
|
|
|
|
142: 'Countdown',
|
|
|
|
143: 'CountdownCancel',
|
|
|
|
148: 'PartyMessage',
|
|
|
|
150: 'PlayerAddedToBlacklist',
|
|
|
|
151: 'PlayerRemovedFromBlacklist',
|
|
|
|
152: 'BlackList',
|
|
|
|
159: 'LinkshellList',
|
|
|
|
159: 'MailDeleteRequest',
|
|
|
|
164: 'MarketBoardItemListingCount',
|
|
|
|
165: 'MarketBoardItemListing',
|
|
|
|
167: 'MarketBoardPurchase',
|
|
|
|
169: 'MarketBoardItemListingHistory',
|
|
|
|
170: 'RetainerSaleHistory',
|
|
|
|
172: 'MarketBoardSearchResult',
|
|
|
|
174: 'FreeCompanyInfo',
|
|
|
|
176: 'ExamineFreeCompanyInfo',
|
|
|
|
177: 'FreeCompanyDialog',
|
|
|
|
202: 'StatusEffectList',
|
|
|
|
203: 'StatusEffectListEureka',
|
|
|
|
204: 'StatusEffectListBozja',
|
|
|
|
205: 'StatusEffectListDouble',
|
|
|
|
207: 'EffectResult1',
|
|
|
|
208: 'EffectResult4',
|
|
|
|
209: 'EffectResult8',
|
|
|
|
210: 'EffectResult16',
|
|
|
|
212: 'EffectResultBasic1',
|
|
|
|
213: 'EffectResultBasic4',
|
|
|
|
214: 'EffectResultBasic8',
|
|
|
|
215: 'EffectResultBasic16',
|
|
|
|
216: 'EffectResultBasic32',
|
|
|
|
217: 'EffectResultBasic64',
|
|
|
|
218: 'ActorControl',
|
|
|
|
219: 'ActorControlSelf',
|
|
|
|
220: 'ActorControlTarget',
|
|
|
|
221: 'UpdateHpMpTp',
|
|
|
|
222: 'ActionEffect1',
|
|
|
|
225: 'ActionEffect8',
|
|
|
|
226: 'ActionEffect16',
|
|
|
|
227: 'ActionEffect24',
|
|
|
|
228: 'ActionEffect32',
|
|
|
|
231: 'StatusEffectListPlayer',
|
|
|
|
233: 'UpdateRecastTimes',
|
|
|
|
235: 'UpdateAllianceNormal',
|
|
|
|
236: 'UpdateAllianceSmall',
|
|
|
|
237: 'UpdatePartyMemberPositions',
|
|
|
|
238: 'UpdateAllianceNormalMemberPositions',
|
|
|
|
239: 'UpdateAllianceSmallMemberPositions',
|
|
|
|
241: 'GCAffiliation',
|
|
|
|
260: 'SpawnPlayer',
|
|
|
|
261: 'SpawnNPC',
|
|
|
|
262: 'SpawnBoss',
|
|
|
|
263: 'DespawnCharacter',
|
|
|
|
264: 'ActorMove',
|
|
|
|
266: 'Transfer',
|
|
|
|
267: 'ActorSetPos',
|
|
|
|
269: 'ActorCast',
|
|
|
|
270: 'PlayerUpdateLook',
|
|
|
|
271: 'UpdateParty',
|
|
|
|
272: 'InitZone',
|
|
|
|
273: 'ApplyIDScramble',
|
|
|
|
274: 'UpdateHate',
|
|
|
|
275: 'UpdateHater',
|
|
|
|
276: 'SpawnObject',
|
|
|
|
277: 'DespawnObject',
|
|
|
|
278: 'UpdateClassInfo',
|
|
|
|
279: 'UpdateClassInfoEureka',
|
|
|
|
280: 'UpdateClassInfoBozja',
|
|
|
|
281: 'PlayerSetup',
|
|
|
|
282: 'PlayerStats',
|
|
|
|
283: 'FirstAttack',
|
|
|
|
284: 'PlayerStateFlags',
|
|
|
|
285: 'PlayerClassInfo',
|
|
|
|
287: 'ModelEquip',
|
|
|
|
288: 'Examine',
|
|
|
|
291: 'CharaNameReq',
|
|
|
|
295: 'RetainerInformation',
|
|
|
|
297: 'ItemMarketBoardInfo',
|
|
|
|
299: 'ItemInfo',
|
|
|
|
300: 'ContainerInfo',
|
|
|
|
301: 'InventoryTransactionFinish',
|
|
|
|
302: 'InventoryTransaction',
|
|
|
|
303: 'CurrencyCrystalInfo',
|
|
|
|
305: 'InventoryActionAck',
|
|
|
|
306: 'UpdateInventorySlot',
|
|
|
|
308: 'OpenTreasure',
|
|
|
|
311: 'LootMessage',
|
|
|
|
315: 'CreateTreasure',
|
|
|
|
316: 'TreasureFadeOut',
|
|
|
|
317: 'HuntingLogEntry',
|
|
|
|
319: 'EventPlay',
|
|
|
|
320: 'EventPlay4',
|
|
|
|
321: 'EventPlay8',
|
|
|
|
322: 'EventPlay16',
|
|
|
|
323: 'EventPlay32',
|
|
|
|
324: 'EventPlay64',
|
|
|
|
325: 'EventPlay128',
|
|
|
|
326: 'EventPlay255',
|
|
|
|
328: 'EventStart',
|
|
|
|
329: 'EventFinish',
|
|
|
|
340: 'EventContinue',
|
|
|
|
342: 'ResultDialog',
|
|
|
|
343: 'DesynthResult',
|
|
|
|
348: 'QuestActiveList',
|
|
|
|
349: 'QuestUpdate',
|
|
|
|
350: 'QuestCompleteList',
|
|
|
|
351: 'QuestFinish',
|
|
|
|
354: 'MSQTrackerComplete',
|
|
|
|
366: 'QuestTracker',
|
|
|
|
387: 'Mount',
|
|
|
|
389: 'DirectorVars',
|
|
|
|
390: 'ContentDirectorSync',
|
|
|
|
392: 'EnvControl',
|
|
|
|
398: 'SystemLogMessage1',
|
|
|
|
399: 'SystemLogMessage2',
|
|
|
|
400: 'SystemLogMessage4',
|
|
|
|
401: 'SystemLogMessage8',
|
|
|
|
402: 'SystemLogMessage16',
|
|
|
|
404: 'BattleTalk2',
|
|
|
|
405: 'BattleTalk4',
|
|
|
|
406: 'BattleTalk8',
|
|
|
|
408: 'MapUpdate',
|
|
|
|
409: 'MapUpdate4',
|
|
|
|
410: 'MapUpdate8',
|
|
|
|
411: 'MapUpdate16',
|
|
|
|
412: 'MapUpdate32',
|
|
|
|
413: 'MapUpdate64',
|
|
|
|
414: 'MapUpdate128',
|
|
|
|
416: 'BalloonTalk2',
|
|
|
|
417: 'BalloonTalk4',
|
|
|
|
418: 'BalloonTalk8',
|
|
|
|
420: 'WeatherChange',
|
|
|
|
421: 'PlayerTitleList',
|
|
|
|
422: 'Discovery',
|
|
|
|
424: 'EorzeaTimeOffset',
|
|
|
|
437: 'EquipDisplayFlags',
|
|
|
|
438: 'NpcYell',
|
|
|
|
443: 'FateInfo',
|
|
|
|
457: 'LandSetInitialize',
|
|
|
|
458: 'LandUpdate',
|
|
|
|
459: 'YardObjectSpawn',
|
|
|
|
460: 'HousingIndoorInitialize',
|
|
|
|
461: 'LandAvailability',
|
|
|
|
463: 'LandPriceUpdate',
|
|
|
|
464: 'LandInfoSign',
|
|
|
|
465: 'LandRename',
|
|
|
|
466: 'HousingEstateGreeting',
|
|
|
|
467: 'HousingUpdateLandFlagsSlot',
|
|
|
|
468: 'HousingLandFlags',
|
|
|
|
469: 'HousingShowEstateGuestAccess',
|
|
|
|
471: 'HousingObjectInitialize',
|
|
|
|
472: 'HousingInternalObjectSpawn',
|
|
|
|
474: 'HousingWardInfo',
|
|
|
|
475: 'HousingObjectMove',
|
|
|
|
476: 'HousingObjectDye',
|
|
|
|
488: 'SharedEstateSettingsResponse',
|
|
|
|
500: 'DailyQuests',
|
|
|
|
502: 'DailyQuestRepeatFlags',
|
|
|
|
504: 'LandUpdateHouseName',
|
|
|
|
515: 'AirshipTimers',
|
|
|
|
518: 'PlaceMarker',
|
|
|
|
519: 'WaymarkPreset',
|
|
|
|
520: 'Waymark',
|
|
|
|
523: 'UnMount', # note: could be up to -2
|
|
|
|
526: 'CeremonySetActorAppearance', # note: could be up to -2
|
|
|
|
532: 'AirshipStatusList', # note: could be up to -2
|
|
|
|
533: 'AirshipStatus', # note: could be up to -2
|
|
|
|
534: 'AirshipExplorationResult', # note: could be up to -2
|
|
|
|
535: 'SubmarineStatusList', # note: could be up to -2
|
|
|
|
536: 'SubmarineProgressionStatus', # note: could be up to -2
|
|
|
|
537: 'SubmarineExplorationResult', # note: could be up to -2
|
|
|
|
539: 'SubmarineTimers', # note: could be up to -2
|
|
|
|
569: 'PrepareZoning', # note: could be up to -1
|
|
|
|
570: 'ActorGauge',
|
|
|
|
571: 'CharaVisualEffect',
|
|
|
|
572: 'LandSetMap',
|
|
|
|
573: 'Fall',
|
|
|
|
622: 'PlayMotionSync',
|
|
|
|
631: 'CEDirector',
|
|
|
|
653: 'IslandWorkshopSupplyDemand',
|
2023-02-06 20:27:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
def find_next_func_by_sig(ea, pattern):
|
|
|
|
return ida_search.find_binary(ea, ida_ida.inf_get_max_ea(), pattern, 16, ida_search.SEARCH_DOWN)
|
|
|
|
|
|
|
|
def find_single_func_by_sig(pattern):
|
|
|
|
ea_first = find_next_func_by_sig(ida_ida.inf_get_min_ea(), pattern)
|
|
|
|
if ea_first == idaapi.BADADDR:
|
|
|
|
print(f'Could not find function by pattern {pattern}')
|
|
|
|
return 0
|
|
|
|
if find_next_func_by_sig(ea_first + 1, pattern) != idaapi.BADADDR:
|
|
|
|
print(f'Multiple functions match pattern {pattern}')
|
|
|
|
return 0
|
|
|
|
return ea_first
|
|
|
|
|
|
|
|
def read_signed_byte(ea):
|
|
|
|
v = ida_bytes.get_byte(ea)
|
|
|
|
return v - 0x100 if v & 0x80 else v
|
|
|
|
|
|
|
|
def read_signed_dword(ea):
|
|
|
|
v = ida_bytes.get_dword(ea)
|
|
|
|
return v - 0x100000000 if v & 0x80000000 else v
|
|
|
|
|
|
|
|
def read_rva(ea):
|
|
|
|
return ea + 4 + read_signed_dword(ea)
|
|
|
|
|
|
|
|
def get_vfoff_for_body(body):
|
|
|
|
# assume each case has the following body:
|
|
|
|
# mov rax, [rcx]
|
|
|
|
# lea r9, [r10+10h]
|
|
|
|
# jmp qword ptr [rax+<vfoff>]
|
|
|
|
if ida_bytes.get_byte(body) != 0x48 or ida_bytes.get_byte(body + 1) != 0x8B or ida_bytes.get_byte(body + 2) != 0x01:
|
|
|
|
return -1
|
|
|
|
if ida_bytes.get_byte(body + 3) != 0x4D or ida_bytes.get_byte(body + 4) != 0x8D or ida_bytes.get_byte(body + 5) != 0x4A or ida_bytes.get_byte(body + 6) != 0x10:
|
|
|
|
return -1
|
|
|
|
if ida_bytes.get_byte(body + 7) != 0x48 or ida_bytes.get_byte(body + 8) != 0xFF:
|
|
|
|
return -1
|
|
|
|
sz = ida_bytes.get_byte(body + 9)
|
|
|
|
if sz == 0x60:
|
|
|
|
return read_signed_byte(body + 10)
|
|
|
|
elif sz == 0xA0:
|
|
|
|
return read_signed_dword(body + 10)
|
|
|
|
else:
|
|
|
|
return -1
|
|
|
|
|
|
|
|
def vfoff_to_index(vfoff):
|
|
|
|
if vfoff < 0x10:
|
|
|
|
return -1 # first two vfs are dtor and exec
|
|
|
|
if (vfoff & 7) != 0:
|
|
|
|
return -1 # vf contains qwords
|
|
|
|
return (vfoff >> 3) - 2
|
|
|
|
|
|
|
|
class ffnetwork(idaapi.plugin_t):
|
|
|
|
flags = idaapi.PLUGIN_UNL
|
|
|
|
comment = 'Build opcode map'
|
|
|
|
help = ''
|
|
|
|
wanted_name = 'ffnetwork'
|
|
|
|
wanted_hotkey = ''
|
|
|
|
|
2023-05-23 12:05:45 +01:00
|
|
|
_unknown_in_output = False
|
|
|
|
|
2023-02-06 20:27:42 +02:00
|
|
|
def init(self):
|
|
|
|
return idaapi.PLUGIN_OK
|
|
|
|
|
|
|
|
def run(self, arg=None):
|
|
|
|
# assume func starts with:
|
|
|
|
# mov rax, [r8+10h]
|
|
|
|
# mov r10, [rax+38h]
|
|
|
|
# movzx eax, word ptr [r10+2]
|
|
|
|
# add eax, -<min_case>
|
|
|
|
# cmp eax, <max_case-min_case>
|
|
|
|
# ja <default_off>
|
|
|
|
# lea r11, <__ImageBase_off>
|
|
|
|
# cdqe
|
|
|
|
# mov r9d, ds::<jumptable_rva>[r11+rax*4]
|
|
|
|
func = find_single_func_by_sig('49 8B 40 10 4C 8B 50 38 41 0F B7 42 02 83 C0 ?? 3D ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 4C 8D 1D ?? ?? ?? ?? 48 98 45 8B 8C 83 ?? ?? ?? ??')
|
|
|
|
if func == 0:
|
|
|
|
return
|
|
|
|
min_case = -read_signed_byte(func + 15) # this is a negative
|
|
|
|
jumptable_size = read_signed_dword(func + 17) + 1
|
|
|
|
def_addr = read_rva(func + 23)
|
|
|
|
imagebase = read_rva(func + 30)
|
|
|
|
jumptable = imagebase + read_signed_dword(func + 40)
|
|
|
|
opcodemap = {}
|
|
|
|
for i in range(jumptable_size):
|
|
|
|
body = imagebase + read_signed_dword(jumptable + 4 * i)
|
|
|
|
if body == def_addr:
|
|
|
|
continue
|
|
|
|
case = i + min_case
|
|
|
|
voff = get_vfoff_for_body(body)
|
|
|
|
index = vfoff_to_index(voff)
|
|
|
|
if index < 0:
|
|
|
|
print(f'Unexpected body for case {case}')
|
|
|
|
continue
|
|
|
|
if index in opcodemap:
|
|
|
|
print(f'Multiple opcodes map to single operation {index}: {hex(opcodemap[index])} and {hex(case)}')
|
|
|
|
continue
|
|
|
|
opcodemap[index] = case
|
|
|
|
for k, v in sorted(opcodemap.items()):
|
2023-05-23 12:05:45 +01:00
|
|
|
if k in packet_names:
|
|
|
|
print(f'{packet_names[k]} = 0x{v:0{4}X},')
|
|
|
|
elif self._unknown_in_output:
|
|
|
|
print(f'Packet{k} = 0x{v:0{4}X},')
|
2023-02-06 20:27:42 +02:00
|
|
|
|
|
|
|
def term(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def PLUGIN_ENTRY():
|
|
|
|
return ffnetwork()
|
|
|
|
|