709 lines
26 KiB
Markdown
709 lines
26 KiB
Markdown
# Google CTF 2020 Write-up: Sprint
|
||
|
||
In this challenge, we're given a binary to reverse engineer. We find
|
||
that it's an interpreter for a Turing-complete language made entirely
|
||
of `sprintf` format strings, along with a program in this language.
|
||
We write a disassembler for this language, and use it to
|
||
reverse engineer the embedded program. We find that the embedded
|
||
program is a game, and map out a series of moves to win the game.
|
||
|
||

|
||
|
||
## Picking apart the binary
|
||
|
||
We're given a single binary file. Running it through `objdump`, we
|
||
see that everything is in a single `main` function. Let's pick it
|
||
apart.
|
||
|
||
After setting up the stack, the first function call is this:
|
||
|
||
1199: 41 b9 00 00 00 00 mov $0x0,%r9d
|
||
119f: 41 b8 ff ff ff ff mov $0xffffffff,%r8d
|
||
11a5: b9 22 00 00 00 mov $0x22,%ecx
|
||
11aa: ba 03 00 00 00 mov $0x3,%edx
|
||
11af: be 00 00 00 04 mov $0x4000000,%esi
|
||
11b4: bf 00 00 00 04 mov $0x4000000,%edi
|
||
11b9: e8 82 fe ff ff callq 1040 <mmap@plt>
|
||
11be: 48 89 45 c8 mov %rax,-0x38(%rbp)
|
||
|
||
This translates to the following C:
|
||
|
||
void *mapped = mmap(0x4000000, 0x4000000, PROT_READ | PROT_WRITE,
|
||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||
|
||
Where `mapped` is the local variable at `-0x38(%rbp)`. This gives us
|
||
a chunk of memory at `0x4000000`. Technically, it only *suggests*
|
||
that the chunk of memory is *near* `0x4000000`, but checking in `gdb`
|
||
shows it actually is `0x4000000`. This is important later for some
|
||
pointer calculations.
|
||
|
||
Next we call `memcpy` to copy some data from a constant `M` to `mapped`:
|
||
|
||
11c6: ba 34 f1 00 00 mov $0xf134,%edx
|
||
11cb: 48 8d 35 4e 0e 00 00 lea 0xe4e(%rip),%rsi # 2020 <M>
|
||
11d2: 48 89 c7 mov %rax,%rdi
|
||
11d5: e8 86 fe ff ff callq 1060 <memcpy@plt>
|
||
|
||
Lets check what this data is. We copy bytes starting at `0x2020`, and
|
||
ending at `0x2020+0xf134=0x11154`.
|
||
|
||
$ objdump -s -j .rodata sprint
|
||
|
||
sprint: file format elf64-x86-64
|
||
|
||
Contents of section .rodata:
|
||
02000 01000200 00000000 00000000 00000000 ................
|
||
02010 00000000 00000000 00000000 00000000 ................
|
||
02020 25312430 30303338 73253324 686e2531 %1$00038s%3$hn%1
|
||
02030 24363534 39387325 31243238 36373273 $65498s%1$28672s
|
||
02040 25392468 6e002531 24303030 37347325 %9$hn.%1$00074s%
|
||
02050 3324686e 25312436 35343632 73253124 3$hn%1$65462s%1$
|
||
02060 2a382473 25372468 6e002531 24303031 *8$s%7$hn.%1$001
|
||
...
|
||
03400 39732533 24686e25 31243630 34313773 9s%3$hn%1$60417s
|
||
03410 25312435 39333932 73253724 686e0025 %1$59392s%7$hn.%
|
||
03420 31243035 31353373 25332468 6e253124 1$05153s%3$hn%1$
|
||
03430 36303338 33732531 24307325 3624686e 60383s%1$0s%6$hn
|
||
03440 00253124 36353533 34732533 24686e00 .%1$65534s%3$hn.
|
||
03450 00000000 00000000 00000000 00000000 ................
|
||
03460 00000000 00000000 00000000 00000000 ................
|
||
...
|
||
11000 00000000 00000000 00000000 00000000 ................
|
||
11010 00000000 00000000 00000000 00000000 ................
|
||
11020 ccb0e77b bcc0ee3a fc7381d0 7a6984e2 ...{...:.s..zi..
|
||
11030 48e3d759 116bf1b3 860b89c5 bf536565 H..Y.k.......See
|
||
11040 f0ef6abf 0878c42c 99353c6c dce0c899 ..j..x.,.5<l....
|
||
11050 c83bef29 970bb38b cc9dfc05 1b67b5ad .;.).........g..
|
||
11060 15c108d0 45452643 456df4ef bb4906ca ....EE&CEm...I..
|
||
|
||
So we have a bunch of null-terminated format strings packed together,
|
||
then zeros, and then finally some binary data. We'll have fun
|
||
deciphering those format strings later, but for now let's just see how
|
||
this data is used.
|
||
|
||
The next few instructions zero out 80 bytes of memory at
|
||
`-0x90(%rbp)`, and then initialize them. That's the size of 10
|
||
pointers, since we're on x86_64. It is equivalent to
|
||
|
||
void *regs[10] = {mapped, mapped};
|
||
|
||
Remember that this means `regs[2]` through `regs[9]` is initialized to
|
||
zero. We'll see why I'm calling this `regs` later.
|
||
|
||
Next we ask the user for a password. We read the input with the
|
||
equivalent of
|
||
|
||
scanf("%255s", mapped+0xe000);
|
||
|
||
This is the only place this program reads input. We have control over
|
||
255 bytes starting at `mapped+0xe000`.
|
||
|
||
Next we have some jumping around, which we can identify as a loop.
|
||
The body of the loop is a single call to `sprintf`, but it takes a lot
|
||
of code to pass the *25 arguments* that it takes. The equivalent C is
|
||
|
||
while (buf[0] != mapped + 0xfffe) {
|
||
sprintf(0x6000000, // destination
|
||
regs[0], // format string
|
||
M + 0xf14a, // vararg 1, pointer to null character
|
||
0, // vararg 2
|
||
&(regs[0]), // vararg 3
|
||
0x6000000, // vararg 4
|
||
*((short*) regs[1]), // vararg 5
|
||
regs[1], &(regs[1]), // varargs 6, 7
|
||
regs[2], &(regs[2]), // varargs 8, 9
|
||
regs[3], &(regs[3]), // varargs 10, 11
|
||
regs[4], &(regs[4]), // varargs 12, 13
|
||
regs[5], &(regs[5]), // varargs 14, 15
|
||
regs[6], &(regs[6]), // varargs 16, 17
|
||
regs[7], &(regs[7]), // varargs 18, 19
|
||
regs[8], &(regs[8]), // varargs 20, 21
|
||
regs[9], &(regs[9]) // varargs 22, 23
|
||
);
|
||
}
|
||
|
||
Clearly, what exactly this call can do depends on the format string
|
||
pointed to by `regs[0]`. This is initialized to `mapped`, where
|
||
our list of format strings starts. We'll get into this in the next
|
||
section.
|
||
|
||
Last, our program checks if the 16-bit short at `mapped+0xe800` is
|
||
zero. If it is, it quits, and if it's not, it prints the flag from
|
||
memory at `mapped+0xe800`.
|
||
|
||
## `sprintf` is Turing complete
|
||
|
||
From our analysis so far, we haven't seen any direct reference to the
|
||
user input (at `mapped+0xe000`) or the flag (at `mapped+0xe800`), so
|
||
the format strings must be doing all the heavy lifting.
|
||
|
||
We read the strings using `gdb`, by examining the memory at
|
||
`0x4000000`. We find there are 146 strings, and save them to a file:
|
||
|
||
(gdb) b *main+555
|
||
(gdb) set logging on
|
||
(gdb) x/146s 0x4000000
|
||
|
||
Let's take apart the first one:
|
||
|
||
%1$00038s%3$hn%1$65498s%1$28672s%9$hn
|
||
|
||
In order:
|
||
|
||
1. `%1$00038s` writes vararg 1 as a string, and pads it with
|
||
spaces to ensure the length is at least 38. Since this argument is
|
||
an empty string, it just writes 38 spaces.
|
||
2. `%3$hn` doesn't write anything, but it checks the number of
|
||
characters written so far, and writes it in the `short int` pointed
|
||
to by vararg 3. That is, it writes it to the lower two bytes of
|
||
`regs[0]`.
|
||
3. `%1$65498s` writes 65498 spaces, so the total number of spaces so
|
||
far is 65536, or `0x10000`.
|
||
4. `%1$28672s` writes 28672 spaces, or `0x7000` spaces.
|
||
5. `%9$hn` counts the number of characters written so far, and writes
|
||
it to the `short int` pointed to by vararg 9. That is, the lower
|
||
two bytes of `regs[2]`. The number of characters is `0x17000`, but
|
||
since we're only writing two bytes, we're effectively writing
|
||
`0x7000`.
|
||
|
||
At the end of this operation, `regs[0]` is `0x4000026` and `regs[2]`
|
||
is `0x7000`. It just so happens that `0x4000026` is the address of
|
||
the next format string, so on the next iteration of the loop we'll use
|
||
that string instead. Let's call this operation `mov 0x7000, %r2`.
|
||
|
||
So far:
|
||
|
||
- We can write an arbitrary number of spaces with `%1$_____s`.
|
||
- We can store the number of spaces written so far, modulo `0x10000`, to
|
||
`regs[_]` with `%_$hn`.
|
||
- By manipulating `regs[0]`, we can control which format string we use
|
||
next.
|
||
|
||
So really, what's going on is this: We have a virtual machine with 10
|
||
registers, the instructions for this machine are `sprintf` format
|
||
strings, and the first register is the program counter. Lets call the
|
||
registers `%r0, %r1, ..., %r9`. We've allocated 8 bytes for each
|
||
register, but in reality, we only ever write to the lower 2 bytes, and
|
||
the registers we read from always have zeros in all but the lower two
|
||
bytes (we do not read from `%r0` or `%r1`, as we'll see). The
|
||
registers `%r0` and `%r1` are initialized to `0x4000000`, so we can
|
||
use them to reference memory locations `0x4000000` to `0x400ffff`. We
|
||
can think of each of these registers as only being two bytes, with a
|
||
"virtual" address space of `0x0000` to `0xffff`.
|
||
|
||
|
||
At this point, our strategy is to understand what types of
|
||
instructions we'll encounter, and then write a disassembler for this
|
||
language, so we can reason about this embedded program.
|
||
|
||
### Reading from registers
|
||
|
||
The next format string shows us a new trick:
|
||
|
||
%1$00074s%3$hn%1$65462s%1$*8$s%7$hn
|
||
|
||
Again, `%1$00074s%3$hn` sets the program counter to the next
|
||
instruction, and `%1$65462s` ensures the number of spaces written so
|
||
far is 0 modulo `0x10000`. `%1$*8$s` is new. This writes a number of
|
||
spaces depending on vararg 8, which is `regs[2]`. Then it saves this
|
||
number to the `short int` pointed to by vararg 7, which is `regs[1]`.
|
||
Effectively, this is `mov %r2, %r1`.
|
||
|
||
### Reading from memory
|
||
|
||
The 5th variadic argument to sprintf is `*((short*) regs[1])`. That
|
||
is, it interprets `regs[1]` as a pointer to a short, and dereferences
|
||
it. Since `regs[1]` was initialized to `0x4000000` and we can modify
|
||
the lower two bytes of it, this allows us to read from memory
|
||
locations `0x4000000` to `0x400ffff`. To do this, we use `%1$*5$s`,
|
||
like in this example:
|
||
|
||
%1$00347s%3$hn%1$65189s%1$*5$s%15$hn
|
||
|
||
We'll write this as `mov (%r1), %r5`.
|
||
|
||
### Addition
|
||
|
||
By concatenating strings of spaces, we can perform addition. For
|
||
example,
|
||
|
||
%1$00149s%3$hn%1$65387s%1$*8$s%1$2s%7$hn
|
||
|
||
This prints a number of spaces based on `%r2`, then prints two more
|
||
spaces, and saves the total count in `%r1`. We'll call this `add %r2,
|
||
0x2, %r1`.
|
||
|
||
We can also add two registers, like
|
||
|
||
%1$00264s%3$hn%1$65272s%1$*10$s%1$*10$s%17$hn
|
||
|
||
This is `add %r3, %r3, %r6`.
|
||
|
||
### Unconditional jumping
|
||
|
||
To do an unconditional jump, all we have to do is set the program
|
||
counter, `%r0`, to the desired value. For example,
|
||
|
||
%1$00430s%3$hn
|
||
|
||
This jumps to the instruction at `0x4000000+430`, or `0x40001ae`.
|
||
We'll write this as `jmp 01ae`.
|
||
|
||
### Branching
|
||
|
||
The most complex type of instruction is branching. Here's an example
|
||
of one:
|
||
|
||
%14$c%1$00419s%2$c%4$s%1$65499s%3$hn
|
||
|
||
Breaking this down:
|
||
|
||
1. `%14$c` writes the lowest byte of `%r5`.
|
||
2. `%1$00419s` writes 419 spaces.
|
||
3. `%2$c` writes a null byte.
|
||
4. `%4$s` writes the string at `0x6000000`, i.e. the string we're in
|
||
the middle of writing to. If the lower byte of `%r5` is zero, this
|
||
writes nothing, but if it's nonzero, this writes 420 bytes.
|
||
5. `%1$65499s` writes 65499 bytes.
|
||
6. `%3$hn` stores the number of bytes written in the program counter.
|
||
|
||
So if the lower byte of `%r5` is zero, we jump to
|
||
`1+419+1+65499=0x10180`, or really `0x0180` since we only keep the
|
||
lower two bytes. If the lower byte is nonzero, we jump to
|
||
`1+419+1+420+65499=0x10324`, or really `0x0324`. It happens that the
|
||
instruction after this one is at `0x180`, so it would make sense to
|
||
call this instruction `jnz %r5, 0324`.
|
||
|
||
### Writing a disassebler
|
||
|
||
Taking our output from gdb from before and running it through `sed`
|
||
gives us a file with a format string on each line:
|
||
|
||
$ sed 's/.*"\(.*\)"/\1/' gdb.txt > format_strings.txt
|
||
|
||
Now we write a python script to parse each of the types of
|
||
instructions we've seen above, and translate them to human-readable
|
||
form. We can also check if some of the assumptions we've made hold
|
||
and print an error message if they don't. The code below is
|
||
abbreviated, you can find the full code [here](disassemble.py).
|
||
|
||
#!/usr/bin/env python3
|
||
|
||
import fileinput
|
||
import re
|
||
|
||
literalMove = re.compile(r"^%1\$(\d*)s%3\$hn%1\$(\d*)s%1\$(\d*)s%(\d*)\$hn$")
|
||
# ... more for each type of instruction
|
||
|
||
def showRegisterDest(regRef):
|
||
if regRef == 6:
|
||
regStr = "(%r1)"
|
||
elif regRef >= 7 and regRef <= 23 and regRef % 2 == 1:
|
||
regStr = "%r{}".format(regRef // 2 - 2)
|
||
else:
|
||
print("bad destination: {}".format(regRef))
|
||
exit()
|
||
return regStr
|
||
|
||
|
||
addr = 0
|
||
|
||
for line in fileinput.input():
|
||
oldAddr = addr
|
||
addr += len(line)
|
||
m = literalMove.match(line)
|
||
if m:
|
||
nextAddr = int(m.group(1))
|
||
filler = int(m.group(2))
|
||
literal = int(m.group(3))
|
||
regRef = int(m.group(4))
|
||
|
||
if addr != nextAddr:
|
||
print("address is not the next instruction")
|
||
exit()
|
||
if nextAddr + filler != 0x10000:
|
||
print("first two numbers do not add to 0x10000")
|
||
exit()
|
||
|
||
dest = showRegisterDest(regRef)
|
||
print("{:04x}: mov 0x{:x}, {}".format(oldAddr, literal, dest))
|
||
continue
|
||
|
||
# ... more for each other type of instruction
|
||
|
||
print("unknown instruction")
|
||
exit()
|
||
|
||
Now we can see the output:
|
||
|
||
$ ./disassemble.py < format_strings.txt > disassembly.txt
|
||
$ cat disassembly.txt
|
||
0000: mov 0x7000, %r2
|
||
0026: mov %r2, %r1
|
||
004a: mov 0x1, (%r1)
|
||
006c: add %r2, 0x2, %r1
|
||
0095: mov 0x1, (%r1)
|
||
00b7: mov 0x2, %r3
|
||
00da: add %r3, %r3, %r6
|
||
0108: add %r6, 0x7000, %r1
|
||
0136: mov (%r1), %r5
|
||
015b: jnz %r5, 0324
|
||
...
|
||
|
||
## Picking apart the embedded code
|
||
|
||
### Using `gdb`
|
||
|
||
Now we have yet another chunk of assembly to reverse. It will be
|
||
useful to be able to use `gdb` to step through this code, so let's
|
||
understand how to do that. The call to `sprintf` is at `main+555`, so
|
||
by putting a breakpoint there, we can step through the embedded code
|
||
one instruction at a time. To view the 10 registers, we can examine
|
||
the memory at `0x90(%rbp)`.
|
||
|
||
Temporary breakpoint 1, 0x0000555555555189 in main ()
|
||
(gdb) b *main+555
|
||
Breakpoint 2 at 0x5555555553b0
|
||
(gdb) c
|
||
Continuing.
|
||
Input password:
|
||
foobar
|
||
|
||
Breakpoint 2, 0x00005555555553b0 in main ()
|
||
(gdb) x/10gx $rbp-0x90
|
||
0x7fffffffdde0: 0x0000000004000000 0x0000000004000000
|
||
0x7fffffffddf0: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde10: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde20: 0x0000000000000000 0x0000000000000000
|
||
(gdb) c
|
||
Continuing.
|
||
|
||
Breakpoint 2, 0x00005555555553b0 in main ()
|
||
(gdb) x/10gx $rbp-0x90
|
||
0x7fffffffdde0: 0x0000000004000026 0x0000000004000000
|
||
0x7fffffffddf0: 0x0000000000007000 0x0000000000000000
|
||
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde10: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde20: 0x0000000000000000 0x0000000000000000
|
||
|
||
Instead of single-stepping, we can also set breakpoints in the
|
||
embedded code, by adding conditional breakpoints in `gdb`. Before the
|
||
call to `sprintf`, the program counter is passed in `%rsi`:
|
||
|
||
(gdb) clear *main+555
|
||
Deleted breakpoint 2
|
||
(gdb) b *main+555 if $rsi == 0x400015b
|
||
Breakpoint 3 at 0x5555555553b0
|
||
(gdb) c
|
||
Continuing.
|
||
|
||
Breakpoint 3, 0x00005555555553b0 in main ()
|
||
(gdb) x/10gx $rbp-0x90
|
||
0x7fffffffdde0: 0x000000000400015b 0x0000000004007004
|
||
0x7fffffffddf0: 0x0000000000007000 0x0000000000000002
|
||
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
|
||
0x7fffffffde10: 0x0000000000000004 0x0000000000000000
|
||
0x7fffffffde20: 0x0000000000000000 0x0000000000000000
|
||
|
||
At pretty much every step below, I was checking in `gdb` as I went,
|
||
but I'll leave that out for brevity.
|
||
|
||
### Checking the input length
|
||
|
||
We'll jump straight to the first place where we read the user input.
|
||
Instead of picking apart the code before that, we can just use `gdb`
|
||
to examine the state of the program at this point. Since the earlier
|
||
code doesn't reference the user input, the state here will always be
|
||
the same.
|
||
|
||
0374: mov 0xe000, %r2 # Address of user input
|
||
039a: mov 0x0, %r3 # Counter, initialized to 0
|
||
|
||
03bd: mov %r2, %r1
|
||
03e1: mov (%r1), %r4 # Read a char from user input
|
||
0406: jnz %r4, 043a # If it's nonzero, skip to 043a
|
||
042b: jmp 04a1 # If it's zero, stop looping
|
||
|
||
043a: add %r3, 0xffff, %r3 # Decrement %r3
|
||
0469: add %r2, 0x1, %r2 # Increment %r2
|
||
0492: jmp 03bd # Repeat the loop
|
||
|
||
At the end of this loop, `%r3` contains the number of characters in
|
||
the user input, but negated.
|
||
|
||
04a1: add %r3, 0xfe, %r6
|
||
04d0: jnz %r6, 0504 # jump to 0504 if %r3+0xfe != 0
|
||
04f5: jmp 0536 # jump to 0536 if %r3+0xfe == 0
|
||
0504: mov 0x5, %r9
|
||
0527: jmp 13d9 # jump to 139d if %r3+0xfe != 0
|
||
|
||
This code checks if the length of the user input is `0xfe`, or 254.
|
||
If it's not, it jumps down to `139d`, which writes zero to the flag
|
||
and halts:
|
||
|
||
13d9: mov 0xe800, %r1
|
||
13ff: mov 0x0, (%r1)
|
||
1421: halt
|
||
|
||
Thus our input needs to be exactly 254 characters long.
|
||
|
||
### Sprinting
|
||
|
||
Next we load some data into registers
|
||
|
||
0536: mov 0x0, %r2
|
||
0558: mov 0x0, %r3
|
||
057b: mov 0xf100, %r1
|
||
05a1: mov (%r1), %r4 # Read from 0xf100, value = 0x11
|
||
05c6: mov 0x1, %r5
|
||
05e9: mov 0x0, %r9
|
||
|
||
We have another chunk of code the jumps around in a pattern that looks
|
||
like a loop. The loop starts with this:
|
||
|
||
060c: add %r2, 0xe000, %r1 # %r2 is index into user input
|
||
0639: mov (%r1), %r6 # read two bytes into %r6
|
||
065e: jnz %r6, 0692 # if lower byte is zero, break
|
||
0683: jmp 0d97 # 0d97 is the end of the loop
|
||
0692: add %r2, 0x1, %r # increment %r2
|
||
|
||
So we're looping over the user input. The byte we just read is in the
|
||
lower half of `%r6`. Now we break into cases depending on this byte.
|
||
|
||
06bb: add %r6, 0xff8b, %r7
|
||
06ea: jnz %r7, 0745 # jump to next block if input != 0x75 ('u')
|
||
070f: mov 0xfff0, %r6 # %r6 = 0xfff0 (-16)
|
||
0736: jmp 0945 # jump to end of section
|
||
|
||
0745: add %r6, 0xff8e, %r7
|
||
0774: jnz %r7, 07cb # jump to next block if input != 0x72 ('r')
|
||
0799: mov 0x1, %r6 # %r6 = 0x1 (1)
|
||
07bc: jmp 0945 # jump to end of section
|
||
|
||
07cb: add %r6, 0xff9c, %r7
|
||
07fa: jnz %r7, 0852 # jump to next block if input != 0x64 ('d')
|
||
081f: mov 0x10, %r6 # %r6 = 0x10 (16)
|
||
0843: jmp 0945 # jump to end of section
|
||
|
||
0852: add %r6, 0xff94, %r7
|
||
0881: jnz %r7, 08dc # jump to next block if input != 0x6c ('l')
|
||
08a6: mov 0xffff, %r6 # %r6 = 0xffff (-1)
|
||
08cd: jmp 0945 # jump to end of section
|
||
|
||
# This executes if input was none of {u,r,d,l}
|
||
08dc: mov 0x0, %r5
|
||
08ff: mov 0x0, %r6
|
||
0922: mov 0x1, %r9
|
||
|
||
At this point, the characters 'u','r','d','l' probably stand for "up",
|
||
"right", "down", and "left", so we're moving something around on a
|
||
grid. It looks like `%r6` holds an offset to move. Data must be
|
||
stored in rows of length 16, since we add or subtract 16 to move up or
|
||
down. If we don't enter a valid direction, we don't move at all, and
|
||
`%r5` and `%r9` are updated to remember that.
|
||
|
||
From the next block of code, it looks like `%r4` stores a position on
|
||
the grid. The code writes and then reads memory at adjacent locations
|
||
to get just the upper byte of the position.
|
||
|
||
0945: add %r4, %r6, %r4 # Update the position
|
||
|
||
0973: mov 0xffef, %r1
|
||
0999: mov %r4, (%r1)
|
||
09be: mov 0xfff0, %r1
|
||
09e4: mov (%r1), %r6
|
||
|
||
# At this point, the lower byte of %r6 is the upper byte of %r4
|
||
# So we jump if the upper byte of the position is nonzero
|
||
0a09: jnz %r6, 0d65
|
||
|
||
# ... snip ...
|
||
|
||
0d65: mov 0x4, %r9
|
||
0d88: halt
|
||
|
||
Since we haven't written anything to the flag yet, halting here means
|
||
we lose, so we had better keep the position between 0 and 255. That
|
||
means we're on a 16×16 grid.
|
||
|
||
Now, as expected, we're using `%r4` as an index into some memory,
|
||
which we can think of as a map.
|
||
|
||
0a2e: add %r4, 0xf000, %r1 # %r4 is index into mem starting at 0xf000
|
||
0a5c: mov (%r1), %r6 # %r6 is two bytes read from that location
|
||
|
||
# This snippet just zeros out the high byte of %r6
|
||
0a81: mov 0xffef, %r1
|
||
0aa7: mov %r6, (%r1)
|
||
0acc: mov 0xfff0, %r1
|
||
0af2: mov 0x0, (%r1)
|
||
0b14: mov 0xffef, %r1
|
||
0b3a: mov (%r1), %r6
|
||
|
||
# Now %r6 holds the byte at 0xf000(%r4)
|
||
# Use 2 * %r6 as an index into memory at 0x7000
|
||
0b5f: add %r6, %r6, %r6
|
||
0b8d: add %r6, 0x7000, %r1
|
||
0bbb: mov (%r1), %r6 # Read from 0x7000+2*%r6
|
||
0be0: jnz %r6, 0d10 # Jump below if it's nonzero
|
||
|
||
# ... snip ...
|
||
|
||
0d10: mov 0x0, %r5
|
||
0d33: mov 0x2, %r9
|
||
0d56: jmp 060c # Continue from start of loop
|
||
|
||
From this, we see that there is an array of 256 bytes at `0xf000` that
|
||
we index by our position. Each of these is in turn an index into an
|
||
array of up to 256 16-bit integers at `0x7000`. If the lower byte of
|
||
the number we read is nonzero, we record it by setting `%r5` and
|
||
`%r9`, and jump to the next iteration. Since we never modify either
|
||
of these regions over the course of this loop, this ends up being a
|
||
round-about way of storing a 16×16 grid of booleans. Let's call this
|
||
grid the "terrain".
|
||
|
||
We use the position one more time in the loop:
|
||
|
||
0c05: add %r3, 0x1, %r6
|
||
0c30: add %r6, 0xf102, %r1
|
||
0c5e: mov (%r1), %r6 # read %r6 <- 0xf103(%r3)
|
||
|
||
0c83: add %r6, %r4, %r6
|
||
0cb1: jnz %r6, 0d01 # check if pos+%r6==0
|
||
0cd6: add %r3, 0x1, %r3 # if true, increment %r3
|
||
0d01: jmp 060c # continue
|
||
|
||
So we have an array of bytes starting at `0xf103`. We start out
|
||
reading the first one, and read the others as `%r3` gets incremented.
|
||
These are the negations of positions on the map. Once we move to this
|
||
position, we increment `%r3`. Call these positions "checkpoints".
|
||
|
||
Now we're done with the loop. Let's see how the data we set in `%r3`,
|
||
`%r5`, and `%r9` are used
|
||
|
||
# If %r5 == 0, you lose!
|
||
0d97: jnz %r5, 0dcb
|
||
0dbc: jmp 13d9 # zeros flag and halts
|
||
|
||
# If %r3 != 9, you lose!
|
||
0dcb: add %r3, 0xfff7, %r6
|
||
0dfa: jnz %r6, 0e2e
|
||
0e1f: jmp 0e60 # jump to end of section
|
||
|
||
0e2e: mov 0x3, %r9
|
||
0e51: jmp 13d9 # zeros flag and halts
|
||
|
||
So we see that setting `%r5` to zero means we lose. That means we
|
||
can't ever enter an invalid direction, and we can't ever step on
|
||
nonzero terrain. We also see that we need to step on exactly nine
|
||
checkpoints.
|
||
|
||
The register `%r9` appears to hold a reason code for why we lose.
|
||
This could be useful for debugging later.
|
||
|
||
There's still more code after, but for now our task is clear: we need
|
||
to map out the terrain and checkpoints, and plan our route.
|
||
|
||
### Mapping it out
|
||
|
||
Since the code at the very beginning appears to mess with memory at
|
||
`0x7000`, we can't just pull the terrain from the program binary.
|
||
Instead, we should run our debugger until it's done messing with
|
||
memory, but before it starts checking any input, and then dump the
|
||
memory from the right locations. In `gdb`:
|
||
|
||
(gdb) b *main+555 if $rsi == 0x4000374
|
||
(gdb) set logging on
|
||
(gdb) x/256bu 0x400f000
|
||
(gdb) x/256hu 0x4007000
|
||
(gdb) x/9bx 0x400f103
|
||
|
||
Now we manipulate this data into a few files, cleaning it up. First
|
||
we have the indices at `0xf000`:
|
||
|
||
204 176 231 123 188 192 238 58
|
||
252 115 129 208 122 105 132 226
|
||
72 227 215 89 17 107 241 179
|
||
134 11 137 197 191 83 101 101
|
||
...
|
||
|
||
Then the values at `0x7000`:
|
||
|
||
1 1 0 0 1 0 1 0
|
||
1 1 1 0 1 0 1 1
|
||
1 0 1 0 1 1 1 0
|
||
1 1 1 1 1 0 1 0
|
||
...
|
||
|
||
And finally the negated positions of the checkpoints:
|
||
|
||
83 01 af 49 ad c1 0f 8b e1
|
||
|
||
Now we run a script to draw our map:
|
||
|
||
with open("values.txt") as f:
|
||
valuesStr = f.read()
|
||
values = [int(s) for s in valuesStr.split()]
|
||
|
||
with open("indices.txt") as f:
|
||
indicesStr = f.read()
|
||
indices = [int(s) for s in indicesStr.split()]
|
||
|
||
with open("checkpoints.txt") as f:
|
||
checkpointStr = f.read()
|
||
checkpoints = [0x100 - int(s, base=16) for s in checkpointStr.split()]
|
||
|
||
for col in range(16):
|
||
print(" " + hex(col), end="")
|
||
print()
|
||
|
||
for row in range(16):
|
||
print(hex(row), end="")
|
||
for col in range(16):
|
||
pos = row * 16 + col
|
||
if pos in checkpoints:
|
||
print(" {} ".format(checkpoints.index(pos)+1), end="")
|
||
elif values[indices[pos]] == 1:
|
||
print(" # ", end="")
|
||
else:
|
||
print(" - ", end="")
|
||
print()
|
||
|
||
And admire the output:
|
||
|
||
0 1 2 3 4 5 6 7 8 9 a b c d e f
|
||
0 # # # # # # # # # # # # # # # #
|
||
1 # - # - - - - - # - - - - - - 9
|
||
2 # - # - # # # # # - # # # # # #
|
||
3 # - - - - - - - # - # - # - - 6
|
||
4 # - # # # # # - # - # - # - # #
|
||
5 # 3 # 5 # - - - - - - - - - - -
|
||
6 # # # - # # # - # # # # # # # -
|
||
7 # - - - # 8 - - # - - - # 1 - -
|
||
8 # - # # # # # - # # # - # # # #
|
||
9 # - - - - - - - # - # - - - # -
|
||
a # - # - # # # # # - # - # # # -
|
||
b # - # - # - # 4 # - - - - - - -
|
||
c # - # # # - # - # - # - # # # #
|
||
d # - - - - - - - - - # - - - - -
|
||
e # # # - # - # # # # # - # - # #
|
||
f # 7 - - # - # - - - - - # - - 2
|
||
|
||
We start at position 0x11, i.e. (1, 1). Now we just need to plot a
|
||
path that hits all the checkpoints in order. It turns out such a path
|
||
is exactly 254 steps long. Save it in a file called `directions`.
|
||
|
||
$ cat <(fold directions) <(echo)
|
||
ddrrrrrrddrrrrrrrrddllrruullllllllddddllllllddddrrrrrrrruurrddrrddrrlluulluulldd
|
||
lllllllluuuurrrrrruuuuuulllllldduurrrrrrddddddllllllddddrrrrrruuddlllllluuuuuurr
|
||
uuddllddrrrrrruuuurrrrrruurrllddllllllddddllllllddddrrddllrruulluuuurrrrrruullrr
|
||
uurruuuurrrrrr
|
||
$ ./sprint < directions
|
||
Input password:
|
||
Flag: CTF{n0w_ev3n_pr1n7f_1s_7ur1ng_c0mpl3te}
|
||
|
||
Success!
|