A hacking story of a classic Dendy or Contra game with spreadgan at the beginning
- Tutorial
Since my last article , to my great surprise, interested you. I decided to supplement its result, a hacked version of the game "Contra (J) [T + Rus_Chronix]", with a little functionality, at the same time showing "code injection" on NES. This time I will make the players start the game with the pumped Spreadgun, to get it in the game you need to select the icon "S", followed by "R".
All interested welcome under cat.
Traditionally:
And we traditionally plan the sequence of actions.
- Find Addresses
- Find out the value of pumped Spreadgun
- Find out what he writes to these addresses at the beginning of the game
- Rewrite ROM
- Easyway - change the value of the basic weapon to the pumped spreadgan
- Hardway - use a full code injection if the easy way fails
- save the result to a new file
To search for addresses, we use the previously described method, but remembering that lives were in neighboring addresses, we will only look for weapons from the first player in the hope that the second player will be nearby. Opening the "Ram watch" window after the start of the first level, we look for an unknown value. I believe the base weapon is set to 0, but I don't know for sure.
We run along the level without running ahead, shoot in all directions and weed out the changing values. The weapon has not changed yet.
Let's use another window option, or rather, in the field "Compare To / By", highlighting the radio frequency "Number of Changes", put in the field 0. The type of comparison is of course "Equals to". The weapon still did not change.
So jumping from the option "equal to the previous value", to the option "the number of changes is 0", you can reach about 10,000 addresses. When further screenings will reduce the address list too slightly or not at all, one can go forward far enough to knock out the first weapon.
Having picked it up, we immediately use the search method "not equal to the previous value", and further reduce the list by searching "the number of changes is 1", the weapon changed exactly once.
In the place where we pick up our first weapon, a weapon amplifier also appears. Having selected and picked it up, you can reduce the number of addresses to 1, but if it doesn’t work out, just kill your character and thus change the weapon again. (Do not forget about the search after each change).
With a weapon amplifier, there is some risk that it will not change the value of the weapon itself, but the flag elsewhere in memory, but let's hope that the authors of the game saved memory and instructions. And, I was lucky bonus "R" changed the very value of the weapon and the address was in coordinate AA 16 . (I could be wrong, but I repeatedly monitored the hero’s weapon in different versions of the Contra game, like everywhere this address was AA 16 ).
I also noticed that the bonus "R" increased the value in the address by 16 10 or 10 16 , that is, increased by 1 the first digit of the hexadecimal number.
After the restart in the “Ram watch” window, it can be seen that the base value is really 00 16 , the bonus “M” increased the second digit by 1, and the bonus “R” the first.
You can go for a spreadgan, it will definitely meet at this level, or you can change the value of the address to see what numbers, what weapons they create. Empirically, I found out that, 01 16 is “Machinegun”, 02 16 is “Fire”, 03 16 is “Spreadgun”, and 04 16 is “Laser”. When you enter other values in the second digit, various glitches occur.
After resetting the game and entering the value 1x 16 , (where "x" is any of the acceptable options) before selecting the "Rapid" bonus, you can find out that re-selecting the bonus does not change anything.
Now you can restart the game, start the game for two and try to change the addresses adjacent to AA 16 . (There are two of them, the search will not be long) Having shot the second player, I very quickly found out that the weapons of the second player are really stored nearby at address AB 16 . And now we know the addresses of interest to us and the value that should be put there, it's time to find out what he writes to these addresses.
Throwing a breakpoint on the record of this address, I found out that the recording happens there several times and one of them after the splash screen. The following code makes this entry:
Address | Opcode | Mnemonic | Arguments | A | X |
---|---|---|---|---|---|
C307 | A2 28 | LDX | # $ 28 | ?? | ?? |
C309 | A9 00 | Lda | # $ 00 | ?? | 28 or 29 or ... or F0 |
C30B | 95 00 | STA | $ 00, X | 00 | 28 or 29 or ... or F0 |
C30d | E8 | Inx | 00 | 28 or 29 or ... or F0 | |
C30e | E0 F0 | CPX | # $ F0 | 00 | 29 or 30 or ... or F0 |
C310 | D0 F9 | Bne | $ C30B | 00 | 29 or 30 or ... or F0 |
If you carefully read the game here zeroes the range of addresses from 0028 16 to 00F0 16 , obviously both addresses of interest to us in the range. So there will be no easy way. I’ll have to use “Code Injection” and the simplest solution that I see to find out where we get from here, redirect the execution to some place free of code and data in memory, write there my version of the loop occupying the entire range except addresses 00AA 16 and 00AB 16 and returning the carriage execution back. By the way, this is the most classic version of the injection. You can also assume that we get here from the JSR (Jump to SubRoutine) instruction, this is easy to check with the stack.
In 6502 processors, the stack is always located in the address range 0100 16 - 01FF 16 for all computers based on this processor, and grows from a larger address to a smaller one. There is a separate register pointing to the top of the stack, initially it is equal to FF 16 since a more significant byte never changes. The register itself always indicates the highest byte unoccupied with useful data.
The emulator debugger does not show the value of the "Stack Pointer" register, instead it shows the address to which the register refers and right now it is 01F2 16 , simple calculations show that the last data on the stack is C3 16 and C2 16 , which could lead I think about the address C3C2 16 but 6502 is a processor of the "Little Endian" type, and therefore, when executing instructions and storing the address in memory or on the stack, the less significant byte is written first. And if the address really is on the top of the stack, this is address C2C3 16 . And this is the address of the last argument of the JSR instruction, if again, this is the address at all. It is very easy to check, just look at what is written two bytes above the address C2C3 16 .
Address | Opcode | Mnemonic | Arguments |
---|---|---|---|
C2C1 | 20 07 C3 | Jsr | $ C307 |
As you can see, this is a JSR instruction to C307 16 , which means that the assumption of subroutine is correct.
Now you need to write the injection code correctly, find a suitable place for it, write it to this place and redirect the JSR instruction to this injection.
Opcodes , instructions , a lot of information about the 6502 assembly .
For this, it is very convenient to use a notepad, I have Visual Studio Code for it. Everyone has their own writing style, personally, I am the first to write a JSR instruction with its address, in order to know where to change and a full opcode, in order to know what to change.
After a couple of indents, I duplicate the loop code, it can already be used without addresses, but it’s extremely useful to see mnemonics with arguments other than opcodes, and it’s useful to grab the instruction following this loop along with the address to know where to return from the injection.
C2C1:20 07 C3 JSR $C307
A2 28 LDX #$28
A9 00 LDA #$00
95 00 STA $00,X
E8 INX
E0 F0 CPX #$F0
D0 F9 BNE $C30B
C312:A2 07 LDX #$07
A couple of indents below you can write the code of the injection itself, in fact it is a duplicate of the cycle itself with some additions.
C312:A2 07 LDX #$07
A2 28 LDX #$28
A9 00 LDA #$00
95 00 STA $00,X
E8 INX
E0 AA CPX #$AA
D0 F9 BNE -7
A9 13 LDA #$13
95 00 STA $00,X
E8 INX
E0 AC CPX #$AC
D0 F9 BNE -7
A9 00 LDA #$00
95 00 STA $00,X
E8 INX
E0 F0 CPX #$F0
D0 F9 BNE -7
For clarity, I indicated the indent instead of the jump location address.
Here is the same cycle, but divided into three parts, in the first cycle we compare the register X with the value AA 16 , since we need to zero all addresses to the address 00AA 16 , after we put 13 16 in the register A , the value of the pumped spreadgan and write the second cycle it to the address 00AC 16 starting from which the remainder of the range should be zeroed again. We return to register A zero and zero the remainder of the range.
It is imperative to complete the injection return instructions at the end.
E0 F0 CPX #$F0
D0 F9 BNE -7
4C 12 C3 JMP $C312
Now, for convenience, I prefer to write the opcodes below in the file.
4C 12 C3 JMP $C312
A2 28 A9 00 95 00 E8 E0 AA D0 F9 A9 13 95 00 E8 E0 AC D0 F9 A9 00 95 00 E8 E0 F0 D0 F9 4C 12 C3
And by counting the opcodes it is easy to find out that there are 32 of them . 10 Therefore, you need to find 20 16 unoccupied addresses on ROM. As a rule, unoccupied addresses are large spaces of the same values, most often zeros or FF 16. It is just such a large piece that needs to be found, it must be at least 35 10 addresses so that there is some margin.
В "Hex Editor" у меня такой диапазон нашёлся в B29E16-BFFF16. Использовать для инъекций начало подобных свободных участков может быть опасно, потому советую писать код инъекций в его конце. Наиболее удобный адрес для начала инъекции BFE016, но это адрес в памяти консоли, чтоб выяснить где он находится в РОМ файле нужно кликнуть по нему правой кнопкой мыши и выбрать пункт "Go Here In ROM File".
Теперь можно скопипастить сюда весь опкод (32 значения). Последний штрих поменять инструкцию
C2C1:20 07 C3 JSR $C307
на прыжок в адрес инъекции у меня это BFE016.
C2C1:20 E0 BF JSR $BFE0
Разумеется найдя истинное место инструкции на РОМе.
The address of the JSR instruction gets on the stack, therefore, regardless of the place of the jump, the same return address will be there, and from the injection we return to the code with a JMP instruction that does not affect the stack in any way. Thus, the RTS instruction works in the same place as without injection, and returns to the same place as without injection. As a result, this injection does not break the stack.
PS For good, you still need to make sure that after death the characters are reborn with a pumped spread spread, but I’m sure armed with the acquired knowledge you can handle it yourself. I’ll turn my eyes to something else. Unity engine for example.