Locating the Packet Distributor (All 2D clients + Pre-Alpha)


Site Admin
Posts: 368
Joined: Wed Apr 08, 2009 6:35 am
PostPosted: Mon Feb 21, 2011 1:34 pm
Today I'm gonna show you a technique I came up with to locate the Packet Distributor in all 2D clients (+ Demo & Pre-Alpha). With Packet Distributor I mean the switch/case statement where the Packet ID is converted to a function inside the EXE. That function is then responsible for handling the actual packet.

My first guess to locate the Packet Distributor involved finding all code references to the Packet Table. But the client changed too much over the years for me to developed a reliable technique. Therefor I decided to brute-force the whole thing and analyze the jump tables. (The official word is Branch Table but I prefer Jump Table, if you don't know what that is, read here : http://en.wikipedia.org/wiki/Branch_table)

IDA Pro put comments where it finds a jump table thus by scanning the whole EXE, storing the addresses IDA commented and then printing the more interesting ones, we can limit the search for the Packet Distributor. This turned out to be a good method, as it worked for Pre-Alpha up to the latest client I tried (7.0.2.x). This is the IDAPython script I used, it will first calculate the average size of the jumptables in the EXE and then print out the ones who are twice above the average.
Code: Select all
ly = []
for f in AllFunctions():
  for x in FuncItems(f):
    if Comment(x) != None and "switch" in Comment(x)  and "cases" in Comment(x):
      y = Comment(x)[:-6][7:]
      ly.append(int(y))
avg = sum(ly) / len(ly)
print "Average = " + str(avg)
for f in AllFunctions():
  for x in FuncItems(f):
    if Comment(x) != None and "switch" in Comment(x)  and "cases" in Comment(x):
      y = Comment(x)[:-6][7:]
      if int(y) > avg*2:
        print Hex(x) + ": " + Comment(x) + "-->" + y
Of course, using this script you get more output than the Packet Distributor, but it's in the list.

Let's look at some of the code found:
Code: Select all
1.23 138 cases + 0x11 = 155 (158 packets in client)
====
mov cl, [eax]
mov [ebp+var14], ecx
mov edx, [ebp+var14]
sub edx, 0x11
mov [ebp+var14], edx
; switch 138 cases

Code: Select all
1.23.37b & 1.25 147 cases + 0x11 = 164 (165 packets in client)
====
mov al, [edi]
add eax, 0xFFFFFFEF -> sub eax, 0x11
; switch 147 cases

Code: Select all
1.25.35 169 cases + 0x11 = 186 (186 packets in client)
====
mov al, [esi]
add eax, 0xFFFFFFEF -> sub eax, 0x11
; switch 169 cases

Code: Select all
5.0.8.3 213 cases + 0x0B = 224
=======
mov dl, [esi]
mov [esp+xxx], dl
mov ebx, [esp+xxx]
and ebx, 0xFF
lea eax, [ebx-0x0B]
; switch 213 cases

Code: Select all
7.0.0.4 233 cases + 0x0B = 244
=======
movzx edi, byte ptr [esi]
lea eax, [edi-0x0B]
; switch 233 cases

Depending on the client the assembler code looks different but overall it does the same thing.
    1. Extract the byte from the packet
    2. Modify the byte by removing the first X unknown packets
    3. Convert the byte to an address and jump (not shown above)

Now, the above script will fly-by the demo & pre-alpha jump tables, for the demo there are 4 tables in the EXE, 3 of them are server side, they are:
    Pre-Login Packets
    Post-Login, No God Rights
    Post-Login, God Rights
Because only 2 clients that require special handling exist, I've decided to hard-code the addresses:
Code: Select all
def IsDemoServerSidePreLoginPacketDistributor(ea):
  return ea == 0x47ED2E and GetOperandValue(ea, 1) == 0x91

def IsDemoServerSideNonGodPacketDistributor(ea):
  return ea == 0x49D1B3 and GetOperandValue(ea, 1) == 0xAF

def IsDemoServerSideGodPacketDistributor(ea):
  return ea == 0x49D5D7 and GetOperandValue(ea, 1) == 0x93

def IsDemoServerSidePostLoginPacketDistributor(ea):
  return IsDemoServerSideNonGodPacketDistributor(ea) or IsDemoServerSideGodPacketDistributor(ea)

def IsDemoServerSidePacketDistributor(ea):
  return IsDemoServerSidePreLoginPacketDistributor(ea) or IsDemoServerSidePostLoginPacketDistributor(ea)

def IsPreAlphaPacketDistributor(ea):
  return ea == 0x4085AF and GetOperandValue(ea, 1) == 0xA2

The actual code that will locate the possible Packet Distributors is this one:
Code: Select all
def LocatePossiblePacketDistributors():
  l = []
  # Scan all functions for an instructions that matches a predefined switch/case pattern
  for f in AllFunctions(): 
    for i in FuncItems(f):
      c = Comment(i)
      if c and "switch" in c and "cases" in c:
        #x = int(c[:-6][7:]) # Extract the number from the comment
        p = PrevHead(i, f)
        ok = False
        if IsDemoServerSidePacketDistributor(i) or IsPreAlphaPacketDistributor(i):
          ok = True
        elif GetMnem(p) == "mov":
          o = GetOpnd(p, 0)
          p = PrevHead(p, f)
          if GetMnem(p) == "sub" and (GetOperandValue(p, 1) == 0x0E or GetOperandValue(p, 1) == 0x11):
            ok = True
        elif GetMnem(p) == "add" and (GetOperandValue(p, 1) == 0xFFFFFFEF or GetOperandValue(p, 1) == 0xFFFFFFF2):
          ok = True
        elif GetMnem(p) == "lea" and (GetOperandValue(p, 1) == 0xFFFFFFEF or GetOperandValue(p, 1) == 0xFFFFFFF5):
          ok = True     
        if ok:
          l.append(i)
  return l

For the oldest clients this script will return 1 address, for most clients 2 addresses should be returned (both within the same function). For the Demo 4 addresses will be returned.
Therefor the locate the real client-side table I've created the following script:
Code: Select all
def LocateClientPacketDistributor(l=None): # l = list of possible addresses
  if l == None:
    l = LocatePossiblePacketDistributors()
  if not l:
    return "Error! Unable to locate the Packet Distributor"
  if len(l) >= 2:
    # If the Demo Server Side is in the list, than we can safely remove it
    m = map(IsDemoServerSidePacketDistributor, l)
    for i in range(m.count(True)):
      ea = l.pop(m.index(True))
  if len(l) == 2:
    if GetFunctionStart(l[0]) == GetFunctionStart(l[1]):
      # Assume the first one is not correct, so remove it
      l.pop(0)
  if len(l) != 1:
    return "Error! Unable to determine the Packet Distributor"
  return l[0]

Once you have the address of the packet distributor, it's only a few steps to locate the jump table, decode it, parse the complete jump table and then rename all functions in it. If you are in need of such a script ask gently :).
<Derrick> RunUO AI is kind of a functional prototype, which i have hacked into something resembling OSI behavior, but only by complitcating everything

Site Admin
Posts: 453
Joined: Tue Jun 17, 2008 2:33 pm
PostPosted: Mon Feb 21, 2011 5:13 pm
This is brilliant work.
I've tested these scripts with Demo, 5.0.8.3 and God client, works great. Thanks!

Posts: 8
Joined: Sat Jan 15, 2011 3:09 am
PostPosted: Sat Feb 26, 2011 6:36 pm
This is really useful. It is exactly what I've been needing. Thank you!

Posts: 1
Joined: Fri Feb 10, 2012 1:54 pm
PostPosted: Fri Feb 10, 2012 1:56 pm
Here is updated version of script above to current IDA/IDApython syntaxis
Code: Select all
def IsDemoServerSidePreLoginPacketDistributor(ea):
  return ea == 0x47ED2E and GetOperandValue(ea, 1) == 0x91

def IsDemoServerSideNonGodPacketDistributor(ea):
  return ea == 0x49D1B3 and GetOperandValue(ea, 1) == 0xAF

def IsDemoServerSideGodPacketDistributor(ea):
  return ea == 0x49D5D7 and GetOperandValue(ea, 1) == 0x93

def IsDemoServerSidePostLoginPacketDistributor(ea):
  return IsDemoServerSideNonGodPacketDistributor(ea) or IsDemoServerSideGodPacketDistributor(ea)

def IsDemoServerSidePacketDistributor(ea):
  return IsDemoServerSidePreLoginPacketDistributor(ea) or IsDemoServerSidePostLoginPacketDistributor(ea)

def IsPreAlphaPacketDistributor(ea):
  return ea == 0x4085AF and GetOperandValue(ea, 1) == 0xA2

def LocatePossiblePacketDistributors():
  l = []
  # Scan all functions for an instructions that matches a predefined switch/case pattern
  for f in Functions(): 
    #print "%x" % (f);
    for i in FuncItems(f):
      c = Comment(i)
      if c and "switch" in c and "cases" in c:
        #x = int(c[:-6][7:]) # Extract the number from the comment
        p = PrevHead(i, f)
        ok = False
        if IsDemoServerSidePacketDistributor(i) or IsPreAlphaPacketDistributor(i):
          ok = True
        elif GetMnem(p) == "mov":
          o = GetOpnd(p, 0)
          p = PrevHead(p, f)
          if GetMnem(p) == "sub" and (GetOperandValue(p, 1) == 0x0E or GetOperandValue(p, 1) == 0x11):
            ok = True
        elif GetMnem(p) == "add" and (GetOperandValue(p, 1) == 0xFFFFFFEF or GetOperandValue(p, 1) == 0xFFFFFFF2):
          ok = True
        elif GetMnem(p) == "lea" and (GetOperandValue(p, 1) == 0xFFFFFFEF or GetOperandValue(p, 1) == 0xFFFFFFF5):
          ok = True     
        if ok:
          l.append(i)
  return l

def LocateClientPacketDistributor(l=None): # l = list of possible addresses
  if l == None:
    l = LocatePossiblePacketDistributors()
  if not l:
    return "Error! Unable to locate the Packet Distributor"
  if len(l) >= 2:
    # If the Demo Server Side is in the list, than we can safely remove it
    m = map(IsDemoServerSidePacketDistributor, l)
    for i in range(m.count(True)):
      ea = l.pop(m.index(True))
  if len(l) == 2:
    if GetFunctionAttr(l[0],FUNCATTR_START) == GetFunctionAttr(l[1],FUNCATTR_START):
      # Assume the first one is not correct, so remove it
      l.pop(0)
  if len(l) != 1:
    return "Error! Unable to determine the Packet Distributor"
  return l[0]

print "Packet distr at: %x" % (LocateClientPacketDistributor());

Posts: 36
Joined: Thu Apr 05, 2012 2:46 am
PostPosted: Sat May 12, 2012 1:12 pm
Here's a trick I found for finding the beginning of the packet handler function.

Just search for:

CMP BYTE PTR DS:[EAX],33
In hex that's : 803833

clients I've seen so far have that instruction and only have it once: near the beginning of the packet handler function.

EDIT: damn, not true for all clients. It's there, but not necessarily next to the main packet handling code. In some clients it's off on it's own in a separate function that gets called before the main packet handler. It's a special handler for packet 0x33...

Also note that in client 7.0.25.0, that location of the 0x33 handler is called from 5 different locations in code and is for handling special packets -before- the main packet handler. Something to keep in mind. This style code is in clients from the 4.x to 7.x range.

Return to UO Client

Who is online

Users browsing this forum: No registered users and 0 guests

cron