1

I'm doing a safes competition and I got this safe:

start:
add     ds:0DEDh, ax
xor     cx, cx
loop    start

From my understanding, cx will be 0 at the end of the loop and will change to FFFF at the next iteration. I also know the 0xCCh is an illegal instruction that will stop the program. how can I crack this safe?

**edit: the goal here is to stop this infinite loop. the loop has no stopping term and I need to somehow make it stop using reverse engineering. for example: this is a simple safe

safe:
  mov     ax, ds:4D2h
  cmp     ax, 1000h
  jl      safe 

this is its key, written using reverse engineering:

mov bx, 1000h
mov [4D2h], bx    
l:
    jmp l

This simulation of a safe and key is done inside the Core Wars 8086 engine. The rules are as follows where both safe and key are survivors in the war:

The survivors cannot place a load on fixed addresses, because the game engine loads them every turn to a random address. The programs that are generated must be COM and not EXEs and contain only 8086 instructions.

Each survivor receives a set of its own complete registers (registers), which is not accessible to the other survivors. In addition, each survivor has a "personal" stack of 2048 bytes, which is also inaccessible to the other survivors.

Before running the first round of the game, the game engine initializes all the bytes in the arena to the value 0CCh (note: this byte value is an "unsupported" instruction - details below). The engine then loads each survivor to a random location in the arena memory, ie - copies the contents of the survivor file exactly as it is. The distance between two survivors, as well as the distance between the survivor and the edge of the arena, is guaranteed to be at least 1024 bytes. The code for each survivor has a maximum of 512 bytes.

Before the first round, the game engine initializes the registers (of each survivor) to the following values:

  • BX, CX, DX, SI, DI, BP - Reset.
  • Flags - Reset.
  • AX, IP - The position of the initial survivor, the random offset in the arena to which the survivor is loaded by the game engine.
  • CS, DS - The segment of the arena common to all survivors.
  • ES - A segment (segment) for the memory shared by survivors of the same group (see Advanced Techniques ).
  • SS - Beginning section of the personal stack of the survivor.
  • SP - Offset The start of the personal stack of the survivor.

At this point the game begins in rounds, with each round running the game engine running the next instruction of each survivor, until the end of the game: after 200,000 rounds, or when a single survivor remains in the arena. The order in which the survivors will play in each round is determined at the beginning of the game at random, and does not change during it.

A survivor is disqualified in the following cases:

  • Running an illegal instruction (example: byte 060h that does not translate into any assembly instruction).
  • Running an "unsupported" instruction by the game engine (example: "INT 021h"). The game engine prevents running instructions that try to initiate direct communication with the operating system or computer hardware. Attempt to access memory that is not within the realm of the arena, and not within the realm of the "personal" stack of the survivor.
  • Attacking other survivors is done by writing information about their code in the arena memory (in order to get them to perform one of the above three actions), and consequently to disqualify them. Earlier, therefore, one has to find where they are hiding :)
Michael Petch
  • 46,082
  • 8
  • 107
  • 198
nit17
  • 47
  • 5
  • Where is the code in memory? Can that `add` modify the code bytes? What exactly is the goal here, and what parameters do you control? I assume you get to choose register contents or something, and you're trying to arrange for execution to exit the loop? "safes competition" isn't a very well-known term. – Peter Cordes Dec 09 '20 at 18:36
  • 2
    For those of us who don't know how such competitions work, can you explain more clearly what you are trying to accomplish? What does "cracking the safe" entail? – Nate Eldredge Dec 09 '20 at 18:37
  • I still don't quite understand. So the "key" code is supposed to execute before the "safe" code, or while it's looping (like an interrupt handler), or what? – Nate Eldredge Dec 09 '20 at 18:49
  • they are running simultaneously, I don't know exactly how, but I think it's line by line from each code. – nit17 Dec 09 '20 at 18:51
  • my goal is to make the "safe" loop to break – nit17 Dec 09 '20 at 18:52
  • 2
    Can the "key" just overwrite the machine code of the safe with different instructions? This safe doesn't make much sense to me. The loop structure only depends on registers (CX specifically), not memory contents. `add ds:0DEDh, ax` only changes memory at `ds:0DEDh`, so unless that happens to overlap with the code, I don't think there's a way to get the safe to unlock itself by putting anything anywhere *else* in memory. Also, the modification it makes depends on what's in the safe "thread"'s AX. Do you have control over registers somehow? – Peter Cordes Dec 09 '20 at 19:08
  • 2
    My only guess is that maybe you're allowed to set `ds` before the loop is entered. In which case, depending how the safe code is aligned to a 16-byte boundary, you might get the `add` to modify its code. But without a more precise and detailed explanation of the "rules" I don't think this will be answerable. – Nate Eldredge Dec 09 '20 at 19:28
  • Note that being a "puzzle" this might be more appropriate for CodeGolf.SE. – Nate Eldredge Dec 09 '20 at 19:29
  • @NateEldredge: I don't think it would be well-received there. It doesn't allow for a lot of different correct answers, or at least doesn't provide any way to say that one key is "better" than another. (e.g. smaller code-size.) The fact that only x86 assembly language can be used is also a negative factor; not a showstopper if there was an otherwise good or interesting challenge, but this is really not the kind of puzzle that codegolf.SE deals with. If anything other than SO, maybe reverseengineering.SE. – Peter Cordes Dec 10 '20 at 13:30
  • re-asked with a couple extra details [here](https://stackoverflow.com/questions/65233532/assembly-safe-competition), e.g. that AX is random. But nothing about where the safe code lies in memory relative to `ds:0DEDh`. – Peter Cordes Dec 10 '20 at 13:33
  • 1
    @MichaelPetch: Ok *that* makes sense. If the OP had taken the time to explain in that level of detail (even without the part about how the solutions should work of course), the question wouldn't have been closed in the first place. Or at this point, could be reopened after an edit. But just saying "AX is random" is not helpful at all. – Peter Cordes Dec 10 '20 at 17:36
  • Did you ever solve your problem and did any of the answers here help? – Michael Petch Dec 17 '20 at 03:28

2 Answers2

5

Although AX has a random value, it does have meaning. That meaning is:

  • AX, IP - The position of the initial survivor, the random offset in the arena to which the survivor is loaded by the game engine.

AX=IP which is the instruction pointer where the safe was loaded in memory. All we have to do is update the first instruction in the loop with something that will force the safe to exit (die). That can be done by writing a byte value of 0x601 to that memory address according to the documentation. Since the safe's loop is also the start of the program we just have to retrieve safe's AX value and use it to overwrite that memory address.

Getting safe's AX would have been trivial if they had just written it to memory address 0x0DED but they kept adding AX to the previous WORD (16-bit value) in memory. This means that in order to figure out what safe's AX value is we need to read it twice and subtract the first value read from the second value read. We also have to do the 2 reads in such a way that we know safe has updated it exactly once between our reads. The rules of the Core Ware engine say:

At this point the game begins in rounds, with each round running the game engine running the next instruction of each survivor, until the end of the game: after 200,000 rounds, or when a single survivor remains in the arena. The order in which the survivors will play in each round is determined at the beginning of the game at random, and does not change during it.

That means every round one instruction is executed by the safe and the key. Since the safe's loop is 3 instructions:

start:
    add     ds:0DEDh, ax ; Instruction 1
    xor     cx, cx       ; Instruction 2
    loop    start        ; Instruction 3

We are guaranteed to read 2 different values at [0x0DED] every 4 instructions. Now that we know this it becomes relatively easy. We can simplify the assembly code by realizing that secondreadAX - firstreadAX is the same as (-firstreadAX) + secondreadAX. With that in mind a solution that does 2 reads four instructions apart and updates the start of safe's code (and the start of its loop) with 0x60 could look like (in NASM syntax):

; Assemble with:
;     nasm -f bin key.asm -o key

start:
    mov bx, [0x0DED] ; Get the WORD from [0x0DED]
    neg bx           ; Negate it. BX=(-firstreadAX)
    nop              ; Need to wait at least one more instruction (3 total)
                     ;     before trying to read 0x0DED again
    add bx, [0x0DED] ; Add the current WORD value at [0x0DED] with BX
                     ;     safeAX = -BX+[0x0DED] = [0x0DED]-BX
                     ;  or safeAX=(-firstreadAX)+secondreadAX = secondreadAX-firstreadAX
    mov byte [bx], 0x60
                     ; Overwrite the first instruction of safe with 0x60
                     ;     to terminate the safe.
    jmp $            ; Infinite loop

In TASM/MASM(v6.00+)/JWASM the code would look like:

.model tiny
.code

start:
    mov bx,[ds:0DEDh]; Get the WORD from [0DEDh]
    neg bx           ; Negate it. BX=(-firstreadAX)
    nop              ; Need to wait at least one more instruction (3 total)
                     ;     before trying to read 0DEDh again
    add bx,[ds:0DEDh]; Add the current WORD value at [0DEDh] with BX
                     ;     safeAX=-BX+[0DEDh]=[0DEDh]-BX
                     ;  or safeAX=(-firstreadAX)+secondreadAX = secondreadAX-firstreadAX
    mov byte ptr [bx], 060h
                     ; Overwrite the first instruction of safe with 060h
                     ;     to terminate the safe.
    jmp $            ; Infinite loop

END

Footnotes

  • 1Any opcode not supported by an 8086 will terminate a survivor and this includes 0x60, 0x66, 0x67 among others. Valid 8086 single byte opcodes like 0xcc (INT3), 0xce (INTO) and 0xf1 (INT1) will also exit because they are unsupported traps in the Core Wars engine. Calling software interrupts that are unsupported by the Core Wars engine (like DOS INT 21h) will also cause a survivor to fail.
Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • I'm a bit surprised to see that you have included the *example* safe and not the *target* safe for which you provide the solution. Is it perhaps true that all of these "safes" have precisely 3 instructions? – Sep Roland Dec 11 '20 at 17:11
  • 1
    Yes, that "1024 bytes. 512 houses" was something I didn't quite get from Google Translate. – Sep Roland Dec 11 '20 at 17:17
  • 1
    @SepRoland : I didn't understand that translation until I read this power point presentation (page 11): https://www.recon.cx/2014/slides/Elad%20Shapira%20-%20Shall%20we%20play%20a%20game%20-%20Lessons%20learned%20while%20playing%20CoreWars8086.pdf . When I get some time a bit later I will add info and links to the [corewars] tag. I had noticed that we have had other questions including this safe: https://stackoverflow.com/questions/59512527/assembly-safes-and-keys-why-it-wont-work – Michael Petch Dec 11 '20 at 17:20
  • @SepRoland I just realized what you were referring to. I didn't notice I had copy and pasted the wrong loop when I put comments on them. Oops, that is now fixed. – Michael Petch Dec 11 '20 at 17:28
  • Why does 0x60/0x66 terminate the program? Wasn't able to find any documentation about that anywhere. – snatchysquid Dec 11 '20 at 19:09
  • @snatchysquid : the documentation converted from Hebrew (there is a link to the original Hebrew docs about code guru extreme tournament in the question) that says 0x60 in particular will terminate the survivor in the Core Wars engine. Some other resources also suggest 0x66 (which is a prefix override on the 80286+ and wasn't used on the 8086). On the 8086 opcode 0x60 wasn't valid (eventually it became the opcode for PUSHA on the 80186+). The Core War engine assumes that anything that is an undefined instruction on the 8086 is not allowable and will kill the survivor. – Michael Petch Dec 11 '20 at 19:24
  • @MichaelPetch Thanks for making it clear. So any other nonvalid opcode will kill the program as well? – snatchysquid Dec 11 '20 at 19:30
  • 1
    @snatchysquid : correct any undefined opcode will make it fail (0x66, and 0x60, and even 0x67 among others). I have verified this by testing it in Core Wars. As well any interrupt call that the Core War engine doesn't allow will also kill the survivor. If you were to put `int 21h` into your survivor it will also end. – Michael Petch Dec 11 '20 at 19:34
3

the goal here is to stop this infinite loop. the loop has no stopping term and I need to somehow make it stop using reverse engineering

It's not too clear what you can and can't do. Following is a solution that only requires you to change a single byte.

Going from xor cx, cx to the harmless mov cx, cx will no longer reset the loop counter and thus the loop will end some time later (depending on the initial value of CX that we do not know).
The opcode for mov cx, cx is 89h. We don't need to change the modr/m byte because its value is the same for both instructions.

mov byte [cs:start+4], 89h

It could be useful to verify that the assembler did not include the otherwise redundant DS: segment override prefix because if that's the case you would have to write mov byte [cs:start+5], 89h.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Sep Roland
  • 33,889
  • 7
  • 43
  • 76
  • 2
    @MichaelPetch `AX` being the safe's address in memory was the crucially missing info. Having to wait a couple of instructions between successive reads is kind of a given. I believe your solution will do the trick. Nice work! – Sep Roland Dec 10 '20 at 20:46