Tutorial : Printing text to Starcraft's screen for Starcraft patch 1.13 by Yonderknight ================================================================= Introduction: I always categorized hacks into two categories: hacks that exploit game glitches and security flaws, and hacks that simply give you information. For the latter, finding a convenient way to display this information is quite tricky at times. In Starcraft, a simple alternative is to display the information as a chat message, but sometimes this isn't enough. One common hack that needs a good way to display information is the "stats hack". I'm sure all of you know what a stats hack for Starcraft is, it displays the minerals/gas/supply of each player on the screen. Since this is constantly being updated, it is impractical to display this information through chat messages. There isn't a pre-defined space for you to write the data in either, besides the leaderboard, which is a pain in the ass to get set up (so says Drakken and Jakor). So our last (decent) alternative is to find out how Starcraft displays any text on to the screen, and use that to display our data. Unlike my last tutorial, I'm not going to go into great detail about coding in mASM, but I am going to explain most of the debugger's output. ================================================================= In this tutorial, you will learn: • How Starcraft displays text onto the screen • How to call the function ourselves (for our own evil purposes) ================================================================= Tools Needed: • A debugger (I'm going to use OllyDbg in this tutorial) • Basic ASM knowledge • Memory Searcher ================================================================= Thinking about our hack: The first thing I did when I started this was think about how Starcraft would display regular text when I send a chat message. First I thought there would be a function similar to PrintTextToScreen(X,Y,[Duration]) But after looking for it for a while, I couldn't find it and gave up =p. Later, I thought about what this function would actually do. All this function would be doing is setting up the text to be written in a variable somewhere, and another function would read from this variable, and print it on the screen multiple times per second. So, I attempted to find this second function in hopes of being able ot draw anywhere on the screen. ================================================================= Stage 1: Finding WHERE Starcraft holds data to be printed Well, as I said above, we are looking for a function that reads the text that another function has set up, and prints it to the screen. So think about this: When you send a chat message, or use Starcraft’s ChatOut() function, all you are doing is writing the text into an offset somewhere. The function we are looking for MUST read from this offset to print on to the screen, or else it wouldn't know what text to print. So the first thing we are going to do is find where Starcraft stores text. This is fairly straightforward, and already described in my previous tutorial, but what the hell, lets do it again =p. Start a multiplayer game in Starcraft so you can send chat messages, and start up OllyDbg. Now type in any message you want, start up a memory searcher, and search for "YourName: Message". Of course, replace YourName and Message with...your name and the message you typed in =p. Write the address down somewhere. If you wait for the text to disappear off the screen, you will notice that a 00 byte is written on the first letter of your name. This basically tells the function that prints the text to stop printing it. This is bad, because we need to catch the function reading the data and actually printing it. ================================================================= Stage 2: Finding HOW Starcraft reads this data and prints it So now that we have one of the places where Starcraft stores its chat messages (there are like 12 in total or something, never really bothered to count), we have to see which function reads from it. This will be the function we're looking for :). So, fill up the chat buffer by sending repeated messages until they start scrolling off the screen. Make sure that none of them disappear, then ALT+TAB into ollydbg and quickly set an ACCESS breakpoint anywhere in your message (as long as its not the beginning of it). To set an access breakpoint: • Locate the "Hex Dump" in the lower left corner of ollyDbg • Go to the location of your message by hitting CTRL+G and typing in the address you wrote down earlier. • If you don't see your message, try repeating the previous step one more time • Once you see the message (in the ASCII Column), highlight any one letter of the message (except the first one, because another function might be reading from this one) • Right click on this letter and go to Breakpoint > Memory, On Access Now quickly ALT+TAB back into Starcraft. If you took too long, your text might have disappeared already. Wait a little bit to see if ollydbg pops. If it doesn't after 10 seconds or so, remove your breakpoint, re-fill the chat buffer, and try again. Ollydbg should land somewhere like this: |INC EDI |MOV DWORD PTR DS:[6BEA24],ESI |MOV CL,BYTE PTR DS:[EDI] ;Olly lands here |TEST CL,CL \JNZ SHORT Starcraf.004DAACF MOVSX ECX,WORD PTR DS:[6BEA64] Well, I really don't care what this is doing, but if you insist on knowing, its basically a loop that moves every letter of your message to somewhere else =p. At least it looks like it =\. A good thing to do right now is to remove your memory breakpoint, so we won't get bugged by other functions reading from the same place. Now we are going to go one function level up To go one function level up: • Look at all those blue buttons at the top of OllYDbg • Find the one that says "Execute Till Return" • Hit the button that says "Step Into", or press F8. We are now staring into the function we were just in. As you can see, we're still in a loop (so says ollydbg), and this doesn't look like its a higher-level function. So go one more function level up by using execute till return and step into. Ok, now we're not in a loop, but think about this. The function we want should have at least 3 parameters, X coordinate, Y coordinate, and a pointer to our text. Now Jakor being the smart guy that he is once told me that Starcraft uses something called "fast-call"...or something like that. This basically means that when Starcraft calls a function, only ECX and EDX are actually parameters. The rest of the parameters are PUSHed into the stack. So if we have 3 parameters (X, Y, and Text), we have to have at least 1 PUSH before the call, which I don't see. So that made me think I'm still too low. This, and for the reason that when I tried to NOP the call out, I got crashed =p. So again, one more level up. You should be looking at this: |PUSH EBP |AND EDX,0FFFF |MOV BYTE PTR DS:[6BEA31],11 |MOV WORD PTR DS:[6BEA60],CX |MOV WORD PTR DS:[6BEA64],276 |MOV WORD PTR DS:[6BEA62],DI |CALL Starcraf.004DAB60 ; this is the function we were in |MOV ECX,DWORD PTR DS:[659176] |LEA EAX,DWORD PTR DS:[ESI+1] Will you look at that, there is a PUSH in there, just like good ol' Yondy said =). Now lets check EDX and ECX before this function gets called to see if they are X and Y coordinates, and check the push to see what we are PUSHing. Press the blue "Play" button at the top of Ollydbg. Next, select the line of code with the "PUSH EBP" on it. Then press F2. This toggles a breakpoint, so whenever this line of code is ran, OllyDbg will stop it and we will be able to check the registers and run through the code line by line. After you set your breakpoint, switch back into SC. Ollydbg should pop immediately. If it doesn't, then try chatting some text. Now look in the upper-right corner at the Registers. Check out EBP. Its a pointer to our message! Now press step into. This runs one line of code. Press Step into again to run the "AND EDX,..." EDX is now something like C0, and ECX is something like 0A. This looks like the function we are looking for! Remove your code breakpoint and press Play. We are now going to try to NOP it out (stop it from being executed). If we NOP it out, and no text is written to the screen, we know that this is the function responsible for writing the text. To NOP out the function: • Right click on PUSH EBP • Select Binary > Fill with NOPS. • Right click on the CALL, and do the same. Go back into the game and check. Whoopee, no text! Also, we can check if this is the function being used by ALL text in the game. To do this, we will try to NOP out the whole entire function; not only this one call to it. If it is called multiple times by different places, we need to either NOP out the whole function, or NOP out all the calls to it. To NOP out the whole function: • First un-NOP the call by right-clicking and selecting "Undo Selection" • Press ENTER on the call to jump inside it • Click on the line of code that was selected and drag the selection all the way down until you see a RETN 4 and a couple NOPs, but don't select the RETN 4 • NOP all of this code out Switch back into the game to see if there is any text. You will notice that there is no text ANYWHERE in the game :). Again, "undo selection" so we can continue with the rest of the tutorial. ================================================================= Stage 3: Figuring out how to call this function The thing about writing to the screen is, if you write at the wrong time, something else might draw over your text, and the text will flicker. This is the same with this function, if we just make it draw every millisecond or so, it will still flicker. So, what better time to draw our text, than right after Starcraft draws its text? Well we already know where Starcraft makes its call to draw regular chat text. So we'll stick with making our call after this one. The only problem is that its only written when there is actually chat text on the screen. Look at the code again. 0046E746 MOV BYTE PTR DS:[6BEA31],11 0046E74D MOV WORD PTR DS:[6BEA60],CX 0046E754 >MOV WORD PTR DS:[6BEA64],276 0046E75D |MOV WORD PTR DS:[6BEA62],DI 0046E764 |CALL Starcraf.004DAB60 0046E769 |MOV ECX,DWORD PTR DS:[659176] 0046E76F |LEA EAX,DWORD PTR DS:[ESI+1] 0046E772 |CDQ 0046E773 |ADD EDI,ECX 0046E775 |MOV ECX,0B 0046E77A |IDIV ECX 0046E77C |DEC DWORD PTR SS:[ESP+C] 0046E780 |MOV ESI,EDX 0046E782 \JNZ SHORT Starcraf.0046E708 0046E784 MOV AL,BYTE PTR DS:[659064] 0046E789 TEST AL,AL Well, if you try setting a breakpoint (F2) on the actual call, you will see that it only if called when there is chat text on the screen (makes sense). But we want our text to appear whenever we want it to. Try setting a breakpoint farther down. I set it at MOV AL,BYTE PTR DS:[659064]. This is being called constantly, whether or not there is chat text on the screen, so we can use this as our patch. Write the address of this line of code down (0046E784). So lets sum up: Call 004DAB60 ECX, EDX - X and Y coordinates Pointer to Text - PUSHed Patch at 0046E784 ================================================================= Stage 4: Problems :( Well, I won't go through coding this, but if you need any help look at my Unit Alert tutorial. I discuss how to make patches, and call SC's function's in that tutorial. So one thing you will notice with this is that the text doesn't appear on the screen right away, only until the screen refreshes =(. Another thing you will notice (maybe not, but I noticed it) is that if the chat buffer's full, your text will disappear! Took me a while to figure this one out myself. I tried setting my patch at several locations, and noticed that every time it takes the formatting of the place I set my patch to. Well, SC is always calling the same function...and the parameters pretty much look the same. The only other thing that could be happening is that SC writes some extra formatting to some static locations. Well, look around the call that we found. 0046E746 MOV BYTE PTR DS:[6BEA31],11 0046E74D MOV WORD PTR DS:[6BEA60],CX 0046E754 >MOV WORD PTR DS:[6BEA64],276 0046E75D |MOV WORD PTR DS:[6BEA62],DI 0046E764 |CALL Starcraf.004DAB60 0046E769 |MOV ECX,DWORD PTR DS:[659176] 0046E76F |LEA EAX,DWORD PTR DS:[ESI+1] See all those MOV's before the call? That's right, STATIC offsets. Try playing with the numbers to see what they do. They are some kind of formatting string. I won't go into great depth with this, but you need to mess around with these to see which one will make your text stop disappearing. You can also use this for centering text and things like that. So before your call, you need to BACKUP the previously formatted strings, and write your own, and then replace them with the backups after you're done. Otherwise, some text will get messed up. There are many static locations like these that hold things like text color and many other things (you can have ANY colored text you want =D). Also, you can make text any size you want, but I won't go into great detail with these. ================================================================= Stage 5: Refreshing the screen This only leaves us with one problem, refreshing our text location so it will be updated immediately. Now i know some people that would be happy with just this *cough* agent *cough*, but I like things nice and neat, and I don't rest until my stuff is perfect :). This is the difference between a good hack, and a crappy one. Now, I spent a lot of time thinking about how to find the refresh function. Well, Starcraft has to refresh the screen when chat text is first written to the screen, so I searched around here for a long time. After having Starcraft crash on me several times because of NOPing random calls, I finally gave up on searching. I gave this project a couple day's break, but still thought about it often. I finally remembered "Starcraft has to refresh when its text DISAPPEARS, too!" So I decided to search around there. To find how Starcraft gets rid of text: • send out some chat text • Search for it • before it disappears, set a breakpoint on the FIRST character of your message. Ollydbg should land here: 0046E62F JS Starcraf.0046E6CE 0046E635 CMP ESI,0C 0046E638 >MOV BYTE PTR DS:[EAX*2+65862C],0 ; lands here 0046E640 JNZ SHORT Starcraf.0046E661 0046E642 MOV EDX,DWORD PTR DS:[659178] 0046E648 AND EDX,0FFFF That basically puts a 0 in the first character to notify that the text shouldn't be written anymore. Now scroll around and look for some calls... This one caught my eye (*ouch*): 0046E6BB PUSH ECX 0046E6BC PUSH 276 0046E6C1 LEA EDX,DWORD PTR DS:[EAX+70] 0046E6C4 MOV ECX,0A 0046E6C9 CALL Starcraf.004D8590 Well, I've messed with the X and Y coordinates so much I noticed the 276 and 0A in there. 0A is the X coordinate that chat text is written at, and 276 is the X coordinate of the right edge of the screen! Try NOPping this function out (remember to NOP out the pushes too) After typing in some text, you will notice that it doesn't refresh when its supposed to disappear! This is our function! Now when refreshing the screen, you have to refresh a RECTANGLE. So this means you need 2 sets of X and Y coordinates. Look back at our function. We have ECX and EDX, and 2 PUSHed parameters. Take a wild guess what these could be ;). 004D8590 - refresh screen ECX: X1 EDX: Y1 PUSH Y2 PUSH X2 This should be all you need :). ================================================================= Conclusion: Well, that's it. I hope you guys have fun with this. Try experimenting on your own too, look for format strings, try to get your text in different colors, sizes, fonts. There's a lot of stuff you can do with this, and be a little respectful and give me a little props if you ever decide to use it in one of your hacks :). ================================================================= Shouts!: Thanks to Jakor for teaching me so much about hacking and for always helping me out with stuff =). Also thanks to TheTempest for always wanting to help. Besides that, thanks to everyone and anyone who's ever helped me Nickolay, Fish Beans, Bulk_4me, NAATYE, Dt, Palomino, and everyone else I forgot. Also thanks to Palomino for figuring out how to write text in different sizes and fonts.. Thanks to Microsoft Word for correcting me every time I spelled "disappear" wrong. =================================================================