0xd13a

A rookie in a world of pwns

ASIS CTF Quals 2020 Writeup: Titanic

Titanic

“… Our ship got caught in a storm and sank. I was one of the few who survived. I was treading water, shocked and disappointed. After a while, I tried swimming toward a shadow which seemed to be the nearest island. …”

nc 76.74.178.201 8002

This is a PPC challenge protected by a PoW. Once we pass that we are greeted with a task description:

1
2
3
4
5
6
7
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ welcome to JPS challenge, its about just printable strings! the number  +
+ n = 114800724110444 gets converted to the printable `hi all', in each   +
+ round find the suitable integer with given property caring about the    +
+ timeout of the submit of solution! all printable = string.printable  :) +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
| whats the nearest number to 1537367419571623870752 that gets converted to the printable string?

So when the number is converted to hex, and then to a string, all characters that are not printable have to be replaced with printable in such a way that the resulting number is the closest possible to the original.

In Python string.printable contains characters from the following ranges: [0x09,0x0d] and [0x20,0x7e].

Analyzing the problem took me on a bunch of detours but essentially it comes down to correctly replacing the 1st nonprintable character on the left.

Suppose the number we have to fix is 0x30104577. Character 0x10 is nonprintable, and the closest printable one is 0x0d. Since are going to a smaller number the rest of the characters would have to have largest possible printable values, so the number becomes 0x300d7e7e.

Let’s do another example - 0x501f3132. Character 0x1f is nonprintable, and the closest printable one is 0x20. Since are going to a larger number the rest of the characters would have to have smallest possible printable values, so the number becomes 0x50200909.

Special care will have to be taken when values cross the 0 threshold. To adjust for that we would have to either increase or decrease by 1 the number before the one we are fixing.

Let’s put this logic into a Python script. It can be a bit better optimized, but this will do:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from pwn import *
import hashlib
import binascii
import string

r = remote('76.74.178.201', 8002)

# Receive and solve the PoW challenge
challenge = r.recv().split()
algo = challenge[8].split("(")[0]
suff = challenge[10]
size = int(challenge[14])
print algo, suff, size
s = iters.mbruteforce(lambda x: getattr(hashlib, algo)(x).hexdigest().endswith(suff), string.letters+string.digits, size, 'fixed')
r.send(s+"\n")

# Print the assignment
print r.recvline().strip()
print r.recvline().strip()
print r.recvline().strip()
print r.recvline().strip()
print r.recvline().strip()
print r.recvline().strip()

while True:

	msg = r.recvline().strip()
	print msg
	
	# Parse the number
	msg_parts = msg.split()
	if msg_parts[1] == "whats":
		num = int(msg_parts[6])
	else:
		break
	
	# Convert to hex
	h = hex(num)[2:]
	if h.endswith("L"):
		h = h[:-1]
	if len(h) % 2 == 1:
		h = '0'+h

	b = bytearray.fromhex(h)
	b_low = bytearray()
	b_high = bytearray()
	
	nearest_num = num

	# Go through the string
	for x in range(len(b)):
		c = b[x]
		
		# Find the first nonprintable character
		if not (chr(c) in string.printable): 
			
			# Find closest lower and higher printable characters
			low = c
			low_threshold_crossed = False
			while not (chr(low) in string.printable):
				low = (low-1) & 0xFF
				if low == 0:
					low_threshold_crossed = True
				
			high = c
			high_threshold_crossed = False
			while not (chr(high) in string.printable):
				high = (high+1) & 0xFF
				if high == 0:
					high_threshold_crossed = True

			b_low[:] = b[:]
			b_high[:] = b[:]

			# Build lower and higher numbers 
			b_low[x] = low
			if low_threshold_crossed and x != 0:
				b_low[x-1] -= 1
			for i in range(x+1,len(b_low)):
				b_low[i] = 0x7e
				
			low_num = int(binascii.hexlify(b_low),16)

			b_high[x] = high
			if high_threshold_crossed and x != 0:
				b_high[x-1] += 1
			for i in range(x+1,len(b_high)):
				b_high[i] = 0x9
					
			high_num = int(binascii.hexlify(b_high),16)
			
			# Decide which number is closer to the original
			if abs(num - low_num) > abs(num - high_num):
				nearest_num = high_num
			else:
				nearest_num = low_num
			print nearest_num
			break
	
	# Send the answer
	r.send(str(nearest_num)+"\n")
	
	msg = r.recvline().strip()
	print msg
	
	
	# Exit if we see the flag
	if "flag" in msg:
		break
	
r.interactive()

Running the script gets us 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
$ python solve_final.py 
[+] Opening connection to 76.74.178.201 on port 8002: Done
md5 3d6f5e 27
[+] MBruteforcing: Found key: "aaaaaaaaaaaaaaaaaaaaaaaxQqB"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ welcome to JPS challenge, its about just printable strings! the number  +
+ n = 114800724110444 gets converted to the printable `hi all', in each   +
+ round find the suitable integer with given property caring about the    +
+ timeout of the submit of solution! all printable = string.printable  :) +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
| whats the nearest number to 61895207845396123731595648074623445905448898599139552975792268813653270992177034837318006329338741700 that gets converted to the printable string?
61895207845396123731595648074623445905448898599139552975792268813653270992177034837318006329410390281
| Correct, pass the next level :)
| whats the nearest number to 423703516379457305862443932870018303927592410127 that gets converted to the printable string?
423702877218628465373942340875780940618983702142
| Correct, pass the next level :)
...
| whats the nearest number to 374735388155597834881026 that gets converted to the printable string?
374735388155597736017534
+ Congratz! You got the flag: ASIS{jus7_simpl3_and_w4rmuP__PPC__ch41LEn93}
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$  

The flag is ASIS{jus7_simpl3_and_w4rmuP__PPC__ch41LEn93}.