About CrowdStrike Adversary Hunt CTF and Space Jackal – The Proclamation writeup
This is our CrowdStrike Adversary Hunt 2021 CTF – Space Jackal – The Proclamation writeup (Bootloader Reverse Engineering Debug Challenge). The event took place from 18.01.2021 until 29.01.2021 and contained 3 main modules with 4 challenges each.
Affiliate: Experience limitless no-code automation, streamline your workflows, and effortlessly transfer data between apps with Make.com.
Space Jackal – The Proclamation Challenge Description
“Space Jackal” module description:
Not to be confused with space flight enthusiasts, SPACE JACKAL have very strict opinions on source code indentation. Brought together by their unbounded hate for ASCII character 9, they will not rest until the last tab stop has been eradicated from the face of the internet.
Note: ASCII character 9 is the [TAB] character. Example: If you press [TAB] in Notepad you will see cursor jumping a several spaces. Notepad will save this as [TAB] character ASCII 9.
“The Proclamation” Challenge description:
A mysterious file appeared on a deep dark web forum. Can you figure out what we can't see right now? NOTE: Flags will be easily identifiable by following the format CS{some_secret_flag_text}. They must be submitted in full, including the CS{ and } parts. FILE: proclamation.dat
Space Jackal – The Proclamation Writeup
Launching VM
Opened Kali Linux VM (you can obtain the pre-installed image from official source) with “VMware Workstation Player”. Installed “VMware Tools” and dropped the downloaded “proclamation.dat” into VM.
Determine the File Type
Executed “file” command in Terminal:
file proclamation.dat
Result:
DOS/MBR boot sector
This is bootable image. Only 512 bytes, meaning it is only the bootloader.
More information about the image with “fdisk” command:
fdisk -l proclamation.dat
“-l” switch is “listing” the partition table of a device; in our case the device is “proclamation.dat” image.
Result:
Disk /proclamation.dat: 512 B, 512 bytes, 1 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x8587ca82 Device Boot Start End Sectors Size Id Type /root/Desktop/AdversryHunt/Proclamation/proclamation.dat1 3939211656 6775681800 2836470145 1.3T 84 OS/2 /root/Desktop/AdversryHunt/Proclamation/proclamation.dat2 3669331162 6720349547 3051018386 1.4T 8e Linu /root/Desktop/AdversryHunt/Proclamation/proclamation.dat3 2543294681 6652988866 4109694186 1.9T 86 NTFS /root/Desktop/AdversryHunt/Proclamation/proclamation.dat4 4109694196 8219388391 4109694196 1.9T f4 Spee
Launching the Bootloader Image with QEMU
Installed QEMU to boot the image:
sudo apt-get install qemu-system
In case you will need more components:
sudo apt-get install qemu
sudo apt-get install qemu-utils
sudo apt-get install qemu-user
Loaded the image:
qemu-system-i386 -drive format=raw,file=proclamation.dat
Commands and Switches:
qemu-system-i386: Since the image type is DOS/MBR, this is the type of emulation architecture that we need for QEMU.
-drive format=raw,file=proclamation.dat: “Drive” is the switch that defines new drive.
“Format” is an option of the “drive” switch, since we did not find the exact image type and it is only the bootloader it is set to “raw”.
“File” is the second option of the “drive” switch, which sets the image file path.
QEMU loaded a screen with a message:
Hello. We are looking for highly intelligent individuals. To find them, we have devised a test. There is a message hidden in this bootloader. Find it, and it will lead you on the road to finding us. We look forward to meet the few that will make it all the way through. Good luck, and remember: We love spaces much more than tabs!
Readable Strings
To make sure I am not missing anything obvious – opened this file with “Lister” application [F3] of “Total Commander” on Windows to see any strings. The only thing readable was:
you're on a good way
Like it would be that easy…
Disassembling the Bootloader Image – Space Jackal – The Proclamation Writeup
Back to Linux – using “ndisasm” to see the assembly:
ndisasm -b 16 proclamation.dat
Since it is MBR bootloader used to load on BIOS – we will use the 16-bit mode.
Result:
00000000 BC0020 mov sp,0x2000
00000003 B407 mov ah,0x7
00000005 30C0 xor al,al
00000007 31C9 xor cx,cx
00000009 B78A mov bh,0x8a
0000000B B661 mov dh,0x61
0000000D B261 mov dl,0x61
0000000F CD10 int 0x10
00000011 B402 mov ah,0x2
00000013 31D2 xor dx,dx
00000015 30FF xor bh,bh
00000017 CD10 int 0x10
00000019 EB46 jmp short 0x61
0000001B 5E pop si
0000001C 83C614 add si,byte +0x14
0000001F B209 mov dl,0x9
00000021 6652 push edx
00000023 B300 mov bl,0x0
00000025 B40E mov ah,0xe
00000027 8A04 mov al,[si]
00000029 83C601 add si,byte +0x1
0000002C 665A pop edx
0000002E 66C1E202 shl edx,byte 0x2
00000032 6683C242 add edx,byte +0x42
00000036 6681E2FF000000 and edx,0xff
0000003D 6631D0 xor eax,edx
00000040 6652 push edx
00000042 0C00 or al,0x0
00000044 7419 jz 0x5f
00000046 88C2 mov dl,al
00000048 80F20A xor dl,0xa
0000004B 7404 jz 0x51
0000004D CD10 int 0x10
0000004F EBD2 jmp short 0x23
00000051 B403 mov ah,0x3
00000053 CD10 int 0x10
00000055 B402 mov ah,0x2
00000057 FEC6 inc dh
00000059 B200 mov dl,0x0
0000005B CD10 int 0x10
0000005D EBC4 jmp short 0x23
0000005F FA cli
00000060 F4 hlt
00000061 E8B7FF call 0x1b
00000064 796F jns 0xd5
00000066 7527 jnz 0x8f
00000068 7265 jc 0xcf
0000006A 206F6E and [bx+0x6e],ch
0000006D 206120 and [bx+di+0x20],ah
00000070 676F a32 outsw
00000072 6F outsw
00000073 64207761 and [fs:bx+0x61],dh
00000077 792E jns 0xa7
00000079 BFC686 mov di,0x86c6
0000007C 85C4 test sp,ax
0000007E CABD8F retf 0x8fbd
00000081 CA8B98 retf 0x988b
00000084 8F db 0x8f
00000085 CA8685 retf 0x8586
00000088 85818384 test [bx+di-0x7b7d],ax
0000008C 8D db 0x8d
0000008D CA8C85 retf 0x858c
00000090 98 cbw
00000091 CA8283 retf 0x8382
00000094 8D828693 lea ax,[bp+si-0x6c7a]
00000098 CA8384 retf 0x8483
0000009B 9E sahf
0000009C 8F868683 pop word [bp-0x7c7a]
000000A0 8D8F849E lea cx,[bx-0x617c]
000000A4 E083 loopne 0x29
000000A6 848E839C test [bp-0x637d],cl
000000AA 838E9F8B86 or word [bp-0x7461],byte -0x7a
000000AF 99 cwd
000000B0 C4 db 0xc4
000000B1 CABE85 retf 0x85be
000000B4 CA8C83 retf 0x838c
000000B7 848ECA9E test [bp-0x6136],cl
000000BB 82 db 0x82
000000BC 8F87C6CA pop word [bx-0x353a]
000000C0 9D popf
000000C1 8F db 0x8f
000000C2 CA828B retf 0x8b82
000000C5 9C pushf
000000C6 8F db 0x8f
000000C7 CA8E8F retf 0x8f8e
000000CA 9C pushf
000000CB 83998F8EE0 sbb word [bx+di-0x7171],byte -0x20
000000D0 8BCA mov cx,dx
000000D2 9E sahf
000000D3 8F db 0x8f
000000D4 99 cwd
000000D5 9E sahf
000000D6 C4 db 0xc4
000000D7 E0E0 loopne 0xb9
000000D9 BE828F mov si,0x8f82
000000DC 98 cbw
000000DD 8F db 0x8f
000000DE CA8399 retf 0x9983
000000E1 CA8BCA retf 0xca8b
000000E4 878F9999 xchg cx,[bx-0x6667]
000000E8 8B8D8FCA mov cx,[di-0x3571]
000000EC 82 db 0x82
000000ED 838E8E8F84 or word [bp-0x7072],byte -0x7c
000000F2 CA8384 retf 0x8483
000000F5 CA9E82 retf 0x829e
000000F8 8399CA8885 sbb word [bx+di-0x7736],byte -0x7b
000000FD 859E8685 test [bp-0x7a7a],bx
00000101 8B8E8F98 mov cx,[bp-0x6771]
00000105 C4 db 0xc4
00000106 E0E0 loopne 0xe8
00000108 AC lodsb
00000109 83848ECA83 add word [si-0x3572],byte -0x7d
0000010E 9E sahf
0000010F C6 db 0xc6
00000110 CA8B84 retf 0x848b
00000113 8ECA mov cs,dx
00000115 839ECA9D83 sbb word [bp-0x6236],byte -0x7d
0000011A 8686CA86 xchg al,[bp-0x7936]
0000011E 8F db 0x8f
0000011F 8B8ECA93 mov cx,[bp-0x6c36]
00000123 859FCA85 test [bx-0x7a36],bx
00000127 84CA test dl,cl
00000129 9E sahf
0000012A 82 db 0x82
0000012B 8F db 0x8f
0000012C CA9885 retf 0x8598
0000012F 8B8ECA9E mov cx,[bp-0x6136]
00000133 85E0 test ax,sp
00000135 8C83848E mov [bp+di-0x717c],es
00000139 83848DCA9F add word [si-0x3573],byte -0x61
0000013E 99 cwd
0000013F C4 db 0xc4
00000140 CABD8F retf 0x8fbd
00000143 CA8685 retf 0x8586
00000146 8581CA8C test [bx+di-0x7336],ax
0000014A 85989D8B test [bx+si-0x7463],bx
0000014E 98 cbw
0000014F 8ECA mov cs,dx
00000151 9E sahf
00000152 85CA test dx,cx
00000154 878F8F9E xchg cx,[bx-0x6171]
00000158 CA9E82 retf 0x829e
0000015B 8F db 0x8f
0000015C E08C loopne 0xea
0000015E 8F db 0x8f
0000015F 9D popf
00000160 CA9E82 retf 0x829e
00000163 8B9ECA9D mov bx,[bp-0x6236]
00000167 838686CA87 add word [bp-0x357a],byte -0x79
0000016C 8B818FCA mov ax,[bx+di-0x3571]
00000170 839ECA8B86 sbb word [bp-0x7436],byte -0x7a
00000175 86CA xchg cl,dl
00000177 9E sahf
00000178 82 db 0x82
00000179 8F db 0x8f
0000017A CA9D8B retf 0x8b9d
0000017D 93 xchg ax,bx
0000017E CA9E82 retf 0x829e
00000181 98 cbw
00000182 859F8D82 test [bx-0x7d73],bx
00000186 C4 db 0xc4
00000187 E0E0 loopne 0x169
00000189 AD lodsw
0000018A 85858ECA test [di-0x3572],ax
0000018E 869F8981 xchg bl,[bx-0x7e77]
00000192 C6 db 0xc6
00000193 CA8B84 retf 0x848b
00000196 8ECA mov cs,dx
00000198 98 cbw
00000199 8F878F87 pop word [bx-0x7871]
0000019D 888F98D0 mov [bx-0x2f68],cl
000001A1 E0CA loopne 0x16d
000001A3 CACACA retf 0xcaca
000001A6 BD8FCA mov bp,0xca8f
000001A9 86859C8F xchg al,[di-0x7064]
000001AD CA999A retf 0x9a99
000001B0 8B898F99 mov cx,[bx+di-0x6671]
000001B4 CA879F retf 0x9f87
000001B7 8982CA87 mov [bp+si-0x7836],ax
000001BB 85988FCA test [bx+si-0x3571],bx
000001BF 9E sahf
000001C0 82 db 0x82
000001C1 8B84CA9E mov ax,[si-0x6136]
000001C5 8B8899CB mov cx,[bx+si-0x3467]
000001C9 EA811911A9 jmp 0xa911:0x1981
000001CE B991DA mov cx,0xda91
000001D1 98 cbw
000001D2 8ED9 mov ds,cx
000001D4 98 cbw
000001D5 B5DA mov ch,0xda
000001D7 8CB5DA92 mov [di-0x6d26],segr6
000001DB D8DA fcomp st2
000001DD B588 mov ch,0x88
000001DF DADA fcmovu st2
000001E1 9E sahf
000001E2 86DA xchg bl,dl
000001E4 8B8ED998 mov cx,[bp-0x6727]
000001E8 97 xchg ax,di
000001E9 97 xchg ax,di
000001EA EAF4F4F4F4 jmp 0xf4f4:0xf4f4
000001EF F4 hlt
000001F0 F4 hlt
000001F1 F4 hlt
000001F2 F4 hlt
000001F3 F4 hlt
000001F4 F4 hlt
000001F5 F4 hlt
000001F6 F4 hlt
000001F7 F4 hlt
000001F8 F4 hlt
000001F9 F4 hlt
000001FA F4 hlt
000001FB F4 hlt
000001FC F4 hlt
000001FD F4 hlt
000001FE 55 push bp
000001FF AA stosb
Will use this as a main map before debugging.
Understanding the Assembly Code and Choosing Main Points
The main points that we see:
0000000F CD10 int 0x10
00000017 CD10 int 0x10
These are interruption assembly instructions of type 10 hex (0x10 / 10h). This specific type is BIOS Graphical interruption. AH register value specifies the exact type of visualization. In addition, it executes only if the IF (Interruption Flag) bit is set (equals 1) in the EFLAGS register.
You can check the AH value in the code of the first two and see that they are not doing anything special besides setting the initial screen and the first point.
The next main point:
00000019 EB46 jmp short 0x61
This is unconditionally jumping to address 0x61. And at address 0x61 is the following:
00000061 E8B7FF call 0x1b
This is calling a function in address 0x1b, so we are going back to 0x1b:
0000001B 5E pop si
After that the code continues to execute, so probably would not call it a function, but rather another unconditional jump.
The next main point is:
00000044 7419 jz 0x5f
The address is 0x44 and it is interesting, since it is conditional jump when ZF (Zero Flag) bit is set (equals 1) in EFLAGS register. If it is set, we will jump to address 0x5f. Probably the ZF will be set because of previous instruction in address 0x42:
00000042 0C00 or al,0x0
Bitwise operators like OR can set the Zero Flag.
And in 0x5f address we have:
0000005F FA cli
00000060 F4 hlt
“cli” clears the IF bit in EFLAGS register, so no interruptions will execute. “hlt” instruction is halting the whole program, so it will be stuck at this point. Probably we will need to check whether ZF will be set at the 0x44 address and clear it, so the execution will not jump to 0x5f address to halt the code execution.
We will remember the addresses 0x42 and 0x44, when we will debug the bootloader, since probably it is a clue.
We will continue to follow the code with the main points from address 0x44.
After address 0x44 there are two instructions and after them there is another JZ:
0000004B 7404 jz 0x51
This is another conditional jump if ZF bit is set in the EFLAGS register. The instruction before that:
00000048 80F20A xor dl,0xa
can set ZF bit of EFLAGS register in case DL register value will be “0xa”.
This is another thing that we need to remember, but probably we will not need to – depends on the debugging process.
First, we will skip a jump in 0x44 and if it will not help, will also set the ZF flag manually in 0x4b to jump into 0x51. So, we will remember this 0x4b address as well.
After address 0x4b the next line is another interruption:
0000004D CD10 int 0x10
If you were following the code, you will see the AH register value at this point is 0xe, which was set at the address 0x25. This means that this particular interruption will throw ASCII characters to the screen. The ASCII character is set through the hex value of AL register. At this point AL hex value is 0x48, which is “H” in ASCII. If you remember the message on the screen after the regular execution in QEMU, the first word is “Hello”, so this interruption will output characters to the screen in the order that we saw in the message.
After this interruption:
0000004F EBD2 jmp short 0x23
We jump back to 0x23 address. Probably AL will receive new values each time until it will equal to 0x0 and OR instruction in 0x42 address will set ZF flag in EFLAGS register. This means that at some point ZF will be set and we will jump to address 0x5f and the program will stop, but we will never get to address 0x51, which could probably have the answer.
If we will check what is going on at the address 0x51:
00000051 B403 mov ah,0x3
00000053 CD10 int 0x10
00000055 B402 mov ah,0x2
00000057 FEC6 inc dh
00000059 B200 mov dl,0x0
0000005B CD10 int 0x10
0000005D EBC4 jmp short 0x23
There are several BIOS visual interruptions of AH value 0x3 and 0x2, and after that we jump back to address 0x23, that already happened before, to print more ASCII characters.
Note: if you view the rest of the code it is:
0000005F FA cli
00000060 F4 hlt
00000061 E8B7FF call 0x1b
that we already reviewed before. Meaning that after 0x61 is nothing related to the program execution. Could be the string that we saw in the beginning and some random stuff.
Time to initiate debugging.
Debugging the Bootloader in GDB – Space Jackal – The Proclamation Writeup
Open new terminal – install GDB:
sudo apt-get install gdb
Start gdb (the “-q” quiet switch will not show you the GDB opening message):
gdb -q
Open another terminal and start QEMU in debugging mode with following switches:
qemu-system-x86_64 -s -S -m 512 -hda proclamation.dat
The above switches:
-s: same as “-gdb tcp::1234”, meaning open a gdbserver on TCP port 1234.
-S: do not start CPU at startup.
-m 512: sets the virtual RAM size to 512 Megabytes.
– hda proclamation.dat: use file as hard disk 0.
The QEMU will start frozen in initial state.
Return to GDB and connect to QEMU session:
(gdb) target remote localhost:1234
The Bootloader MBR code is starting to execute in the memory at address 0x7c00. So, we will set the first break point there (temporary, since we do not want to hit it each time):
(gdb) tb *0x7c00
tb is equivalent to tbreak command.
Continue the execution of the code until the break point:
(gdb) c
c is equivalent to continue command.
Since we hit the break point at 0x7c00 let us check that we see the code that we found in the output of “ndisasm”:
(gdb) x/50i $pc
Some explanations:
x: shows values of registers, instructions, etc. depending on the switches.
/50i: show 50 instructions of the input address
$pc: is the address of the current instruction that is about to execute.
The whole command means it will show 50 instructions from the next execution address.
Why 50 instructions: The latest address of the code execution should be 0x7c61 as we saw in “ndisasm”. 61 in hex is 97 in decimal. Each instruction has several hex values, it could be one hex value if it is only the instruction itself (like “cli”) or it can be three hex values (like “mov al, 0x3”), basically dividing 97 in 2 should be fine. So, rounding this to 50 should give the approximate number of instructions around address 0x7c61.
The result:
0x7c00: mov $0x7b42000,%esp
0x7c05: xor %al,%al
0x7c07: xor %ecx,%ecx
0x7c09: mov $0x8a,%bh
0x7c0b: mov $0x61,%dh
0x7c0d: mov $0x61,%dl
0x7c0f: int $0x10
0x7c11: mov $0x2,%ah
0x7c13: xor %edx,%edx
0x7c15: xor %bh,%bh
0x7c17: int $0x10
0x7c19: jmp 0x7c61
0x7c1b: pop %rsi
0x7c1c: add $0x14,%esi
0x7c1f: mov $0x9,%dl
0x7c21: push %dx
0x7c23: mov $0x0,%bl
0x7c25: mov $0xe,%ah
0x7c27: mov (%rbx,%rax,4),%al
0x7c2a: movb $0x66,(%rcx)
0x7c2d: pop %rdx
0x7c2e: shl $0x2,%dx
0x7c32: add $0x42,%dx
0x7c36: and $0xff,%dx
0x7c3b: add %al,(%rax)
0x7c3d: xor %dx,%ax
0x7c40: push %dx
0x7c42: or $0x0,%al
0x7c44: je 0x7c5f
0x7c46: mov %al,%dl
0x7c48: xor $0xa,%dl
--Type <RET> for more, q to quit, c to continue without paging--
0x7c4b: je 0x7c51
0x7c4d: int $0x10
0x7c4f: jmp 0x7c23
0x7c51: mov $0x3,%ah
0x7c53: int $0x10
0x7c55: mov $0x2,%ah
0x7c57: inc %dh
0x7c59: mov $0x0,%dl
0x7c5b: int $0x10
0x7c5d: jmp 0x7c23
0x7c5f: cli
0x7c60: hlt
0x7c61: call 0x6f7a7c1d
0x7c66: jne 0x7c8f
0x7c68: jb 0x7ccf
0x7c6a: and %ch,0x6e(%rdi)
0x7c6d: and %ah,0x20(%rcx)
0x7c70: outsl %ds:(%esi),(%dx)
0x7c72: outsl %ds:(%rsi),(%dx)
I was close. You can see that the code is similar. As you remember the address of interest was 0x44. Since we are debugging the bootloader, the address point will be 0x7c44, which is a conditional jump if ZF flag is set.
Now let us check the EFLAGS register to see what flags are set:
(gdb) i r eflags
i r eflags is equivalent to info registers eflags command.
The result:
eflags 0x202 [ IOPL=0 IF ]
This Wikipedia FLAGS Register page will show you all the Flags bits position and its hex value.
Bit 1 of EFLAGS register is always set to 1, so its hex value 0x2 always will be added to the overall hex value of EFLAGS register. IF bit hex value is 0x200. If only IF bit is set – the overall hex value of EFLAGS register will be 0x202.
Now to the main point. We want the execution to stop at address 0x7c44 (when ZF bit of EFLAGS register is already set) before conditional jump to 0x7c5f address. This way we will have the ZF set right before the conditional jump without actually jumping.
Let us check what is the value of EFLAGS register when we are on the first hit of 0x7c44 address.
Set the breakpoint to that location:
(gdb) b *0x7c44
Continue the code execution util that breakpoint:
(gdb) c
Now we hit first time the 0x7c44.
What is the value of eflags:
(gdb) i r eflags
Result:
eflags 0x206 [ IOPL=0 IF PF ]
The PF bit was set also, which has the hex value of 0x4. The overall value of EFLAGS is now 0x206 = 0x2 + 0x4 + 0x200 = Default set bit + IF + PF.
If you continue the execution with “c” command, it will stop at this break point the second time. Check the QEMU window and you will see that the first letter “H” will print. If you will continue, you will see the rest of the characters printed. To make this faster we need to set a conditional break point when the ZF bit of EFLAGS will be set, before jumping.
We will delete all the breakpoints (there is only one), so we will not break on it each loop:
(gdb) d
d is equivalent to delete command.
As you remember, ZF is set because of OR operator in address 0x7c42:
0x7c42: or $0x0,%al
So, to know when the ZF is set, we need to check whether the AL register has value of zero (0).
We will set a conditional break for that:
(gdb) b *0x7c42 if $al == 0x0
Continue the execution until this condition is true:
(gdb) c
When this condition will occur – you will be at the 0x7c42 address. If you will look at the QEMU screen you will see the exact message that we saw in the beginning.
Check what is the next execution line:
(gdb) x/i $pc
Result:
0x7c42: or $0x0,%al
This is what we needed. Next – execute exactly 1 line:
(gdb) si
si is equivalent to stepi command.
Executed the OR instruction. Check the EFLAGS register to see if ZF was set:
(gdb) i r eflags
Result:
eflags 0x246 [ IOPL=0 IF ZF PF ]
The hex value of ZF bit is 0x40, so = Default Bit + IF + ZF + PF = 0x2 + 0x4 + 0x40 + 0x200 = 0x246
Check again what is the next line to make sure it is the conditional jump:
(gdb) x/i $pc
Result:
0x7c44: je 0x7c5f
if we will continue code execution at this point – the program will simply halt as it happened in the regular QEMU execution.
We want to avoid that, so we will clear the ZF bit:
(gdb) set $eflags = $eflags & ~0x40
Check EFLAGS value again to make sure ZF is not set:
(gdb) i r eflags
Result:
eflags 0x206 [ IOPL=0 IF PF ]
Since we know that ZF is no more – we can remove the breakpoints:
(gdb) d
and continue the execution:
(gdb) c
Now look at the QEMU window and collect the flag:
CS{0rd3r_0f_0x20_b00tl0ad3r}
Note: if the flag was not available at this point, we would return to the steps until the last “c” command. Instead of “c” we would “stepi 3” to the address of “0x7c4b” (with second conditional jump) and set the ZF flag with “set $eflags = $eflags ^ 0x40” command and then “c” continue.