📜 ⬆️ ⬇️

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

It was 2009, in the month of March, when I was fond of creating maps for the game WarCraft 3. Once I was shown a map that when I started the game in some tricky way I created a console and wrote some “Hello World!” In it. To put it mildly, I was dumbfounded - this meant that there is an opportunity to execute arbitrary code in the system.

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

One of my acquaintances, also from, as we called it, the “mod-maker” party, posted a translation of an article that explained how to use the external program to expand the possibilities of the game. Then someone whose name I wouldn’t name, wrote to him in a personal “This can be done without an external program” and sent him this card. I asked for any of his contacts, and I was able to contact him via ICQ. To my surprise, he gave exhaustive information on what exactly is the vulnerability 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

The scripts in WarCraft 3 are written on Blizzard-developed JAY (Just Another Script System) pre-tale. In general, it is quite flexible (the entire standard campaign is made on the visual superstructure of this language), but the trouble is: there are no structures, and the standard Sleep counts time even when the game is paused, and even with reduced accuracy, in a word, make a smooth movement of a unit, for example, moving it every .03 seconds is not possible. But there are timers that can be started, and which are sufficiently accurate, and at the end of the counting of which the function specified in the argument callback can be called. But they also do not solve problems: for example, an ability that would delay some actions will not work correctly if it is used twice: it is impossible to transfer any argument to a function that is called at the end of the timer countdown. ability this function should handle.

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

function RB takes unit u return integer return u //    ,     , //     return 0 //       endfunction 


But the error was not issued - apparently the type of the return value was recorded in a separate field, but with the next return statement, this field was overwritten again. As a result of executing this code, the function will return a certain number, which will be the in-game descriptor of the transferred function of the unit. 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 only numbers, strings and logical variables could be written to it, and two lines were used as an index. As a result, a method was invented that a unit descriptor was written to the cache at the index of the timer descriptor converted to a string, with which it was necessary to perform delayed actions, and then the timer descriptor could be obtained in the callback function and it was on the necessary unit to perform actions. It was a victory that significantly expanded the capabilities of the map creators.

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

JASS interpreter operation principle

On the basis of the map script, when loading, the game creates pseudocode, somewhat resembling an assembler. The game uses 8 byte opcodes, where the first 4 bytes indicate the operation code and optionally a virtual register (there are 256), and the second 4 bytes are the argument, for example, when the value is loaded into the register, the value itself will be there. Register IDs for each next operation code increase, for example, foo = 0; bar = 2; - to assign the value of foo, register # 00 will be used, and for bar # 01, after using the register # FF, # 00 will be used again. Similarly, the indices of global variables also increase, and go in order - the first declared variable will have the index n, the next n + 1.

There is also a big difference between a regular variable and an array: if a regular variable simply contains the value itself, the array contains a reference to a structure that contains data on the size of the array and a link to the memory area that directly contains the data of the array. The array, having initially small size when writing on large indices, makes realloc, but the size of the array is limited to 8192 elements.

Proper vulnerability

There is such a type in JASS as code, it is a relative pointer to a function, it is used to transfer callback functions, for example, by using an object to call such a function. With it, in theory, you can not do any mathematical operations, and it is given only by a literal, for example code c = function foo. And it does not indicate directly to the memory where the pseudocode is located, but only sets the indentation from the beginning of the entire processed pseudocode. And here comes the return bug:

 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 


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

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


And this is how you can call a function, or rather, not its own, but its offset. Further, by selecting by declaring variables in order and writing meaningless assignment operations, the game can be made to generate its own special pseudo-code. By the way, a similar method is used for anti-debugging, when a call instruction is written in an x86 assembler, and another instruction is specified as an address, and the transition is made to it. In JASS, this task is notably facilitated by the fact that the interpreter when it encounters an unfamiliar instruction ... just jumps to the next one! So, thanks to this code, we can assign the value of a regular 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 will get an array whose data will not be located in a specially designated place, but directly in the executable process code. It is necessary that the size field be larger, and realloc was not produced when trying to write something at high addresses, and the second field pointed to the address to which we are going to write. As a result, it is possible to write and read the process memory. A simple entry to this array:

 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 memory of the process at the right address. Then the matter of technology ... The author of the vulnerability found a stack, and wrote something to him that allowed him to take control. If I remember correctly in the archive of the card it was possible to shove my own * .dll, but the code could also be encrypted in the script.

Story

For some time we discussed how to use the find. Putting viruses in the cards seemed ignoble. I did not want to develop a library for expanding the functionality of the game for two reasons: the possibility that the vulnerability would be covered up and the possibility of abuse (again, someone would figure out how it works and start writing viruses). Not finding the use of the map was shown to some more people. And got to Blizzard. And then the circus began - they could not understand how the vulnerability works. At first, they by technical means forbade hosting in Battle.net maps, which had functions that return the type code. This solved the problem, but curtailed the functionality of the language.

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

Did anyone try to contact the game developers, not a bit later they released patch 1.24, in which the Return Bug was completely banned and their legal counterpart was added, which really didn’t allow reverse transformations - it was now impossible to get the object from among them. As a result, part of the cards that were written in JASS sensibly - quickly replaced the functions and began to work correctly, while the other part, which used the inverse transformation from number to object, broke down. Maps created on the visual add-in did not react at all to this event. In general, Blizzard chose not the best option, and besides, they broke compatibility.

Conclusion

What do I want to say at the end?

Using undocumented features you need to be careful and understand that the shop can cover.

Being closer to users, you can make your life much easier - the author of the vulnerability had previously had a negative experience with Blizzard tech support, who offered to reinstall the game to solve all problems. And he did not want to tell them about the vulnerability.

And the most important thing: always , always , always check the input data . There would be no vulnerability if the return bug was immediately replaced by a function built into the engine. It would not exist if the interpreter started a panic, stumbling upon an unknown opcode.

Source: https://habr.com/ru/post/148543/


All Articles