Quake 3 wallhack tutorial by ^chaos^ ####### VERSION 0.9 @@2@@@ most of this tutorial is a plagiarized version of cppdude's quake 2 tutorial forgive me in advance cppdude. This fairly advanced tutorial is going to teach you how to make a wallhack for Quake3. To use it you will need TSearch and W32Dasm. We are going to be hacking the first version of Quake3 (3.11); and we are going to be using OpenGL apis. Don't worry if you dont know opengl, i'll explain what you need to know. You will also need a basic knowledge of asm. Read sheeps tuts if you dont(www.gamehacking.com). Knowing C is also helpful but not required. Ok lets go. Look in you Quake3 dir. Youll find a quake3.exe file. Unlike quake 3, all the opengl code is in the exe and not a stand alone dll file. This file we are gonna hack. So disasemble it with dasm. If you can, save it to a text file and open it with notepad, cos we are gonna be doing a lot of copy pastin. Our wallhack will have 2 modes. The first will be a wireframe mode, where we will force quake2 to draw all walls in wireframe. To do this we must first find the function that draws the world, and replace it with a call to our code. By world i mean not things like guns, monsters etc. Here is the pseudocode of our code section: DrawMode(Lines) Drawworld() DrawMode(Fill) return //to game code Pretty easy that. DrawMode will force all drawwing after that call to be done with lines, then we drawworld in lines, then we restore DrawMode to fill to make sure everything else is drawn properly; cos we dont want guns, players etc to be drawn in lines. Ok now you understand the theory of our first wallhack mode, lets put it into practice. We will first need to find out the Quake3 function to drawworld. In your asm view of Quake3 that you just made do a search for r_drawworld. r_drawworld is a Quake3 cvar that determines wether Quake3 will draw the world. Without this it would be very difficult to find the function that draws the world, and we would have to draw everything in wireframe mode (not nice). * Possible StringData Ref from Data Obj ->"r_drawworld" :449b19 push 4c339c ;pushes the text "r_drawworld" :449b1e mov dword ptr [007cb250], eax ;[7cb250] = 6993280 dec = 006AB580 hex. [006ab580] = 26618852 dec = 01962BE4 hex. [01962BE4] = "r_nocurves" ;which seems to be the CVAR before r_drawworld :449b23 call dword ptr [007be364] ;[007be364] = 4312496 dec = 0041CDB0 hex ??? ADDRESS :449b29 push 00000000 ;pushes 0 * Possible StringData Ref from Data Obj ->"0" :449b2b push 4b55bc ;pushes the text "0" :449b30 mov dword ptr [007cb340], eax ;[7cb340] = 6993320 dec = [006AB5A8] hex= 26618944 dec = [01962C40] hex = "r_drawworld" The address of the string ref is being pushed as a parameter to a function. What does this function do? It takes a string pointer as one of its parameters("r_drawworld" in this case) and adds a cvar to the console, so that you can use that cvar in the console for example by typing: r_drawworld 0 The function then returns a pointer to a variable. Functions return values by saving to the eax register, so the r_drawworld varibale is saved like this: mov dword ptr [007cb340], eax So now at location 007cb340, we have a pointer to the r_drawworld variable. We now need to do a search for that address and find out where that pointer is dereferenced. So do a search for 007cb340 and you should find this: * Referenced by a CALL at Addresses: |:0044BFAE , :0044C760 , :0044C90E | :00466D90 51 push ecx :00466D91 A140B37C00 mov eax, dword ptr [007CB340] <---- :00466D96 8B4820 mov ecx, dword ptr [eax+20] :00466D99 53 push ebx :00466D9A 33DB xor ebx, ebx :00466D9C 3BCB cmp ecx, ebx :00466D9E 0F8470020000 je 00467014 :00466DA4 F6058CEB7B0001 test byte ptr [007BEB8C], 01 :00466DAB 0F8563020000 jne 00467014 :00466DB1 8B0D44B37C00 mov ecx, dword ptr [007CB344] :00466DB7 55 push ebp :00466DB8 C705C0E87B00FE030000 mov dword ptr [007BE8C0], 000003FE :00466DC2 C705C4E87B0000E03F00 mov dword ptr [007BE8C4], 003FE000 :00466DCC 395920 cmp dword ptr [ecx+20], ebx :00466DCF 56 push esi :00466DD0 57 push edi :00466DD1 0F85F3010000 jne 00466FCA :00466DD7 8B35C0E37B00 mov esi, dword ptr [007BE3C0] :00466DDD 3BF3 cmp esi, ebx :00466DDF 7516 jne 00466DF7 ... Great. You've now found the function that draws the world. All three of them this time around. You can see that the pointer is being dereferenced (mov eax, dword ptr [007CB340]). W32Dasm tells you that the function is called at 0044BFAE,0044C760, and 0044C90E. Go there by doing a search for 0044BFAE,0044C760, and 0044C90E. Or Highlight this line "|:0044BFAE , :0044C760 , :0044C90E" in WinDasm, and now DOUBLE RIGHT CLICK on the addresses to jump to them. You'll come to a code section like this: :0044BFA4 E8A7F0FFFF call 0044B050 :0044BFA9 E832F4FFFF call 0044B3E0 :0044BFAE E8DDAD0100 call 00466D90 <-- //call to drawworld :0044BFB3 E8F8590100 call 004619B0 :0044BFB8 E833F2FFFF call 0044B1F0 :0044BFBD E84E050000 call 0044C510 ... :0044C760 E82BA60100 call 00466D90 <-- //call to drawworld :0044C765 E846520100 call 004619B0 :0044C76A E881EAFFFF call 0044B1F0 :0044C76F E99CFDFFFF jmp 0044C510 ... :0044C904 E847E7FFFF call 0044B050 :0044C909 E8D2EAFFFF call 0044B3E0 :0044C90E E87DA40100 call 00466D90 <-- //call to drawworld :0044C913 E898500100 call 004619B0 //i draw shadows at the feet of mine enemies :0044C918 E8D3E8FFFF call 0044B1F0 :0044C91D E8EEFBFFFF call 0044C510 We have now found out where the drawworld function is called. Hooray! We can now replace that call with a call to our code section. At the end of files there is usually a large area of unused memory that you can use for a code section. So go to the end of the asm of the file. Youll find a large area of 0x00s after 5 0x90s. We can start our code section at the 0x90s; at 1002644B. This would be smart, but I don't really care so I am going to throw it at 0x004000000 This is where we start using TSearch; to make asm scripts. Read the TSearch help file if you dont know how to make TSearch asm scripts. Load up TSearch and open up the easywrite interpreter. In a new script type: offset 400000 ; define the start of our code section We now need to make a call to the DrawMode function. The OpenGL api we will use for this is glPolygonMode. In C if you wanted to do make following polygons drawn with lines you would do: glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); So now we need to make a call to the function glPolygonMode, and we need to find its the address of the pointer to it. In W32Dasm do a search for glPolygonMode. Youll find a string ref address being pushed to a function like this: * Possible StringData Ref from Data Obj ->"glPolygonMode" | :0044E563 6828464C00 push 004C4628 ;text gets pushed "glPolygonMode" :0044E568 52 push edx :0044E569 A3F0A65C00 mov dword ptr [005CA6F0], eax :0044E56E A3ECE07B00 mov dword ptr [007BE0EC], eax :0044E573 FFD6 call esi :0044E575 A394A75C00 mov dword ptr [005CA794], eax :0044E57A A388DF7B00 mov dword ptr [007BDF88], eax ;[007bdf88] = 1766930516 dec = 69513854 hex <-- which is the ofset of glPolygonMode ;in Opengl32.dll :0044E57F A1ECD77B00 mov eax, dword ptr [007BD7EC] Here the address of the string ref "glPolygonMode" is being pushed as a function parameter and the called is then called(call esi). In C this function is GetProcAddress. It is used to get a pointer to a function imported from a module such as a dll file. GetProcAddress returns a pointer to the function(glPolygonMode in this case). So now we need to look for where the address of glPolygonMode is saved. Like all functions, the address is stored in the eax register so we need to look after call esi to where the eax register is saved to. Ive commented above where that is in case you cant figure it out. The address is saved in a couple places. Do a search for one of these addresses and youll find that the api is called like this: call dword ptr [007BDF88] You now know how to call glPolygonMode. Soon you can add it to your TSearch asm script. But first you need to push the parameters to the function. In asm we push parameters in reverse order becuase pushing puts the parameters onto a stack which is a first in last out data structure. So we first push GL_LINE. This is where the gl.h header file comes in. It will tell what GL_LINE is defined as. If you dont already have the gl.h header file, download it from www.opengl.org. Ill tell you what value to use anyway, its 0x1B01. So in your asm script in TSearch add a push for that value. Your script now looks like this: //define the start of our code section offset 400000 //push GL_LINE push 1B01 Now we need to push GL_FRONT_AND_BACK which is defined as 0x0408. Then you call glPolygonMode. So now your asm script looks like this: //define the start of our code section offset 400000 //push GL_LINE push 1B01 //push GL_FRONT_AND_BACK push 408 //call glPolygonMode call dword ptr [007BDF88] Now that all following polygons are drawn in line mode, we can call drawworld. We can then restore drawing to GL_FILL and then return to the game code. So your asm script now looks like this: //define the start of our code section offset 4000000 //push GL_LINE push 1B01 //push GL_FRONT_AND_BACK push 408 //call glPolygonMode call dword ptr [007BDF88] //call drawworld call 466d90 //return to game code ret Great. We now have our code section defined. We now need to make a call to it, by replacing the existing call to the drawworld function. We already found that the existing call to drawworld was at 0044BFAE,0044C760, and 0044C90E. Doing some research, by NOPing the 3 calls one at a time, i concluded that: 0044bfae - seems to control REFLECTIONS? like mirrors and such 0044c760 - don't know. I couldn't tell what this did. 0044c90e - draws the main world So we add a call to our code section in our asm script so that it now looks like this: //define the start of our code section offset 400000 //push GL_LINE push 1B01 //push GL_FRONT_AND_BACK push 408 //call glPolygonMode call dword ptr [007BDF88] //call drawworld call 466d90 //return to game code ret //make call to our code offset 0044C90E //by replacing existing call to drawworld function call 400000 Save this script and run it on quake3. Disaster!!! Quake2 now looks a mess. Every frame that Quake2 draws is being drawn on the previous frame. When the world was drawn in fill mode this wasnt a problem but now that we are drawing the world in lines we need to clear the screen everytime Quake2 draws a new frame. We could do this by using an opengl api, like this: glClear(GL_COLOR_BUFFER_BIT); but Quake3 already has a cvar to do this so i would rather hack that. The Quake3 cvar to do this is r_clear. Do a search for r_clear in W32Dasm and youll find, like r_drawworld, its the address of the string being pushed to the add cvar function, and a pointer returned. * Possible StringData Ref from Data Obj ->"r_clear" | :00449D78 6878324C00 push 004C3278 ;pushes the text "r_clear" :00449D7D A368A17C00 mov dword ptr [007CA168], eax :00449D82 FF1564E37B00 call dword ptr [007BE364] :00449D88 6800020000 push 00000200 * Possible StringData Ref from Data Obj ->"-1" | :00449D8D 68F4164C00 push 004C16F4 * Possible StringData Ref from Data Obj ->"r_offsetfactor" | :00449D92 6868324C00 push 004C3268 :00449D97 A398A17C00 mov dword ptr [007CA198], eax ;[007CA198] = 6994240 dec = [006AB940] = 26621068 dec = 0196348C hex = text "r_clear" :00449D9C FF1564E37B00 call dword ptr [007BE364] :00449DA2 6800020000 push 00000200 ... Search for where that pointer (007CA198) is being referenced like you did before and youll come to a code section like this: :0045A1C9 FF15A4E17B00 call dword ptr [007BE1A4] ;[007BE1A4] = 69513ADC = offset for glDrawBuffer in Opengl32.dll :0045A1CF 8B0D98A17C00 mov ecx, dword ptr [007CA198] ;[007CA198] = 6994240 dec = [6AB940] hex = 26621068 dec = 0196348C hex = text "r_clear" :0045A1D5 8B4120 mov eax, dword ptr [ecx+20] :0045A1D8 85C0 test eax, eax :0045A1DA 7422 je 0045A1FE :0045A1DC 680000803F push 3F800000 ;float value 1.0 (ALPHA) :0045A1E1 680000003F push 3F000000 ;float value 0.5 (B) :0045A1E6 6A00 push 00000000 ;float value 0.0 (G) :0045A1E8 680000803F push 3F800000 ;float value 1.0 (R) :0045A1ED FF1500E27B00 call dword ptr [007BE200] ;[007BE200] = 6951323C = offset for glClearColor :0045A1F3 6800410000 push 00004100 :0045A1F8 FF15CCE07B00 call dword ptr [007BE0CC] ;[007BE0CC] = 695131F4 = offset for glClear ... :0045A715 FF15A4E17B00 call dword ptr [007BE1A4] :0045A71B 8B1598A17C00 mov edx, dword ptr [007CA198] :0045A721 8B4220 mov eax, dword ptr [edx+20] :0045A724 85C0 test eax, eax :0045A726 7422 je 0045A74A :0045A728 680000803F push 3F800000 :0045A72D 680000003F push 3F000000 :0045A732 6A00 push 00000000 :0045A734 680000803F push 3F800000 :0045A739 FF1500E27B00 call dword ptr [007BE200] :0045A73F 6800410000 push 00004100 :0045A744 FF15CCE07B00 call dword ptr [007BE0CC] ... :0045A8A5 FF15A4E17B00 call dword ptr [007BE1A4] :0045A8AB 8B1598A17C00 mov edx, dword ptr [007CA198] :0045A8B1 396A20 cmp dword ptr [edx+20], ebp :0045A8B4 7421 je 0045A8D7 :0045A8B6 680000803F push 3F800000 :0045A8BB 680000003F push 3F000000 :0045A8C0 55 push ebp :0045A8C1 680000803F push 3F800000 :0045A8C6 FF1500E27B00 call dword ptr [007BE200] :0045A8CC 6800410000 push 00004100 :0045A8D1 FF15CCE07B00 call dword ptr [007BE0CC] You can see that 0x4100 is being pushed. If this is used as the paramater to glClear(the opengl api, not to be confused with gl_clear, the Quake2 cvar), then the screen buffer and the depth buffer(not important) will be cleared. But there is a jump above that push that will cause only 0x100 to be pushed to glClear. This will only clear the depth buffer, and that mess our wallhack was making will still be there. So all we need to do is(you guessed it) nop the jne. At all three spots. //define the start of our code section offset 400000 //push GL_LINE push 1B01 //push GL_FRONT_AND_BACK push 408 //call glPolygonMode call dword ptr [007BDF88] //call drawworld call 466d90 //return to game code ret //make call to our code offset 0044C90E //by replacing existing call to drawworld function call 400000 //nop 3 x jne to make sure GL_COLOR_BUFFER_BIT is also passed to glClear offset 0045A1DA //je 0045A1FE hex 9090 offset 0045A726 //je 0045A74A hex 9090 offset 0045A8B4 //je 0045A8D7 hex 9090 If you run this you will have a fully working wallhack for Quake3. If you want to toggle it off you use the bottom half of the easywrite interpreter. In this you can just replace the call to our code section with a call to the original drawworld function. The rest of the code section you can leave intact. Note: If you are implementing this wallhack method on other games you may have to nop any original calls to glPolygonMode. You are probably thinking: how can i remove that gay pink as the clear color? John Carmack seems to like this color because its exactly the same for Quake3. You simply call glClearColor like this: glClearColor(0.0f, 0.5f, 0.75f, 1.0f); If you noticed earlier at offset :0045A1ED :0045A739 :0045A8C6 these are all calls to glClearColor setting that awful pink color. It is called three times by the three different routines. We want to get rid of those calls - and later replace it with one call in our script. add this to the bottom of your script. offset 45a728 hex 9090909090 hex 9090909090 hex 9090 hex 9090909090 hex 909090909090 offset 45a1dc hex 9090909090 hex 9090909090 hex 9090 hex 9090909090 hex 909090909090 offset 45a8b6 hex 9090909090 hex 9090909090 hex 90 hex 9090909090 hex 909090909090 for a nice gray. The first 3 params are RGB, and the 4th is an alpha value which you dont need to know unless you decide to get into the wonderful world of OpenGL programming. Use the TSearch converter to get the hex values from the floating point values i just specified. Remember params are pushed in reverse order. Very important that. Stick a call to glClearColor in your code section before the drawworld call. I know its silly and a waste of processor power to call that api everytime but it works and youve gained lots of power by only drawing lines anyway. Your script so far: //define the start of our code section offset 400000 //push GL_LINE push 1B01 //push GL_FRONT_AND_BACK push 408 //call glPolygonMode call dword ptr [007BDF88] push 3f800000 push 3F000000 push 3F000000 push 3F000000 //call glClearColor values are set to light gray call dword ptr [007BE200] //call drawworld call 466d90 //return to game code ret //make call to our code offset 0044C90E //by replacing existing call to drawworld function call 400000 //nop 3 x jne to make sure GL_COLOR_BUFFER_BIT is also passed to glClear offset 0045A1DA //je 0045A1FE hex 9090 offset 0045A726 //je 0045A74A hex 9090 offset 0045A8B4 //je 0045A8D7 hex 9090 // gets rid of calls to glClearColor offset 45a728 hex 9090909090 hex 9090909090 hex 9090 hex 9090909090 hex 909090909090 offset 45a1dc hex 9090909090 hex 9090909090 hex 9090 hex 9090909090 hex 909090909090 offset 45a8b6 hex 9090909090 hex 9090909090 hex 90 hex 9090909090 hex 909090909090 That sums up my Quake3 wallhack tutorial. The methods here can be applied to any OpenGL game, but if you cant find the specific function that draws the world, then youll just have to make everything wireframe mode, which is bad cos you cant see your ammo health etc. special thanks to cppdude for writting his original tutorial... so that i could plagiarize it =) ^chaos^