Back to Home

History and description of the already fixed vulnerability in the game WarCraft 3

Warcraft · vulnerability · hacking · return bug

History and description of the already fixed vulnerability in the game WarCraft 3

It was 2009, in the month of March, then I was fond of creating maps for the game WarCraft 3. Once they showed me a map that, when the game started, created a console in some cunning way and wrote in it some kind of “Hello World!”. To put it mildly, I was dumbfounded - this meant that it was possible to execute arbitrary code in the system.

Under the cut is a description of the vulnerability and the history of its closure.

One of my acquaintances, also from what we called the "modmaker" hangout, posted a translation of an article that described how to use the external program to expand the capabilities of the game. Then someone, whose nickname I will not call, wrote to him in a personal “It can be done without an external program” and sent him this card. I asked for any of his contacts, and I managed to contact him via ICQ. To my surprise, he gave comprehensive information on exactly what the vulnerability is and how to use it. But, for the full picture, I will have to start by explaining some of the nuances of writing scripts for the game.

Return bug

Scripts in WarCraft 3 are written on the Blizzard-developed JASS (Just Another Script System). In general, it is flexible enough (the entire standard campaign is made on a visual add-on for this language), but the trouble is: there are no structures, and the standard Sleep counts the time even when the game is paused, and even with reduced accuracy, in a word, make the unit move smoothly, for example, moving it every .03 seconds is impossible. But there are timers that can be started, and which are fairly accurate, and at the end of the count of which the function specified in the callback argument can be called. But they don’t solve the problems either: for example, an ability that would delay some actions will not work correctly if used twice: it is impossible to pass any arguments to the function that is called at the end of the timer,

And here Return Bug was discovered (it was a long time ago, I think in 2005, and maybe earlier. I did not find this moment). JASS is a strongly typed language, but this bug allowed it to be circumvented. It looked like this:

function RB takes unit u return integer
  return u  // тут бы выдать ошибку, что функция должна вернуть число,
            // а она возвращает юнита
  return 0  // игра проверяла только последнюю инструкцию возврата
endfunction


But the error was not issued - apparently the return type was written in a separate field, but the next return instruction rewrites this field again. As a result of the execution of this code, the function will return a certain number, which will be the in-game descriptor of the passed unit function. And for each unit this number will be unique.

The language also had such a tool as a cache - it was intended to save data in a campaign and transfer data from one mission to another, and normally it was possible to write only numbers, lines and logical variables into it, and two lines acted as an index. As a result, a method was invented that in the cache by the index of the timer descriptor converted to a string was written to the unit descriptor with which it was necessary to perform deferred actions, and then in the callback function it was possible to get a timer descriptor and perform actions on the desired unit. It was a victory, significantly expanding the capabilities of map makers.

It is also strange that the developers of the game did not close this loophole, and its use became widespread. Actually, any interesting non-standard abilities of units were made like this.

How the JASS interpreter works

Based on the map script, upon loading, the game creates pseudo-code, somewhat reminiscent of assembler. The game uses 8 byte opcodes, where the first 4 bytes indicate the operation code and optionally virtual register (there are 256 of them), and the second 4 bytes indicate the argument, for example, when the value is loaded into the register, the value itself will be there directly. The register IDs for each subsequent operation in the code are incremented, for example, foo = 0; bar = 2; - register # 00 will be used to assign the value foo, and for bar # 01, after using the #FF register, # 00 will be used again. In the same way, the indices of global variables also increase, and go in order - the first declared variable will have index n, the next n + 1.

There is also a big difference between an ordinary variable and an array: if an ordinary variable just contains the value directly, then the array contains a link to the structure that contains data about the size of the array and also a link to the memory area that contains the array data directly. The array, having initially small size when recording at large indices, makes realloc, however, the size of the array is limited to 8192 elements.

Vulnerability itself

There is such a type as code in JASS, this is a relative pointer to a function, it is used to pass a callback to a function, for example, on using an object to call such and such a function. In principle, no mathematical operations can be done with it, and it is set only by a literal, for example code c = function foo. And it does not directly point to the memory where the pseudocode is located, but only sets the indent from the beginning of the entire processing pseudocode. And here comes the return bug to the rescue:

function StubFunc takes nothing returns nothing
endfunction
function I2C takes integer ic returns code
  return ic
  return (function StubFunc) // Просто пустая функция, нужна для проверки синтаксиса.
endfunction
function C2I takes code c returns integer
  return c
  return 0
endfunction


Such a code will return the relative address of the first opcode of the transferred function as a number.

function HackArrayW takes nothing returns nothing
  local code ctcode = I2C(C2I(function zOmgFunc2) + 1)
  // ...


But this is how you can call a function, or rather, not itself, but it with an offset. Further, by selecting by declaring variables in order and writing meaningless assignment operations, you can make the game generate its own, special pseudo-code. A similar method is used by the way for anti-debugging, when a call instruction is written in x86 assembler, and another instruction is specified as the address, and the transition is made to it. In JASS, this task is noticeably facilitated by the fact that the interpreter, when meeting an unfamiliar instruction ... just jumps to the next! So, thanks to this code, we can assign the value of an ordinary variable to an array variable and vice versa .

It is known that the game uses one library that is not updated with patches - Storm.dll. In its address space, you can find data whose address is assigned to an array variable, and it turns out that we get an array whose data will not be in a specially allocated place, but directly in the executable code of the process. It is necessary that the size field be larger and realloc is not produced when trying to write something to high addresses, and the second field indicates the address to which we are going to write. As a result, it becomes possible to write and read process memory. A simple write to an array like this:

function Inj_PrepareInjector takes nothing returns nothing
  set zg0oI[0x200]=0xE8575653
  set zg0oI[0x201]=0x000000F3
  set zg0oI[0x202]=0x9F2DC88B
  set zg0oI[0x203]=0xFF000006
  set zg0oI[0x204]=0x72656BE0
  // ...


Will write to the process memory at the desired address. Further, the technical matter ... The author of the vulnerability found the stack, and wrote something in it, which made it possible to take control. If I remember correctly, I could also put my * .dll into the map’s archive, but the code could also be encrypted in the script.

History

For some time we discussed how to use the find. Placing viruses on cards seemed ignoble. I did not want to develop a library to expand the functionality of the game for two reasons: the possibility that the vulnerability would be covered and the possibility of abuse (again, someone will figure out how it works and start to write viruses). Finding no use, the map was shown to some more people. And got to Blizzard. And then the circus began - they still could not understand how the vulnerability works. First, they banned the technical means of hosting maps in Battle.net that contained functions that returned the code type. This solved the problem, but curtailed the functionality of the language.

I undertook to write my patch, as a result, a small program appeared that copied one of the libraries in the game and overwritten it ... 5 bytes. Yes, it was a complete salvation. And I did (the address was suggested to me by the vulnerability developer) to intercept processing of type code. The game constantly uses the conversion of the relative gaming address of the opcode (the one that can be obtained using Return Bug) into a real address, and I just simply added the operation and 0xfffffff8, simply preventing the incorrect pseudo-code from being executed. I posted it on the forums dedicated to creating maps for WarCraft 3. And I went to the cottage, where I had no Internet. I understand that I acted irresponsibly;)

Did anyone try to contact the developers of the game, a little later they released patch 1.24, which completely prohibited Return Bug and added its legal counterpart, which really didn’t allow the inverse transformations - now it’s become impossible to get an object from a number. As a result, part of the cards that were written in JASS intelligently - quickly replaced functions and began to work correctly, while the other part, which used the inverse transforms from a number to an object, broke. Maps created on a visual add-in did not react at all to this event. In general, Blizzard did not choose the best option, and also broke compatibility.

Conclusion

What do I want to say in the end?

Using undocumented features, you need to be careful and understand that the bench can be covered.

Being closer to users, you can significantly simplify your life - the author of the vulnerability previously had a negative experience communicating with Blizzard tech support, which he suggested reinstalling the game to solve all problems. And he did not want to inform them of the vulnerability.

And most importantly: always , always , always check the input . There would be no vulnerability if the return bug were immediately replaced with a function built into the engine. It would not have been if the interpreter had begun to panic when stumbled upon an unknown opcode.

Added:

habr.com/en/post/148543/#comment_20641885

Read Next