Assembly Assortment


Computing 101.

You have a working knowledge of assembly from your journey thus far. Let's broaden it!

This module will explore the effects of a number of different assembly instructions, teaching you to recognize them and not panic in their presence. In most of these challenges, you'll reverse-engineer a binary to understand an instruction's effect on data and feed it input it will accept. In a few others, you'll put an instruction to work yourself, writing a small function around it. Either way, if you understand what the instruction does, you will get flags!


In the previous challenge, you reversed a program by finding password characters directly in the cmp instructions. This time, the program transforms your input before comparing it. You'll need to understand and mentally invert this operation to successfully pass the check!

At /challenge/reverse-me, there's a new SUID binary. It will do some math on the first byte of the first program argument, and compare it against a hardcoded value. If the comparison passes, it reads and prints the flag. Otherwise, it silently exits.

The new instruction here is add, as so:

add rax, 42

This adds 42 to the rax register and updates rax with the result. The following would result in rax having the value 99:

mov rax, 57
add rax, 42

Like many other instructions, add can handle memory, registers, or immediates, when you disassemble this binary with objdump -d -M intel /challenge/reverse-me, you might see something like:

add    BYTE PTR [rax],0x2a
cmp    BYTE PTR [rax],0x96

This adds 0x2a (42) to the first byte of your input (in memory), then checks if the result equals 0x96 (150). To figure out what character you need, just reverse the math: 150 - 42 = 108 (6c). Looking at man ascii, 0x6c is the character 'l'. So the required input character in this case is l (remember, man ascii is your friend for converting between hex values and characters)!

Once you've figured out the character, run the program:

hacker@dojo:~$ /challenge/reverse-me YOUR_CHARACTER_HERE

Now it's your turn! Go and get the flag.


WARNING: /challenge/reverse-me is a SUID binary --- it runs with elevated privileges so it can read /flag. However, debugging a program will drop its SUID privileges, which means the open("/flag") syscall inside will silently fail if you run it under gdb. You can use gdb or objdump to understand the binary, but make sure to run it directly (outside of gdb) to get the flag.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

In the previous challenge, the program used add to transform your input before checking it. This time, it uses sub (as in, subtract) instead. Analogous to add, sub rax, 42 will subtract 42 from rax and store the result in rax.

Otherwise, this challenge is the same as the previous one. Go get it!

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

This challenge introduces a new type of operation: bitwise XOR. Unlike add and sub, which do arithmetic, xor operates on individual bits.

The xor instruction computes the exclusive or of two values: for each bit position, the result is 1 if exactly one of the two input bits is 1, and 0 otherwise. For example:

  01100001  (0x61, 'a')
^ 00101010  (0x2a, 42)
---------
  01001011  (0x4b, 75)

In diagrams and expressions such as 0x4b ^ 0x2a, ^ means XOR; in assembly, the instruction is xor. The syntax is the same as add and sub: xor rax, 42.

A key property of XOR is that it's its own inverse: xoring a value with the same value twice gives back the original value. So if you see:

xor    BYTE PTR [rax],0x2a
cmp    BYTE PTR [rax],0x4b

The program XORs your input byte with 0x2a and checks if the result is 0x4b. To reverse this, XOR the target with the key: 0x4b ^ 0x2a = 0x61, which is 'a'.

Now: disassemble the binary, reverse the XOR, and get the flag!

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Here's a new instruction for your toolkit: and.

A bitwise and compares two values bit by bit. Each output bit is 1 only if both input bits are 1; otherwise it's 0. Here is the rule for a single pair of bits:

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

and applies that rule to every bit position at once:

  1011 0111   (your value)
& 0000 0001   (the mask)
---------
  0000 0001   (only the lowest bit survives)

Notice the mask above is 1 in only one place: the lowest bit. Wherever the mask is 0, the output is forced to 0, so every bit except the lowest is wiped out. What survives is just the value's lowest bit, all on its own.

That lowest bit is special: it tells you whether the whole number is even or odd. A number is even exactly when its lowest bit is 0, and odd exactly when its lowest bit is 1. So masking off everything but the low bit --- the way you just did --- hands you the number's parity.

Write a function called solve that takes a 64-bit value in rdi and returns, in rax, 1 if the value is even and 0 if it is odd.

One thing to watch: the bit you isolate comes out 1 for odd, but solve has to return 1 for even. So the low bit isn't quite your answer --- it's the answer turned around.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

You met and for the even/odd test; here you'll use it to keep only the bits you want.

A bitwise and compares two values bit by bit: each output bit is 1 only if both input bits are 1. That makes and the tool for masking --- keeping the bits you want and forcing the rest to zero.

Wherever the mask has a 1, the original bit passes through; wherever it has a 0, the result bit is cleared:

  1011 0110   (your value)
& 0000 1111   (the mask: keep the low 4 bits)
---------
  0000 0110   (everything above the low 4 bits is gone)

In x86 that masking is a single and, with the mask as the second operand:

and rax, 0xF

A common use is isolating the lowest byte of a value --- the low 8 bits --- by masking with 0xFF.

Write a function that takes a 64-bit value in rdi and returns, in rax, just its lowest byte. Call it LOBYTE, in capitals: standing for LOw BYTE, and often used as shorthand for this functionality in tools you'll become familiar with later. Export it with .global LOBYTE.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o lobyte.o lobyte.s
hacker@dojo:~$ ld -shared -o lobyte.so lobyte.o
hacker@dojo:~$ /challenge/check lobyte.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

A bitwise or also compares two values bit by bit, but its rule is the opposite of and: each output bit is 1 if either input bit is 1. That makes or the tool for setting bits --- turning specific bits on while leaving the rest alone.

Wherever the mask has a 1, the result bit is forced to 1; wherever it has a 0, the original bit passes through unchanged:

  0100 0001   ('A', 0x41)
| 0010 0000   (turn on 0x20)
---------
  0110 0001   ('a', 0x61)

That example is a real trick. In ASCII, an uppercase letter and its lowercase partner differ only in the 0x20 case bit (the sixth bit from the right). The lowercase value is the uppercase value with the case bit set:

Uppercase Lowercase
A = 0x41 a = 0x61
H = 0x48 h = 0x68
P = 0x50 p = 0x70
Z = 0x5A z = 0x7A

or takes its operands the same way and does --- the value to modify, then the mask. Set the case bit with or, and any uppercase letter becomes its lowercase form.

Write a function that takes an uppercase ASCII letter in rdi and returns its lowercase form in rax. Call it chr_lower --- name your functions for what they do, and your code stays readable as it grows. Export it with .global chr_lower.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o chr_lower.o chr_lower.s
hacker@dojo:~$ ld -shared -o chr_lower.so chr_lower.o
hacker@dojo:~$ /challenge/check chr_lower.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Lowercasing set the case bit with or. Uppercasing is the mirror image: you clear that same bit.

and is the bit-clearing tool --- you used it to mask bits down to zero. A 0 in the mask forces the result bit off; a 1 lets the original bit through. So to clear just the 0x20 bit and keep everything else, the mask is 0x20 flipped: 0xDF.

  0110 0001   ('a', 0x61)
& 1101 1111   (0xDF: keep every bit except 0x20)
---------
  0100 0001   ('A', 0x41)
and rax, 0xDF

That clears the case bit of one letter. This time, though, you'll do it to a whole string.

A string is a run of bytes in memory, one after another, ending in a 0 byte --- the NUL terminator --- that marks where it stops. To walk it you need a loop: the same jmp-back-to-the-top shape you took apart in the Looping challenge, now written by hand.

So your loop is: look at the next byte; if it's the NUL (0), jump past the loop and you're done; otherwise clear its case bit, store the byte back, advance to the next one, and jump back to the top. The high-level of the loop would be:

loop:
  ...
  je done
  ...
  jmp loop
done:
  ...
  ...
  ret

Write a function that takes a pointer in rdi to a lowercase ASCII string and uppercases it in place, looping until the NUL. It returns nothing. Call it str_upper (it works on a whole string, not one character), and export it with .global str_upper.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o str_upper.o str_upper.s
hacker@dojo:~$ ld -shared -o str_upper.so str_upper.o
hacker@dojo:~$ /challenge/check str_upper.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

or always sets the case bit (forcing lowercase); and always clears it (forcing uppercase). Neither one swaps case: each only pushes a letter one direction.

To flip a bit --- on to off, off to on --- you need xor. Since xor sets a bit exactly when its two inputs differ, XORing a bit with 1 inverts it, and XORing with 0 leaves it alone. So XORing a letter with 0x20 flips its case bit either way: uppercase becomes lowercase, and lowercase becomes uppercase.

  0110 0001   ('a', 0x61)        0100 0001   ('A', 0x41)
^ 0010 0000   (flip 0x20)      ^ 0010 0000   (flip 0x20)
---------                      ---------
  0100 0001   ('A', 0x41)        0110 0001   ('a', 0x61)

That trick works because every byte here is a letter. The 0x20 bit only means "case" for letters; flip it on a space or a digit and you get a different, wrong character. The strings you're handed are all A-Z and a-z, so a blind toggle of every byte is safe --- no need to check.

Walk the string the same way as before --- load a byte, flip its case bit, store it back, advance, and repeat until the NUL.

Write a function that takes a pointer in rdi to a mixed-case ASCII string and swaps the case of every letter in place, looping until the NUL. It returns nothing. Call it str_swapcase, and export it with .global str_swapcase.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o str_swapcase.o str_swapcase.s
hacker@dojo:~$ ld -shared -o str_swapcase.so str_swapcase.o
hacker@dojo:~$ /challenge/check str_swapcase.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The shift instructions slide the bits of a value sideways. shl (shift left) moves every bit toward the high end, dropping bits off the top and feeding in zeros at the bottom:

  0000 0011   (3)
shl by 2
  0000 1100   (12)

Each position you shift left doubles the value, so shifting left by n multiplies by 2ⁿ (here, 3 << 2 == 3 * 4 == 12). Shifting left is also how you slide a small value up into a higher byte to make room for other values beside it.

The instruction takes the value and a shift amount:

shl rax, 2

Write a function called solve. It takes one input in rdi. Compute input << 4 (the original value times 16), and return the result in rax.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

shr (shift right) is the mirror of shl: it slides every bit toward the low end, dropping bits off the bottom and feeding in zeros at the top. Shifting right by n divides by 2ⁿ, but its other big use is positioning --- bringing a byte that sits higher up in a register down to the bottom, where you can mask it off.

Say you want the second byte of a value (bits 8 through 15). First shift it down to the bottom with shr, then keep just those 8 bits with the and mask from earlier:

  .... 1010 1011  0000 0001   (byte 1 = 0xAB, byte 0 = 0x01)
shr by 8
  .... 0000 0000  1010 1011   (byte 1 is now at the bottom)
and 0xFF
  .... 0000 0000  1010 1011   (just byte 1: 0xAB)

Two instructions, one idea: shift the byte you want into place, then mask it. To get the third byte of a register:

shr rax, 16
and rax, 0xFF

Write a function called solve that takes a value in rdi and returns its second byte --- bits 8 through 15, as a number from 0 to 255 --- in rax.

Build it into a shared library and hand it to the grader:

hacker@dojo:~$ as -o solve.o solve.s
hacker@dojo:~$ ld -shared -o solve.so solve.o
hacker@dojo:~$ /challenge/check solve.so

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Back in Opening the Flag, with RIP, you used a label and lea to pass the address of stored bytes to open. In Examining Memory with GDB, you used x/s to display the string at an address.

The next challenge combines those ideas in a binary you are reversing. The binary has already done the lea-style work for you: one of its registers will hold the runtime address of a stored string. Instead of looking for every secret byte as an immediate inside a cmp, step through the code until a register points at the stored string, then examine that address as a string:

(gdb) x/s $rsi

Here, $rsi is just an example register. Use the register that the binary loads with the stored string's address, then run /challenge/reverse-me directly with the string you found.

So far, the values you've been reversing have been embedded directly in instructions as immediate operands. However, this challenge compares the first program argument against a hardcoded string inside the challenge. The string lives in a different section of the program file: the binary's .rodata (read-only data) section, rather than in the instructions themselves.

There are several options to find it:

  • The most familiar: stepi to where the comparison is happening and x the registers pointing to the data.
  • Use strings /challenge/reverse-me to list all printable strings in the binary. There are a lot, but one of them will be the password.
  • Use objdump -s -j .rodata /challenge/reverse-me to dump the raw contents of the .rodata section.

Which you use is up to you!

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score