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.

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!

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!

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

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

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

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 practiced in Writing Loops.

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

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

A bit shift moves every bit in a fixed-width value the same number of positions. A left shift moves bits toward the high end, drops the bits that leave that end, and inserts zeros at the low end. Here is a two-position left shift shown in one byte:

before:                    1011 0010  (178)
dropped from high end:     10.. ....
inserted at low end:       .... ..00
after shifting left by 2:  1100 1000  (200)

A logical right shift does the mirror image: it moves bits toward the low end, drops the low bits, and inserts zeros at the high end.

before:                     1101 1011  (219)
dropped from low end:       .... ..11
inserted at high end:       00.. ....
after shifting right by 2:  0011 0110  (54)

Each binary position is worth twice the position to its right. Moving every bit left by one therefore doubles an unsigned value, and shifting left by n multiplies by 2^n as long as no 1 bit is lost past the high end. Moving every bit right by one halves an unsigned value, discarding any remainder, so shifting right by n divides by 2^n.

On 64-bit x86, shl shifts left and shr performs the zero-filling right shift. They use the same destination-first form as instructions such as add:

shl rax, 2
shr rax, 2

These examples use a byte so every bit fits on screen, but the same movement happens across all 64 bits of rax.

Now put the left shift you just studied into code.

Write a function called solve that takes a 64-bit value in rdi, shifts it left by 4 positions (multiplying it by 16), and returns 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

Now use shr for positioning. Move the input's second byte down to the low end, then reuse the mask from Masking Bits to discard everything outside that byte.

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

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!


30-Day Scoreboard:

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

Rank Hacker Badges Score