Alok Menghrajani

Previously: security engineer at Square, co-author of HackLang, put the 's' in https at Facebook. Maker of CTFs.


This blog does not use any tracking cookies and does not serve any ads. Enjoy your anonymity; I have no idea who you are, where you came from, and where you are headed to. Let's dream of an Internet from times past.


Home | Contact me | Github | RSS feed | Consulting services | Tools & games

A few years ago, I crafted a bootable floppy disk and a retro game which fit in a tweet. Since then, Twitter doubled the length of tweets, so I decided to handcraft a bootable CD. The bootable disk runs a slightly improved version of tron.

perl -E 'say"A"x46422,"BDRDAwMQFFTCBUT1JJVE8gU1BFQ0lGSUNBVElPTg","A"x54,"Ew","A"x2634,"/0NEMDAxAQ","A"x2721,"BAAAAYQ","A"x30,"SVVVqogAAAAAAAEAF","A"x2676,"LMBaACgB76gfbgTAM0Qv8D4uYAI86qqgcc+AXP45GA8SHIRPFB3DTeYSEhyBSwCa8CwicMB3rSGtu/NFbRFJjAke9rrwQ","A"x2638'|base64 -D>cd.iso

The code in the tweet creates a bootable CDROM disk image: cd.iso. You can boot the code in qemu, virtual box or your favorite virtualization software and play the game with the arrow keys. You can even burn the iso to a blank CD (or USB drive) and boot real hardware.

note: on some systems, e.g. Linux, you'll have to use base64 -d.

To handcraft a CD disk image, you must first get a basic understanding of ISO 9660. ISO standards unfortunately are usually expensive. ISO 9660 is however the same as ECMA 119, which means you can read the specification for free.

ISO 9660 has many extensions, such as UDF, El Torito, RockRidge, Joliet, etc. For bootable images, we only care about El Torito. The El Torito specification is, in my opinion, poorly written. There are mistakes (e.g. figure 7, last row), it's easy to forget that values are hex (no 0x prefixes), the figures aren't sorted in an intuitive order, etc. Thankfully, the document is fairly short.

To create a bootable disk, we start by writing 17 empty sectors followed by the Volume Descriptor Set. A sector is 2048 bytes.

Note: The ISO-9660 specification says the Volume Descriptor Set starts at sector 16. The El Torito specification calls for the Boot Record to live at sector 17. Technically, we should put a dummy Volume Descriptor in sector 16, but things seem to work fine without.

We write the first Volume Descriptor:

0x00                      // Type (0 = boot record)
'CD001'                   // Identifier
0x01                      // Version
'EL TORITO SPECIFICATION' // Boot System Identifier
9 x 0x00                  // Padding
32 x 0x00                 // Unused
0x13 0x00 0x00 0x00       // Boot Catalog address (in absolute sectors)
1973 x 0x00               // Unused

The next sector is the Volume Descriptor Set Terminator:

0xff                      // Type (255 = terminator)
'CD001'                   // Identifier
0x01                      // Version
2041 x 0x00               // Unused

The Volume Descriptors are followed by the Boot Catalog. El Torito supports various emulation modes. The CD-ROM can emulate a bootable floppy, bootable harddrive, etc. I picked no emulation, which implies the BIOS will load a specific number of sectors and jump into our bootloader.

The checksum is computed such that all 16-bit values in the record sum to 0 (mod 65536).

First entry in the Boot Catalog (Validation Entry):

0x01                      // Header ID
0x00                      // Platform ID (0 = Intel x86)
0x00 0x00                 // Reserved
'a'                       // ID string
23 x 0x00                 // Padding
cksum cksum               // Checksum (2 bytes)
0x55 0xaa                 // Key bytes

Second entry (Default Entry):

0x88                      // Boot Indicator (0x88 = bootable)
0x00                      // Boot Media Type (0 = no emulation)
0x00 0x00                 // Load segment
0x00                      // System Type
0x00                      // Unused
0x01 0x00                 // Number of sectors to load
0x14 0x00 0x00 0x00       // Virtual disk address (in absolute sectors)
20 x 0x00                 // Unused

Followed by zeros until the end of the sector:

1984 x 0x00               // Unused

The next sector is the bootloader / retro game:

; to compile:
; nasm bootloader.asm -o bootloader.img
          [bits 16]                    ; Pragma, tells the assembler that we
                                       ; are in 16 bit mode (which is the state
                                       ; of x86 when booting from a floppy).
          [org 0x7C00]                 ; Pragma, tell the assembler where the
                                       ; code will be loaded.

          mov bl, 1                    ; Starting direction for the worm.
          push 0xa000                  ; Load address of VRAM into es.
          pop es

restart_game:
          mov       si, 320*100+160    ; worm's starting position, center of
                                       ; screen

          ; Set video mode. Mode 13h is VGA (1 byte per pixel with the actual
          ; color stored in a palette), 320x200 total size. When restarting,
          ; this also clears the screen.
          mov       ax, 0x0013
          int       0x10

          ; Draw borders. We assume the default palette will work for us.
          ; We also assume that starting at the bottom and drawing 2176 pixels
          ; wraps around and ends up drawing the top + bottom borders.
          mov       di, 320*199
          mov       cx, 2176
          rep
draw_loop:
          stosb                        ; draw right border
          stosb                        ; draw left border
          add       di, 318
          jnc       draw_loop          ; notice the jump in the middle of the
                                       ; rep stosb instruction.

game_loop:
          ; We read the keyboard input from port 0x60. This also reads bytes from
          ; the mouse, so we need to only handle [up (0x48), left (0x4b),
          ; right (0x4d), down (0x50)]
          in        al, 0x60
          cmp       al, 0x48
          jb        kb_handle_end
          cmp       al, 0x50
          ja        kb_handle_end

          ; At the end bx contains offset displacement (+1, -1, +320, -320)
          ; based on pressed/released keypad key. I bet there are a few bytes
          ; to shave around here given the bounds check above.
          aaa
          cbw
          dec       ax
          dec       ax
          jc        kb_handle
          sub       al, 2
          imul      ax, ax, byte -0x50
kb_handle:
          mov       bx, ax

kb_handle_end:
          add       si, bx

          ; The original code used set pallete command (10h/0bh) to wait for
          ; the vertical retrace. Today's computers are however too fast, so
          ; we use int 15h 86h instead. This also shaves a few bytes.

          ; Note: you'll have to tweak cx+dx if you are running this on a virtual
          ; machine vs real hardware. Casual testing seems to show that virtual machines
          ; wait ~3-4x longer than physical hardware.
          mov       ah, 0x86
          mov       dh, 0xef
          int       0x15

          ; Draw worm and check for collision with parity
          ; (even parity=collision).
          mov ah, 0x45                 ; Color (must have odd parity)
          xor [es:si], ah

          ; Go back to the main game loop.
          jpo       game_loop

          ; We hit a wall or the worm. Restart the game.
          jmp       restart_game

TIMES 2048 - ($ - $$) db 0             ; Fill the rest of the sector with 0

I then wrote a script to compile the bootloader, assemble the image, and generate the tweet. Finally, I burned a CD and tested that things work on real hardware.