OverTheWire Advent Bonanza 2019 - Challenge Zero

In this challenge, we have to reverse engineer a real-mode, self-modifying, bootloader which uses AES-NI instructions to decrypt itself at runtime.

Finding the bootloader binary

Here is the challenge description:

Visiting the Service’s URL with Firefox gives us the following:

The Did you know: Plain text goes best with a text browser. message tells us that a regular browser is not the best option to view the site, so we use curl instead:

In the flames ascii-art animation, there seems to be some base64 text between all the # characters.
Copying and pasting the text and removing the # while piecing it together, gives us the following valid base64 string:

1
2
3
4
5
6
7
8
9
10
11
12
13
YmVnaW4gNjQ0IGJvb3QuYmluCk1eQydgQ01CLlAoWzBPYCFcMGBeQiNSI2BAXiNbQFxAIiNSK2Aj
UiNAIzBgJiNSK0BPTzlcWitYYDlAXloKTVgxRVMzO1g4Pz5CUWArXGA/QydgUzE4XCM3MDovYEFV
I1gnX2AnWV5bS1kmPz5CNmAkX0tZOkpUI0xUMApNWl1aIV9RIV49PFwvKmA7UD8wXEgnQCFeWiMw
YDlAX08nTiFdOUBcWCVdTVQiTk5TT0I1XVomMGBaX1peCk00J1QvKmA4YD9AXEgnLkAvYGBcSSco
LyYkKCdeWCdVVVo+Rk1gJjgvW10pRiNeXzhOXjRWTScvIVpgP1YKTVxYQEZPR1FGI1NLP1IkNUYj
VyMpX1BfJlQhIUYjXl8iQCM7Jz8pUVhcNjgvW1wkWF8nMCc5QFxYVy1DSwpNU0Y4Ly4tVzhQWzAu
SyMxIj1gOFFWXFQwWl8vIzNUQDUpVihXLEI0UChSOEcpRihELCJUTzhBYCE9RihWCk0qQkxROENM
RyhTIUMwRF0oJEIsUSwzNE0sIjlYOEQpLzJgJE0rUj1CKCIsQSo2KFUqUzhKOEItQitSVEYKTSlT
YEw4QCQyJVYpWCREKSo4REkiRCkiMEQpIjBUKC9LQlFeIU9aLFAsITE5RVlIVSQhUSIwXjBeKz84
PQpNTEdZWicsS18iND4nK05fVUsmPUEtRjsqJUNcJVw9PSgvM0tUYFosMlo5SCMiIichITheUiRA
NztdQi1YCk0sIUlCIV4wR15cIktWSkE7QjNMWltVV0gqWiZOW1wxQz5CST4hKjpdPEsvSCUrMywi
O1tCQD47RTcySVYKTT4kWiU/KlE0YEZfUSdCIktEV1guMkJeWUBaOEYtMiQ4LzBNRFYnLl1dX1g6
QCM5MS4pIkAtISVNLCVZMgoxNSZVWUArRkUiSTxEIzJXXC1AVjU1OkhgCmAKZW5kCg==

Which decodes to the following uuencoded text:

boot.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
begin 644 boot.bin
M^C'`CMB.P([0O`!\0`^B#R#`@^#[@\@"#R+`#R#@#0`&#R+@OO9\Z+X`9@^Z
MX1ES3;X8?>BQ`+\`?C'`S18\#70:/`AU#X'_`'Y^[KY&?>B6`$_KY:JT#LT0
MZ]Z!_Q!^=<\/*`;P?0\H'@!^Z#0`9@_O'N!]9@\X%]MT"NNSOB5]Z&0`Z_Z^
M4'T/*`8`?@\H'.@/``\I'(/&$('^X'UUZ>FM`&8/[])F#^_8N^4VM'/!Z`?V
M\X@FOGQF#SK?R$5F#W#)_P_&T!!F#^_"@#;'?)QX\68/[\$X_'0'9@\XW-CK
MSF8/.-W8P[0.K#1"=`8QV\T0Z_/#3T@5)V(W,B4P(R8G)F(D,"TO8A`!=F(V
M*BLQ8CLG(S!C0D]($B,Q,34M,"9X8D)/2`$M+R=B(",A*6(U*S8J8B-B+RTF
M)S`L8@$2%V)X$D)*8DI"D)"0D)"0T(/KBQ^!OZ,P,!19EYHU$!Q"0^0^+?8=
MLGYZ',K_"4>'+N_UK&=A-F;*%C\%\==(/3KT`Z,2Z9H#""'!!8^R$@7;]B-X
M,!IB!^0G^\"KVJA;B3LZ[UWH*Z&N[\1C>BI>!*:]<K/H%+3,";[B@>;E72IV
M>$Z%?*Q4`F_Q'B"KDWX.2B^Y@Z8F-2$8/0MDV'.]]_X:@#91.)"@-!%M,%Y2
15&UY@+FE"I<D#2W\-@V55:H`
`
end

We decode it using the uudecode linux command, and get the bootloader binary:

1
2
3
$ cat boot.txt | uudecode -o boot.bin
$ file boot.bin
boot.bin: DOS/MBR boot sector

File: boot.bin

Static Analysis Environment Setup

We will use Ghidra to statically reverse engineer the bootloader:

  • Create a new non-shard project in Ghidra
  • Import the boot.bin file, and select the following options
    • Select Read Mode for the Language setting in the Import window:
    • The final Language configuration should look like the following:
  • Double click the boot.bin to open the Ghidra disassembler, and analyze it.
  • Since Ghidra is loading the file in Raw mode, it will not perform the disassembly automatically.
    This can be done manually by right-clicking the first line of the program and selecting Disassemble:

After the Disassemble operation is done, we can see that some parts of the program are still encoded, and there are no human-readable strings in the binary.
Since this binary is small, we are not going to spend time trying to fix the decompiler output from Ghidra, and will mostly read the disassembler ouput.
Next, we setup the dynamic analysis environment using gdb and QEMU.

Dynamic Analysis Environment Setup

First, we install QEMU with the following command under Ubuntu 18.04:

  • sudo apt install qemu-system-x86

When we try to run the boot.bin under QEMU, it gives us the following message:

  • $ qemu-system-i386 boot.bin

This means the binary is using instructions not supported by the CPU which QEMU selected.
Taking a quick look in Ghidra and we can see AES-NI instructions being used:

Reading the AES-NI Wikipedia page, we can see which Intel CPUs support these instructions.
Then, we tell QEMU to use the SandyBridge CPU microarchitecture, and successfully run boot.bin:

1
$ qemu-system-i386 -cpu SandyBridge -fda boot.bin -s -S


Here is what the other flags mean:

  • -s spawn a local GDB server listening on port 1234
  • -S start the program in stopped mode, only run when a GDB client issues a continue command
  • -fda boot.bin load the file boot.bin from disk

Once QEMU is running in stopped mode, and waiting for a GDB client, we connect to it as follows:

1
2
$ gdb
(gdb) target remote localhost:1234

However, gdb by itself is not very good at debugging real mode code.
This guide provides a very neat gdb script which help us with real mode debugging: gdb_init_real_mode.txt.

To make things easier, we create a file gdb_cmds.txt and use it to store commands we want to run everytime we launch gdb:

gdb_cmds.txt
1
2
3
4
source ./gdb_init_real_mode.txt
target remote localhost:1234
hbreak *0x7c00
continue

Note: The hbreak *0x7c00 sets a hardware breakpoint at the start of the bootloader code. (More info)
Use the following command to tell gdb to load this file:

1
$ gdb -x ./gdb_cmds.txt

Static & Dynamic Analysis Workflow Summary

Now that Ghidra, QEMU, and GDB are configured, we can begin reverse engineering the bootloader.
This is a high-level summary of the workflow used:

  • Use Ghidra as a reference for certain code sections we want to inspect, and to read the disassembly.
    • We know that the bootloader is loaded at 0x7c00, however I could not find a way to get Ghidra to set the base address correctly.
    • Instead, Ghidra loads the bootloader code at 0x0000, so just add 0x7c when going from GDB <-> Ghidra
  • Use GDB and QEMU to inspect values at runtime as follows:
    • Start the program in stopped mode with QEMU: qemu-system-i386 -cpu SandyBridge -fda boot.bin -s -S
    • Run GDB and connect to the gdbserver instance created by QEMU: gdb -x ./gdb_cmds.txt
      • At this point, GDB will break at the start of the bootloader code in 0x7c00
    • Repeat the two steps above as needed

Reverse Engineering


The following is the annotated Ghidra disassembly for the main parts of the program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
START:
0000:0000 fa CLI
0000:0001 31 c0 XOR AX,AX
0000:0003 8e d8 MOV DS,AX
0000:0005 8e c0 MOV ES,AX
0000:0007 8e d0 MOV SS,AX
0000:0009 bc 00 7c MOV SP,0x7c00
0000:000c 40 INC AX
0000:000d 0f a2 CPUID
0000:000f 0f 20 c0 MOV EAX,CR0
0000:0012 83 e0 fb AND AX,0xfffb
0000:0015 83 c8 02 OR AX,0x2
0000:0018 0f 22 c0 MOV CR0,EAX
0000:001b 0f 20 e0 MOV EAX,CR4
0000:001e 0d 00 06 OR AX,0x600
0000:0021 0f 22 e0 MOV CR4,EAX
0000:0024 be f6 7c MOV SI,0x7cf6
0000:0027 e8 be 00 CALL DECODE_STR
0000:002a 66 0f ba BT ECX,0x19
0000:002f 73 4d JNC BAD_CPU_MSG

PASSWORD_PROMPT:
0000:0031 be 18 7d MOV SI,0x7d18
0000:0034 e8 b1 00 CALL DECODE_STR
0000:0037 bf 00 7e MOV DI,0x7e00

PASSWORD_PROMPT_LOOP:
0000:003a 31 c0 XOR AX,AX
0000:003c cd 16 INT 0x16
0000:003e 3c 0d CMP AL,0xd // ENTER key is pressed
0000:0040 74 1a JZ CHECK_PASSWD
0000:0042 3c 08 CMP AL,0x8 // BACKSPACE key is pressed
0000:0044 75 0f JNZ HANDLE_BACKSPACE
0000:0046 81 ff 00 7e CMP DI,0x7e00
0000:004a 7e ee JLE PASSWORD_PROMPT_LOOP
0000:004c be 46 7d MOV SI,0x7d46
0000:004f e8 96 00 CALL DECODE_STR
0000:0052 4f DEC DI
0000:0053 eb e5 JMP PASSWORD_PROMPT_LOOP

HANDLE_BACKSPACE:
0000:0055 aa STOSB ES:DI=>USER_INPUT
0000:0056 b4 0e MOV AH,0xe
0000:0058 cd 10 INT 0x10
0000:005a eb de JMP PASSWORD_PROMPT_LOOP

CHECK_PASSWD:
0000:005c 81 ff 10 7e CMP DI,0x7e10 // Password len is 16
0000:0060 75 cf JNZ PASSWORD_PROMPT
0000:0062 0f 28 06 MOVAPS XMM0,xmmword ptr [ENC_IV]
0000:0067 0f 28 1e MOVAPS XMM3,xmmword ptr [USER_INPUT]
0000:006c e8 34 00 CALL ENCRYPT
0000:006f 66 0f ef PXOR XMM3,xmmword ptr [EXPECTED_CIPHERTEXT]
0000:0075 66 0f 38 PTEST XMM3,XMM3
0000:007a 74 0a JZ UNPACK_FLAG_CODE
0000:007c eb b3 JMP PASSWORD_PROMPT

BAD_CPU_MSG:
0000:007e be 25 7d MOV SI,0x7d25
0000:0081 e8 64 00 CALL DECODE_STR
EXIT_NOT_IMPORTANT
0000:0084 eb fe JMP EXIT_NOT_IMPORTANT

UNPACK_FLAG_CODE:
0000:0086 be 50 7d MOV SI,0x7d50

UNPACK_FLAG_CODE_LOOP:
0000:0089 0f 28 06 MOVAPS XMM0,xmmword ptr [USER_INPUT]
0000:008e 0f 28 1c MOVAPS XMM3,xmmword ptr [SI]=>PRINT_FLAG
0000:0091 e8 0f 00 CALL ENCRYPT
0000:0094 0f 29 1c MOVAPS xmmword ptr [SI]=>PRINT_FLAG,XMM3
0000:0097 83 c6 10 ADD SI,0x10
0000:009a 81 fe e0 7d CMP SI,0x7de0
0000:009e 75 e9 JNZ UNPACK_FLAG_CODE_LOOP
0000:00a0 e9 ad 00 JMP PRINT_FLAG


This roughly translates to the following pseudo-code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if not check_cpu():
print BAD_CPU_MSG
exit(0)

while True:
USER_INPUT = raw_input("Password: ")
if len(USER_INPUT) == 16:
ENC_IV = [...] # stored in the program
result = ENCRYPT(ENC_IV, USER_INPUT)
if result == EXPECTED_CIPHERTEXT:
break

# unpack code that prints the flag
PRINT_FLAG_CODE = [...] # stored in program at 0x7d50
while True:
PRINT_FLAG_CODE = ENCRYPT(USER_INPUT, PRINT_FLAG_CODE)
*PRINT_FLAG_CODE += 0x10
if PRINT_FLAG_CODE == 0x7de0:
PRINT_FLAG_CODE() # print the flag when done decrypting
break

From the pseudocode, we can see that there two values which we need to extract from the program:

  • ENC_IV initial value passed to the ENCRYPT() function
  • EXPECTED_CIPHERTEXT value which the result of the ENCRYPT() call must match in order for the flag to be printed

Once we have the ENC_IV and the EXPECTED_CIPHERTEXT, we can perform the reverse of the operations within ENCRYPT(), and get the original USER_INPUT.

Lets take a closer look at the relevant parts of the Ghidra decompiler output for the ENCRYPT() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __cdecl16near ENCRYPT(void)
{
// ..
auVar7 = in_XMM3 ^ in_XMM0;
// ..
while( true )
{
// ..
do
{
// ..
} while (/*..*/);
// ..
in_XMM0 = in_XMM0 ^ (/* some value that was calulated in the loop above */)
// ..
auVar7 = aesenc(auVar7,in_XMM0);
}
aesenclast(auVar7,in_XMM0);
return;
}

Looking at Line 50 of the Ghidra disassembly listing:

  • in_XMM0 corresponds to the ENC_IV param
  • in_XMM3 corresponds to the USER_INPUT param

However, after Line 4 of the decompiler listing:

  • in_XMM0 is used to store the Round Key param for the aesenc call

Decrypting USER_INPUT: Dumping memory with GDB

In order to successfully decrypt USER_INPUT, we must do the following (pseudocode):

Decrypt
1
2
3
4
5
6
7
8
9
ROUND_KEYS = [...] # contains all the values of in_XMM0 passed to the aesenc() call
EXPECTED_CIPHERTEXT = [...] # stored in the program
ENC_IV = [...] # stored in the program

EXPECTED_CIPHERTEXT = AESDECLAST(EXPECTED_CIPHERTEXT, ROUND_KEYS[-1]) # round key used in the aesenclast() call
for rk in ROUND_KEYS[:-1][::-1]: # go through ROUND_KEYS in reverse order
EXPECTED_CIPHERTEXT = AESDEC(EXPECTED_CIPHERTEXT, rk)

return EXPECTED_CIPHERTEXT ^ ENC_IV

The pseudocode above is missing the following values:

  • EXPECTED_CIPHERTEXT
  • ENC_IV
  • ROUND_KEYS

I wrote a gdb script which sets breakpoints where those values are used, and prints the contents of the XMM register containing the value.
The script logs all ouput to a dump.txt file.

gdb_cmds.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#source ./gdb_init_real_mode.txt
target remote localhost:1234

set logging file dump.txt
set logging on

hbreak *0x7ca7
command
silent
printf "ENC_IV:\n"
p/x $xmm0.v16_int8
continue
end

hbreak *0x7cdb
command
silent
printf "AESENC ROUND_KEY:\n"
p/x $xmm0.v16_int8
continue
end

hbreak *0x7ce2
command
silent
printf "AESENCLAST ROUND_KEY:\n"
p/x $xmm0.v16_int8
continue
end

hbreak *0x7ce7
command
silent
set $xmm3.uint128=0x0
continue
end

hbreak *0x7c75
command
silent
printf "EXPECTED_CIPHERTEXT:\n"
p/x $xmm3.v16_int8
set logging off
end

continue

Running the script with gdb -x gdb_cmds.txt, and entering helloworld123456 as a password, results in the following dumps.txt file:

dumps.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
ENC_IV:
$1 = {0x6d, 0x79, 0x80, 0xb9, 0xa5, 0xa, 0x97, 0x24, 0xd, 0x2d, 0xfc, 0x36, 0xd, 0x95, 0x55, 0xaa}
AESENC ROUND_KEY:
$2 = {0x46, 0x85, 0x2c, 0x6e, 0xe3, 0x8f, 0xbb, 0x4a, 0xee, 0xa2, 0x47, 0x7c, 0xe3, 0x37, 0x12, 0xd6}
AESENC ROUND_KEY:
$3 = {0xde, 0x4c, 0xda, 0x7f, 0x3d, 0xc3, 0x61, 0x35, 0xd3, 0x61, 0x26, 0x49, 0x30, 0x56, 0x34, 0x9f}
AESENC ROUND_KEY:
$4 = {0x6b, 0x54, 0x1, 0x7b, 0x56, 0x97, 0x60, 0x4e, 0x85, 0xf6, 0x46, 0x7, 0xb5, 0xa0, 0x72, 0x98}
AESENC ROUND_KEY:
$5 = {0x83, 0x14, 0x47, 0xae, 0xd5, 0x83, 0x27, 0xe0, 0x50, 0x75, 0x61, 0xe7, 0xe5, 0xd5, 0x13, 0x7f}
AESENC ROUND_KEY:
$6 = {0x90, 0x69, 0x95, 0x77, 0x45, 0xea, 0xb2, 0x97, 0x15, 0x9f, 0xd3, 0x70, 0xf0, 0x4a, 0xc0, 0xf}
AESENC ROUND_KEY:
$7 = {0x66, 0xd3, 0xe3, 0xfb, 0x23, 0x39, 0x51, 0x6c, 0x36, 0xa6, 0x82, 0x1c, 0xc6, 0xec, 0x42, 0x13}
AESENC ROUND_KEY:
$8 = {0xe8, 0xff, 0x9e, 0x4f, 0xcb, 0xc6, 0xcf, 0x23, 0xfd, 0x60, 0x4d, 0x3f, 0x3b, 0x8c, 0xf, 0x2c}
AESENC ROUND_KEY:
$9 = {0xc, 0x89, 0xef, 0xad, 0xc7, 0x4f, 0x20, 0x8e, 0x3a, 0x2f, 0x6d, 0xb1, 0x1, 0xa3, 0x62, 0x9d}
AESENC ROUND_KEY:
$10 = {0x1d, 0x23, 0xb1, 0xd1, 0xda, 0x6c, 0x91, 0x5f, 0xe0, 0x43, 0xfc, 0xee, 0xe1, 0xe0, 0x9e, 0x73}
AESENCLAST ROUND_KEY:
$11 = {0xca, 0x28, 0x3e, 0x29, 0x10, 0x44, 0xaf, 0x76, 0xf0, 0x7, 0x53, 0x98, 0x11, 0xe7, 0xcd, 0xeb}
EXPECTED_CIPHERTEXT:
$12 = {0xf7, 0xfe, 0x1a, 0x80, 0x36, 0x51, 0x38, 0x90, 0xa0, 0x34, 0x11, 0x6d, 0x30, 0x5e, 0x52, 0x54}

Decrypting USER_INPUT: Writing a Python script and getting the flag!

Now that we have dumped all values needed, we can write a Python script to decrypt USER_INPUT.
However, we still need a Python implementation for the aesdec and aesdeclast functions.
I modified a publicly available Python AES implementation: aes.py

Using the the following docs as reference:

I added the equivalent AES-NI instructions needed:

aes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# ...
def AESENCLAST(self, plaintext, round_key):
state = text2matrix(plaintext)
rkey = text2matrix(round_key)
self.sub_bytes(state)
self.shift_rows(state)
self.add_round_key(state, rkey)
return matrix2text(state)

def AESENC(self, plaintext, round_key):
state = text2matrix(plaintext)
rkey = text2matrix(round_key)
self.shift_rows(state)
self.sub_bytes(state)
self.mix_columns(state)
self.add_round_key(state, rkey)
return matrix2text(state)

def AESDECLAST(self, ciphertext, round_key):
state = text2matrix(ciphertext)
rkey = text2matrix(round_key)
self.add_round_key(state, rkey)
self.inv_sub_bytes(state)
self.inv_shift_rows(state)
return matrix2text(state)

def AESDEC(self, ciphertext, round_key):
state = text2matrix(ciphertext)
rkey = text2matrix(round_key)
self.add_round_key(state, rkey)
self.inv_mix_columns(state)
self.inv_sub_bytes(state)
self.inv_shift_rows(state)
return matrix2text(state)
# ...

The fully modified file can be downloaded here: aes.py

Now that we have everything in place, we can write the full decrypt.py script:

decrypt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python2
import aes as crypto

ROUND_KEYS = [
[0x46, 0x85, 0x2c, 0x6e, 0xe3, 0x8f, 0xbb, 0x4a, 0xee, 0xa2, 0x47, 0x7c, 0xe3, 0x37, 0x12, 0xd6],
[0xde, 0x4c, 0xda, 0x7f, 0x3d, 0xc3, 0x61, 0x35, 0xd3, 0x61, 0x26, 0x49, 0x30, 0x56, 0x34, 0x9f],
[0x6b, 0x54, 0x1, 0x7b, 0x56, 0x97, 0x60, 0x4e, 0x85, 0xf6, 0x46, 0x7, 0xb5, 0xa0, 0x72, 0x98],
[0x83, 0x14, 0x47, 0xae, 0xd5, 0x83, 0x27, 0xe0, 0x50, 0x75, 0x61, 0xe7, 0xe5, 0xd5, 0x13, 0x7f],
[0x90, 0x69, 0x95, 0x77, 0x45, 0xea, 0xb2, 0x97, 0x15, 0x9f, 0xd3, 0x70, 0xf0, 0x4a, 0xc0, 0xf],
[0x66, 0xd3, 0xe3, 0xfb, 0x23, 0x39, 0x51, 0x6c, 0x36, 0xa6, 0x82, 0x1c, 0xc6, 0xec, 0x42, 0x13],
[0xe8, 0xff, 0x9e, 0x4f, 0xcb, 0xc6, 0xcf, 0x23, 0xfd, 0x60, 0x4d, 0x3f, 0x3b, 0x8c, 0xf, 0x2c],
[0xc, 0x89, 0xef, 0xad, 0xc7, 0x4f, 0x20, 0x8e, 0x3a, 0x2f, 0x6d, 0xb1, 0x1, 0xa3, 0x62, 0x9d],
[0x1d, 0x23, 0xb1, 0xd1, 0xda, 0x6c, 0x91, 0x5f, 0xe0, 0x43, 0xfc, 0xee, 0xe1, 0xe0, 0x9e, 0x73],

[0xca, 0x28, 0x3e, 0x29, 0x10, 0x44, 0xaf, 0x76, 0xf0, 0x7, 0x53, 0x98, 0x11, 0xe7, 0xcd, 0xeb]
]
CIPHERTEXT = [0xf7, 0xfe, 0x1a, 0x80, 0x36, 0x51, 0x38, 0x90, 0xa0, 0x34, 0x11, 0x6d, 0x30, 0x5e, 0x52, 0x54]
ENC_IV = [0x6d, 0x79, 0x80, 0xb9, 0xa5, 0xa, 0x97, 0x24, 0xd, 0x2d, 0xfc, 0x36, 0xd, 0x95, 0x55, 0xaa]

def decrypt(ciphertext, iv, round_keys):
aes = crypto.AES()
last_rkt = ''.join([chr(x) for x in round_keys[-1]])

ctxt = ciphertext
ctxt = aes.AESDECLAST(ctxt, last_rkt)
for rk in round_keys[:-1][::-1]:
rkt = ''.join([chr(x) for x in rk])
ctxt = aes.AESDEC(ctxt, rkt)

return crypto.xor(ctxt, iv)

iv = ''.join([chr(x) for x in ENC_IV])
ctxt = ''.join([chr(x) for x in CIPHERTEXT])
ptxt = decrypt(ctxt, iv, ROUND_KEYS)
print 'USER_INPUT: ' + ptxt

Running the script gives the value needed for USER_INPUT (Password):

1
2
$ python decrypt_final.py 
USER_INPUT: MiLiT4RyGr4d3MbR

Finally, running the bootloader in QEMU without a debugger, and entering MiLiT4RyGr4d3MbR as the password, gives us the flag:

1
$ qemu-system-i386 -cpu SandyBridge -fda boot.bin

1
AOTW{31oct__1s__25dec}