Decompiling AMX Plugins, Part 4

So far, we know two things: there is a registered cvar called pl_enabled, and if it’s set to 1 during client_authorized, a “display” timer is set for 10 seconds.

Luckily, the next function is the “display” function:

 0x178       PROC                        ; display
 0x188       PUSH.S                  0xC
 0x190       PUSH.C                  0x4
 0x198       CALL                func_00

We’ve come across our first local procedure call – a private function whose name was not stored, only identifiable by “func_00″ and its address. We know from the final PUSH.C that this function is only receiving one parameter. That one parameter is the first variable on the stack – the id passed to the display() function. Remember that if set_task does not have an array set, it will simply pass its task id to the timed function.

Let’s jump down to func_00 and see what it does:

 0x1E4       PROC                        ; func_00
 0x1F4       STACK                 -0x80
 0x1FC       ZERO.pri  
 0x200       ADDR.alt              -0x80
 0x208       FILL                   0x80
 0x210       STACK                 -0x30
 0x218       ZERO.pri  
 0x21C       ADDR.alt              -0xB0
 0x224       FILL                   0x30

We already know this function takes at least one parameter (since that’s how many was pushed to it). So, what’s up with these weird instructions?

The STACK instruction simply moves the stack pointer – negative (downwards) effectively allocates, and positive (upwards) deallocates. Each STACK instruction is meant to either reserve or unreserve local memory. -0×80 is allocating 0×80 bytes on the stack. That’s 128 in decimal, or divided by the cellsize (4), 32 cells. The next three instructions are important. ZERO.pri is obvious; the PRI register is zeroed. ADDR.alt is our second FRM relative instruction. It sets the ALT register to an address on the local stack frame. In this case, the stack pointer has been lowered by 0×80 bytes, so it makes sense that the starting address of the reserved block is FRM – 0×80. The FILL instruction then fills the memory at ALT with the number in PRI, for N bytes. So, in summary, a variable of 32 cells has been created on the stack, then overwritten with the number in PRI, which was zero. The concept of stack storage is made difficult to understand because it’s top-down, so I made a quick diagram:

The next four instructions are exactly the same, essentially. This time, only 12 cells are reserved (0×30, or 48 bytes). Since the stack pointer is now at FRM-0×80, it must be moved down to (FRM-0×80)-0×30, or FRM-0xB0. Thus, ADDR.alt sets the FILL address to -0xB0, then FILL zeroes the 12 cells. We can now write a prologue to this function:

func_00(id)
{
   new var1[32]
   new var2[12]
}

Note – we didn’t need an

={0,...}
after each variable. The compiler zeroes out variables automatically. For local functions, this is a large performance hit, but it makes scripting much easier! Onto the rest of the function:

 0x238       PUSH.C                 0x1F
 0x240       PUSHADDR              -0x80
 0x248       PUSH.S                  0xC
 0x250       PUSH.C                  0xC
 0x258       SYSREQ.C      get_user_name
 0x260       STACK                  0x10
 0x274       PUSH.C                  0x0
 0x27C       CONST.pri             0x110
 0x284       HEAP                    0x4
 0x28C       MOVS                    0x4
 0x294       PUSH.alt  
 0x298       PUSH.S                  0xC
 0x2A0       PUSH.C                  0xC
 0x2A8       SYSREQ.C      get_user_team
//get_user_name(index, name[], len)
//get_user_team ( index, team[]="", len=0)

The first native is pretty straighforward. PUSH.c is passing 0x1F or 31, the number of bytes the string can hold. PUSHADDR is another FRM relative instruction, and it’s pushing the address for FRM-0×80, which conveniently is var1[32]. PUSH.S, like we’ve seen before, is pushing a passed parameter; again this is the first one given to the function (at this point we’re assuming it’s the player id). The second native clearly uses the familiar trick for temporary storage. Now we have:

func_00(id)
{
   new var1[32]
   new var2[12]

   get_user_name(id, var1, 31)
   get_user_team(id)
}

Directly after, we get a surprise:

 0x2C0       EQ.C.pri                0x1
 0x2C8       JZER              jump_0001

EQ.C.pri checks if the value in PRI is equal to the opcod parameter (in this case, 0×1). If true, PRI is set to 1, otherwise, PRI is set to 0. This branch is occuring if PRI is zero. The result from SYSREQ.C (get_user_team) was stored in PRI, not in a temporary variable, so the return value of the function must be used directly in the branch. In summary, if PRI is not equal to 1, the branch will be taken. Therefore:

func_00(id)
{
   new var1[32]
   new var2[12]

   get_user_name(id, var1, 31)
   if (get_user_team(id) == 1)
   {
      //conditional code
   }
   //branched code (jump_0001)
}

Reading on for the conditional code, which is everything up to jump_0001:

 0x2DC       PUSH.C                0x114
 0x2E4       PUSH.C                  0xB
 0x2EC       PUSHADDR              -0xB0
 0x2F4       PUSH.C                  0xC
 0x2FC       SYSREQ.C               copy
 0x304       STACK                  0x10
 0x30C       JUMP              jump_0002

This is a simple call to copy() – we’re passing in DAT address 0×114, which AMXReader tells us is the string “T”. 0xB is 11 bytes, and FRM-0xB0 is the address of var2. Then, we’re jumping to jump_0002. Wait, jumping? Wouldn’t we simply move on to the branched code? In fact, that is exactly what’s happening. In an IF…ENDIF block one conditional is checked, and at most one branch is taken. In an IF/ELSE/ENDIF block, each conditional block of code must branch past the remaining conditional checks in order to move on. So, at this point, we can guess the function is this:

func_00(id)
{
   new var1[32]
   new var2[12]

   get_user_name(id, var1, 31)
   if (get_user_team(id) == 1)
   {
      copy(var2, 11, "T")
   } else[?] { //branched code (jump_0001)
      //[un?]conditional code
   }
   //branched code (jump_0002)
}

Skipping past that branch, we arrive at the location for jump_0001:

 0x354       SYSREQ.C      get_user_team
 0x36C       EQ.C.pri                0x2
 0x374       JZER              jump_0002

Here we see the same exact code as above for get_user_team, and another conditional branch. If the result is not 0×2, the branch will be taken. This is another conditional block, meaning an ‘else if’ rather than an ‘else’ statement. Furthermore, since the result of the function was recalculated again, we know the comparison occurred inside the statement. The code occuring continuing on until jump_0002 is trivial so I’ve included it.

func_00(id)
{
   new var1[32]
   new var2[12]

   get_user_name(id, var1, 31)
   if (get_user_team(id) == 1)
   {
      copy(var2, 11, "T")
      //branch to jump_0002 to skip next block
   } else if (get_user_team(id) == 2) { //branched code (jump_0001)
      copy(var2, 11, "CT")
   }
   //branched code (jump_0002)
}

Finally, all branches have been resolved, and we arrive at jump_0002:

 0x3C4       PUSHADDR              -0x80
 0x3CC       PUSHADDR              -0xB0
 0x3D4       PUSH.C                0x128
 0x3DC       PUSH.S                  0xC
 0x3E4       PUSH.C                 0x10
 0x3EC       SYSREQ.C         client_cmd

The only difficult part of this is remembering that PUSHADDR is local to FRM, and thus -0×80 is var1 and -0xB0 is var2. The string itself you can find in AMXReader’s DAT viewer. Note the function cleans up its stack by resetting the stack pointer – it adds (unreserving) 0xB0, which is the number of bytes it allocated. Lastly, note that the function uses RETN, which cleans up its own caller’s stack usage for parameters.

We’ll actually return to the display function now, which we see contains a trivially decodable client_print function. It did not use STACK after CALL because the local call cleaned up automatically. That is one of the reasons the number of parameters is pushed before each procedure call: RETN can pop that value, and use it to correct the stack before returning.

Finally, we have the entire source code to this plugin:

public plugin_init()
{
   register_plugin("Hello", "1.0", "BAILOPAN")
   register_cvar("pl_enabled", "1")
}

public client_authorized(id)
{
   if (!get_cvar_num("pl_enabled"))
      return
   set_task(10.0, "display", id)
   return
}

public display(id)
{
   func_00(id)
   //Note - we saw PUSH.C 3, the constant 3 is 'print_chat'
   client_print(id, print_chat, "[HELLO] Hello.")
}

func_00(id)
{
   new var1[32]
   new var2[12]

   get_user_name(id, var1, 31)
   if (get_user_team(id) == 1)
   {
      copy(var2, 11, "T")
   } else if (get_user_team(id) == 2) { //branched code (jump_0001)
      copy(var2, 11, "CT")
   }
   client_cmd(id, "[%s] %s", var2, var1)
}

Finally, we’ve disassembled a rather simple and trivial plugin. While it seems like a long process, eventually it becomes very easy to see patterns compilers use. Like anything, decompiling takes practice. For more practice, I’ll be disassembling a small portion of a real closed source plugin in the next article.

Bonus question: Why does the stack grow downwards? Isn’t it easier and less confusing for it to grow upwards?

3 Responses to “Decompiling AMX Plugins, Part 4”

  1. Freecode says:

    Decompiler reads from end-> start?

  2. Wraith says:

    The disassembler doesn’t read either the stack or the heap, they aren’t present unless the amx is inside a virtual machine which has allocated them, which the disassembler does not.

    A single region of memory is allocated for the entire plugin and a simple memcpy done to put the amx file in this segment, the heap then grows downwards from the end of the DAT area and the STK grow upwards from the end of the memory. This has two benefits, it means it is always easy to find the FRM and it prevents the need to detect overflow from the DAT into unreserve memory since if either the DAT or STK overflow they do so into eatch other and a simple check for the crossing of the current extents of both is enough to trigger a collision exception.

  3. Freecode says:

    ok so i was somewhat close

Leave a Reply

You must be logged in to post a comment.