📜 ⬆️ ⬇️

Solve a simple crackme for Sega Mega Drive

Hello to all,



Despite my great experience in the reverse of games under the Sega Mega Drive , I never solved cracks for it, and they did not come across to me on the Internet. But the other day there was a funny crack, which I wanted to solve. I share with you the decision ...


Description


The task description and the rum itself can be downloaded here .


Despite the fact that the list of resources it says about Hydra, the standard de facto among the tools for debugging and reverse games on the Segou is Smd Ida Tools . It has everything you need to solve this crack:



We throw in the plug-ins for Ida the latest release, and start looking at what we have.


Decision


The launch of any game on Segou begins with the execution of the vector Reset . A pointer to it can be found in the second DWORD from the beginning of the rum.




We see a couple of unidentified functions starting at address 0x27A . Let's take a look at what's there.


sub_2EA ()



From my own experience I will say that this is how the function of waiting for the completion of a VBLANK interrupt usually looks. Let's see where else there is an access to the variable byte_FF0026 :



We see that the zero bit is set in the VBLANK interrupt. So the variable is called vblank_ready , and the function where it is checked is wait_for_vblank .


sub_60E ()


Further, the function sub_60E is called by code. Let's see what is there:



What the first command writes to the VDP_CTRL is the VDP control command. To find out what she does, we get on this command and press the J key:



We see that the entry in CRAM initialized (the place where the palettes are stored). So, all the subsequent function code simply sets some initial palette. Accordingly, the function can be called init_cram .


sub_71A ()



We see that some command is VDP_CTRL again to VDP_CTRL , then we press J again and we learn that this command initializes the recording in the video memory:



Next, understand what is transferred to the video memory, it makes no sense. So just call the load_vdp_data function.


sub_C60 ()


Here, almost the same thing happens as in the previous function, so without going into details, we simply call the function load_vdp_data2 .


sub_8DA ()


There is already more code. And, besides, in this function one more is caused. Look right there - in sub_D08 .


sub_D08 ()



We see that in the D0 register the command for VDP_CTRL , in D1 - the value with which VRAM will be filled, and in D2 and D3 - the width and height of the fill (since two cycles are obtained: internal and external). Call the function fill_vram_by_addr .


sub_8DA ()


We return to the previous function. Once the value in the D0 register is transmitted as a command for VDP_CTRL , we press the J key on the value. We get:



Again, from the experience of reverse games on the Segou, I can say that this command initializes the record of the mapping of tiles. Addresses that begin with $Fxxx , $Exxx , $Dxxx , $Cxxx in 90% of cases will be addresses of the regions with these same mappings. What are mappings:
these are values ​​that can be used to indicate where to display a particular tile on the screen (a tile is a square of 8x8 pixels).


So the function can be called as init_tile_mappings .


sub_CDC ()



The first command initializes the entry at $F000 . One note: among the mapping addresses, there is another region where the sprite table is stored (these are their positions, the tiles to which they point, etc.) Find out which region is responsible for what can be debugged. But so far we don’t need it, so we init_other_mappings call the function just init_other_mappings .


Also, we see that two variables are initialized in this function: word_FF000A and word_FF000C . From my experience (yes, he decides) I will say that if some two variables are near in the address space and are associated with mappings, then in most cases these will be the coordinates of some object (for example, a sprite). Therefore, I suggest calling them as sprite_pos_x and sprite_pos_y . Error in x and y valid, because Further under debugging it will be easy to fix.


VBLANK


Since the loop goes on through the code, it can be assumed that we have completed the main initialization. Now you can look at VBLANK interruption.



We see that two variables are incremented (which is strange, in the list of links to each of them is absolutely empty). But, since they are updated once per frame, you can call them timer1 and timer2 .


Next, the sub_2FE function is sub_2FE . Let's see what is there:


sub_2FE ()



And there - work with IO_CT1_DATA port (responsible for the first joystick). The port address is loaded into the A0 register, and passed to the sub_310 function. Go there:


sub_310 ()



My experience helps me again. If you see the code that works with the joystick and two variables in memory, then one stores the pressed keys , and the second holds the held keys , i.e. just pressed and held keys. So let's call these variables: pressed_keys and held_keys . And then the function can be called as update_joypad_state .


sub_2FE ()


Call the function as read_joypad .


Handler loop


Now everything looks much clearer:



So this cycle responds to keystrokes, and performs the corresponding actions. Let's go through each of the functions called in the loop.


sub_4D4 ()



There is a lot of code here. Let's start with the first function to be called: sub_60C .


sub_60C ()


She does nothing - so it may seem at first. Just return from the current function - rts . But, since on it only jumps occur ( bsr ), which means rts will take us back to the handler loop. I would call this function as retn_to_loop .


sub_4D4 ()


Further we see the call to the word_FF000E variable. It is not used anywhere except for the current function and, at first, I did not understand its purpose. But, if you look closely, you can assume that this variable is needed only for a small delay between the processing of keystrokes. ( It is already poorly implemented in this rum, but I think it would have been much worse without this variable ).



Next we have a large amount of code that somehow processes the sprite_pos_x and sprite_pos_y , which can only say one thing - this is needed to display the selection sprite around the selected character in the alphabet.


So now you can safely call the function as update_selection . Go ahead.



The code checks whether the bits of any keystrokes are set and calls certain functions. Let's look at them.


sub_D28 ()



Some kind of shaman magic. First, WORD is taken from the word_FF0018 variable, then one interesting instruction is executed:


 bsr.w *+4 

This command simply jumps to the instruction following it.


Next is another magic:


 move.l d0,(sp) rts 

The value in register D0 is put on top of the stack. It is worth noting that in Segi, like in any x86 , the return address from the function when it is called is placed on the top of the stack. Accordingly, the first instruction puts some address on the top, and the second one picks it up from the stack and makes a transition on it. Good trick .


Now you need to understand what is the value in the variable, which then goes the transition. But, for starters, let's call this variable jmp_addr .


And the function is called:



jmp_addr


Find out where this variable fills. See the list of references:



There is only one entry in this variable. Let's look at it.


sub_3A4 ()



Here, depending on the coordinates of the sprite (remember that this is most likely the address of the selected character), this or that value is entered. We see the following piece of code:



The existing value is shifted to the right by 4 bits, the new value is placed in the lower byte, and the result is entered into the variable again. In theory, our jmp_addr variable stores the characters that we can enter on the key entry screen. Note also that the size of the variable is WORD .


In fact, the function sub_3A4 can be called as update_jmp_addr .


sub_414 ()


Now we have only one function left in the loop, which is not recognized. And it is called sub_414 .



Its code resembles the code of the update_jmp_addr function, only at the end we call the sub_45E function. Look in there.


sub_45E ()



We see that the number #$4B1E2003 entered in the D0 register, which is then sent to VDP_CTRL , which means that we are dealing with another VDP control command. We press J , we receive a command of record to the region with $Cxxx .


Next, the code is used to work with the variable byte_FF0014 , which is not used anywhere except for the current function. If you look at how it is used, you will notice that the maximum number that can be set in it is 4 . I have the assumption that this is the current length of the key entered. Let's check it out.


Run the debugger


I will use the Smd Ida Tools debugger, but, in fact, some Gens KMod or Gens ReRecording will be enough. The main thing is to have a feature with the display of addresses in memory.



My theory is confirmed. So the variable byte_FF0014 can now be key_length .


There is one more variable: dword_FF0010 , which is also used only in the current function, and its contents, after adding with the initial command in D0 (remember, this was the number #$4B1E2003 ) is sent to VDP_CTRL . add_to_vdp_cmd think, I called the variable add_to_vdp_cmd .


So what does this feature do? I have an assumption that it draws the entered character. It’s easy to check this by running the debugger, and comparing the state before calling the sub_45E function and after:


Before:



After:



I was right - this function draws the entered character. do_draw_input_char call it do_draw_input_char , and the function that calls it ( sub_414 ) is draw_input_char .


Now what?


Let's check for now that the variable we called jmp_addr actually stores the key entered. Let's use the same Memory Watch :



As you can see, the guess was true. What does this give us? We can jump to any address. Just what? In the list of functions, all are dismantled because:



Then I started to simply scroll through the code until I discovered this:



The trained eye saw the sequence $4E, $75 at the end of the unbundled bytes. This is the rts instruction opcode, i.e. return from function. So these unpartitioned bytes can be the code of some function. Let's try to designate them as a code, click C :



Obviously, this is the function code. You can also click on it P , so that the code becomes a function. Remember this name: sub_D3C .


Then a thought arises: what if you jump on sub_D3C ? It sounds good, though a single jump here will obviously not be enough, because word_FF0020 no more references to the word_FF0020 variable.


Then another thought came to me: what if I looked for another such unallocated code? Open the Binary search (Alt + B) dialog, enter the 4E 75 sequence in it, put the Find all occurrences :



Click to start the search, we get the following results.



At least two more places in the Roma may contain the function code, you need to check them. We click on the first of the options, scroll slightly up, and again we see a sequence of undefined bytes. Denote them as a function? Yes! Hit P where the bytes begin:



Cool! Now we have a sub_34C function. We try to repeat the same thing with the last of the options found, and ... we get a bummer. There is such a large number of bytes before 4E 75 , which is not clear where the function begins. And, obviously, not all of these bytes above are code, since a lot of duplicate bytes.


Determine the beginning of the function


It will be easiest for us to find the beginning of a function if we find where the data ends. How to do it? In fact, absolutely not difficult:


  1. We turn to the beginning of the data (there will be a link to them from the code)
  2. We follow the link and look for a cycle in which the size of this data will have to appear.
  3. Mark up the array

So, we carry out the first item ...:



... and immediately see that the loop from our array is copied 4 data bytes at a time (because move.l ) into VDP_DATA . Next we see the number 2047 . It may first appear that the total size of the array is 2047 * 4 , but the loop based on dbf is performed by +1 iteration more, since The last compared value is not 0 , but -1 .


Total: the size of the array is equal to 2048 * 4 = 8192 . Denote bytes as an array. To do this, click * and specify the size:



We turn to the end of the array, and we see there bytes, which are code bytes:




Now we have a sub_2D86 function, and we have everything to solve this crack! Let's see what the new function does.


sub_2D86 ()


And it only enters the value of #$4147 in the D1 register and calls the sub_34C function. Take a look at her.


sub_34C ()



We see that the value of the word_FF0020 variable is read here. If you look at the links to it, you will see another place where the variable is written to this variable, and this will be exactly the place where I wanted to jump through the variable jmp_addr . This confirms the guess that it is definitely necessary to jump on sub_D3C .


But what was happening next I was too lazy to understand, so I threw rum in GHIDRA , found this function, and looked at the decompiled code:


 void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

We see that the variable with the strange name in_D1w , and also the variable DAT_00ff0020 , which reminds with its address the previously mentioned word_FF0020 .


in_D1w tells us that this value is taken from the D1 register, or rather from its younger WORD half, and sets the D1 register to the function that passes it. Remember #$4147 ? So you need to designate this register as the input argument of the function.


To do this, in the window with decompiled code, right-click on the function name, and select the Edit Function Signature menu item:



In order to indicate that the function takes an argument through a specific register, namely, not in the standard way for the current convention of calls, check the Use Custom Storage and click on the green plus icon:



The position for the new input argument appears. We click on it twice, and we get the dialog indicating the type and carrier of the argument:



In the decompiled code, we see that in_D1w is of type ushort , which means we will specify it in the field with type. Then click the Add button:



A position will appear to indicate the carrier of the argument, we need to specify the D1w register in the Location , and click OK :



The decompiled code will look like:


 void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; } 

We know that the value of param_1 is constant, transmitted by the calling function and is #$4147 . Then what should be the value of DAT_00ff0020 ? We consider:


 0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50 

Because xor is a reversible operation, all constant numbers can be poked with each other and get the desired value of the variable DAT_00ff0020 .


 DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553 

It turns out that the value of the variable should be 0x4553 . It seems that I have already seen the place where this value is set ...



Conclusions and solutions


We arrive at the following results:


  1. First you need to jump to the address 0x0D3C , for this you need to enter the code 0D3C
  2. Jump to the function at address 0x2D86 , which sets the value of #$4147 in register D1 , for this you need to enter the code 2D86

Experimentally, we find out the key that needs to be pressed to verify the entered key: B We try:



Thank!


')

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


All Articles