This page looks best with JavaScript enabled

pbctf 2021 Writeups

Hosted by perfect blue from 9 October - 11 October

 ·  ☕ 6 min read  ·  🌈🕊️ rainbowpigeon

Thanks to perfect blue for these nice challenges! The Binary Tree and Switching it up reversing challenges were probably also doable but I didn’t really have the required skill to complete them within the given CTF duration 👍
🎵 Sander van Doorn x Lucas Steve - The World is actually so good.

Details Links
CTFtime.org Event Page https://ctftime.org/event/1371

Misc

BTLE

I stored my flag inside a remote database, but when I tried to read it back it had been redacted! Can you recover what I wrote?

Author: UnblvR

Attached: btle.pcap

Opening up the capture file in Wireshark, we notice that a large percentage of the packets are just Empty PDUs from the Bluetooth Low Energy Link Layer (LE LL).

Wireshark capture shows Bluetooth Low Energy Link Layer and Bluetooth Attribute Protocols

Packets of the Bluetooth Attribute Protocol (ATT) appear more interesting because they seem to contain Read/Write requests, which should refer to the storing and retrieving of the flag from the remote database stated in the challenge’s description.

If we filter by btatt and scroll to the bottom, we can spot a Read Response packet containing the redacted flag in the Value attribute.

ATT Read Response packet containing redacted flag in Value attribute in Wireshark

Before this single Read Request/Response, there’s a whole bunch of Prepare Write Request/Responses with long strings in the Value attribute being written at various Offsets. These are also known as Queued Writes which will all be written together by a separate Execute Write Request opcode.

Many Prepared Write Request/Responses containing long strings in Value attribute in Wireshark

I posited that these long strings written at different overlapping Offsets would eventually overwrite each other and leave behind the flag in plaintext, so I replicated this behavior in Python using a io.BytesIO object which basically behaves like a file but only resides in memory.
In the script, I filtered for Prepare Write Responses with btatt.opcode == 0x16, retrieved the Value attribute and wrote them at their corresponding Offsets to the BytesIO object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import pyshark
import binascii
import io
cap = pyshark.FileCapture("btle.pcap", display_filter="btatt.opcode == 0x16")

flag = io.BytesIO()
for packet in cap:
	offset = int(packet.btatt.offset)
	flag.seek(offset)

	data = packet.btatt.value.replace(":", "")
	data = binascii.unhexlify(data)

	flag.write(data)

flag.seek(0)
flag.write(b"pb")

print(flag.getvalue())

cap.close()

I also added an additional write of b"pb" to the beginning of the BytesIO object (lines 16-17) because among the sea of Prepare Write Requests, there was a sneaky standard Write for just these 2 letters. But of course, even if you missed this out, it wouldn’t matter because the flag format is known and you would know the correct first 2 letters.

One standard Write request for letters 'pb' in Wireshark

1
2
C:\Downloads\PB\btle_solve.py
b'pbctf{b1Ue_te3Th_is_ba4d_4_y0u_jUs7_4sk_HAr0lD_b1U3tO07h}\n'

Flag: pbctf{b1Ue_te3Th_is_ba4d_4_y0u_jUs7_4sk_HAr0lD_b1U3tO07h}

Reverse

Cosmo

To make it fair for everyone, this binary is fully portable. Run it anywhere! This definitely makes it easier, right?

Author: UnblvR

Attached: hello.com

Googling “cosmo portable binary” led me to discover that the binary is built with Cosmopolitan and is in what is known as the αcτµαlly pδrταblε εxεcµταblε (Actually Portable Executable) format. In short, the binary can run natively on multiple different operating systems without requiring a virtual machine or interpreter.

What are the implications of this? Static analysis will be more difficult, because there will be many distracting unused code paths present in the binary that are only meant to be taken when run on other operating systems. Thus, we will be doing mostly dynamic analysis.

First, basic black-box testing tells us that hello.com accepts a command-line argument that will be checked for the flag.

1
2
3
4
5
C:\Downloads\PB>hello.com
Give flag

C:\Downloads\PB>hello.com asd
No

Next, I opened up the binary in IDA Pro and did a binary string search (Alt-B) for the UTF-8 string “Give flag”.

Flag-checking related strings all cross-referenced by the same subroutine 'sub_403066' in IDA

The strings “Give flag”, “No”, and “Correct” located next to each other all seem related to flag-checking and are all cross-referenced by sub_403066. Thus, we’ll call this the check_flag function.

Flag-checking related strings all cross-referenced by the same subroutine 'sub_403066' in IDA

Through dynamic analysis with a dummy command-line argment passed in, I got a good sense of the significance of the variables and subroutines in this check_flag function so I renamed them.
Here’s what I understood from my analysis:

  • rdi (v0) is the number of command-line arguments including the executing binary’s filename (argc)
  • rsi is the vector of command-line arguments (argv), so [rsi+8] is the 2nd command-line argument which is our input
  • length of input is checked to be 38 characters
  • enc_flag is an array of 19 QWORDs
  • 2 characters from the input are taken at a time, encoded, and checked against a QWORD from enc_flag

You might notice there are variables in red (v0, v1, v2) in the pseudocode view on the right, and also subroutine calls like len(), print(), and exit() contain no arguments though the arguments can be clearly seen in the disassembly view on the left.
We can fix this by specifying proper custom function type declarations so that IDA knows where function arguments and return values are stored. IDA has helpful documentation for this.

As an example, right click on print(), click Set Item Type, and enter in the definition __int64 __userpurge print@<rax>(char *y@<rdi>). This essentially means that the argument y is passed in rdi, and the __int64 return value will be in rax. _userpurge means that the callee will clean up the stack. I’ll leave the rest of the function prototype redefinitions as an exercise for the reader, but once you’re done the pseudocode will make more sense:

Clean pseudocode in IDA after redefining function prototypes

In the encode subroutine,

  • 1st argument will always be 2 which is the number of characters to take from our string input
  • 2nd argument is our input
  • 3rd argument is 1 for the first time encode is called but will be the encoded output for future runs

Pseudocode of encode function in IDA with comments

On lines 8 and 9, the 3rd argument is split into it’s high DWORD and low DWORD. So for the example of the 3rd argument being 1, the high DWORD will be 0 while the low DWORD will be 1.
Since the first argument is always 2, most of the if/while conditions are not taken and we can ignore a lot of the code. The main loop that is actually executed is below:

Main loop executed in encode function as IDA pseudocode

The result of this is the following where $ c_1 $ and $ c_2 $ refers to the 2 characters from the input:

  • $ low_{new} = [low_{old} + c_1 + c_2] \ \% \ \mathrm{0xfff1} $
  • $ high_{new} = [high_{old} + (low_{old} + c_1) + (low_{old} + c_1 + c_2)] \ \% \ \mathrm{0xfff1} $

$ low_{new} $ and $ high_{new} $ will then form the encoded QWORD result that is checked with enc_flag, and passed in as the 3rd argument to encode the next time is called to become the next $ low_{old} $ and $ high_{old} $.
Armed with this knowledge, we can extract out the 19 QWORDs stored at enc_flag and basically do the reverse of the operations to get the flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
flag = ""

enc_flag = [
	0x14400D3, 0x42401AA, 0x8BF028B, 0x0EFA034F, 0x16A1040D, 0x200004EA, 0x2AE20597,
	0x3721065C, 0x4507072B, 0x542F07CD, 0x651208A2, 0x77860970, 0x8B8F0A34, 0x0A0D50ADF,
	0x0B75C0B75, 0x0CFA40C5E, 0x0E9440D01, 0x4520DB2, 0x20B10E6E
]

high_old = 0
low_old = 1

for qword in encoded_flag:
	high = (qword & 0xffff0000) >> 16
	low = (qword & 0x0000ffff)

	c1 = (high - high_old - low_old - low) % 0xfff1
	flag += chr(c1)
	c2 = (low - low_old - c1) % 0xfff1
	flag += chr(c2)
	
	high_old = high
	low_old = low

print(flag)
1
2
C:\Downloads\PB>cosmo_solve.py
pbctf{acKshuaLLy_p0rtable_3x3cutAbLe?}

Flag: pbctf{acKshuaLLy_p0rtable_3x3cutAbLe?}


Thanks for reading!

Share on

rainbowpigeon
WRITTEN BY
rainbowpigeon
OSCP