Skip to content

Latest commit

 

History

History

reversing-runme

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Reversing - runme

This sweet little program just wants us to run it, and in exchange for this unweighty trifle it will give us the flag! How nice of it!

But maybe let's first have a short look in IDA? It starts with the quite predictable open-malloc-read-close dance, but then it forks, increments a 16-bit counter:

.text:080489E3 memset_0_done:
.text:080489E3                 call    _fork
.text:080489E8                 mov     [esp+0F0h+pid], eax
.text:080489EC                 movzx   eax, [esp+0F0h+incme_after_fork]
.text:080489F1                 add     eax, 1
.text:080489F4                 mov     [esp+0F0h+incme_after_fork], ax
.text:080489F9                 cmp     [esp+0F0h+pid], 0
.text:080489FE                 jz      in_child

hashes a 16-byte buffer we've just memset to all zeroes:

.text:08048A04 in_parent:
.text:08048A04                 mov     [esp+0F0h+arg1], 10h
.text:08048A0C                 lea     eax, [esp+0F0h+buf]
.text:08048A10                 mov     [esp+0F0h+arg0], eax
.text:08048A13                 call    hash_md5_val
.text:08048A18                 cmp     [esp+0F0h+incme_after_fork], 0
.text:08048A1E                 jns     incme_after_fork_ge_0 ; 2**15 times

goes through a bunch of nops:

.text:08048B2A in_child:                               ; CODE XREF: main+101↑j
.text:08048B2A                 nop

falls through to waiting for its child:

.text:08048B3F incme_after_fork_ge_0:                  ; CODE XREF: main+121↑j
.text:08048B3F                 call    _wait
.text:08048B44                 jmp     memset_0_done

and repeats the whole sequence until 16-bit counter's sign bit does not become 1, that is, 2 ** 15 times. This means that that many processes will be spawned at a time. That's not a proper fork bomb yet, but it's not hard to imagine that a reasonable person would still find this rude and not acceptable.

So, let's defuse the evil contraption?

# skip fork
b *0x080489E3
commands
  set $eip = 0x080489E8
  set $eax = 1
  c
end

# skip wait
b *0x08048B3F
commands
  set $eip = 0x08048B44
  c
end

The code runs quite quickly and orderly under this gdbscript. Instead of creating new processes, it does all the hashing inside of the main one, and then, as intended, uses the result as an AES key to decrypt the flag. Unfortunately, this produces total garbage. Where is the catch?

Let's try to recap how the program is supposed to work, given infinite time and resources:

main:
  memset(buf, 0, sizeof(buf))
  for _ in range(2 ** 15):
    hash(buf)
    child0:
      wait()  # should return -1, errno=ECHILD
      for _ in range(2 ** 15 - 1):
        hash(buf)
        child1:
          ...
          exit(0)
        wait(child1)  # should return 0 and set *wstatus = 0
        decrypt()
        exit(0)
    wait(child0)  # should return 0 and set *wstatus = 0
  decrypt()
  exit(0)

So, the whole action is sequential, despite the number of involved processes. Children will create each other recursively, each ultimately hashing the buffer 2 ** 15 times and saving the decrypted flag to the disk, overwriting each other's work. Finally, the parent would save its own decrypted version, overriding what the last child did. This should be the correct flag.

The bothersome thing is:

.text:08048B3F                 call    _wait
.text:08048B44                 jmp     memset_0_done

Where is the argument? Where will wstatus be saved? This is a 32-bit program, so all the arguments are passed on stack. The last time something was put on top of the stack was:

.text:08048A0C                 lea     eax, [esp+0F0h+buf]
.text:08048A10                 mov     [esp+0F0h+arg0], eax
.text:08048A13                 call    hash_md5_val

This is the buffer that is being hashed! So, sneakily, in addition to hashing, the code overwrites the first 32 bits of the buffer with wstatus, which is supposed to always be zero. It could be possible to enhance gdbscript to simulate that, but now it's also possible to simply implement the whole logic in Python. Doing this correctly decrypts the flag: SECCON{infinite_fork_is_not_good_for_system}. It sure isn't.