Zh3r0 CTF V2 - b00tleg
- Author: qopruzjf
- Date:
- Solves: 26
Challenge
Source? Gotta comply to Indian CTF’s crypto standards
nc crypto.zh3r0.cf 1111
Solution
Unfortunately, there’s no source for this challenge. Connecting to the provided connection shows this:
Welcome to your basic cryptanalysis tutorial
There would be 8 levels, each level bringing something new on the plate.
I would be generous enough to let you encrypt any string of your choice.
Once you figure out the encryption, submit the flag to proceed to the next level.
We are then given an encrypted flag, and the option to either encrypt a message of our choosing, or submit what we think is the original unencrypted flag, like so:
Level: 1, encrypted flag: 69666d6d70217870736d6522214d667574216866752168706a6f68
[1] Encrypt
[2] Submit level flag
>>>
So basically, our job is figure out what the encryption scheme is through repeatedly encrypting different messages, and then reverse it to get the original flag.
Level 0
There were two level 1s, so we’ll call this first one level 0. After a bit of testing by sending multiple hex strings of 0s, I figured out that it just adds 1 to the value of each byte in the message. So, we just take the encrypted flag and subtract 1 from each byte to get the original flag. Here is the code to do this:
1def level0(passed):
2 print("Level 0")
3 if not passed:
4 r.recvuntil("Level: 1, encrypted flag: ")
5 ef1 = r.recvline().decode()[:-1]
6 r.recvuntil(">>> ")
7 f1 = ""
8 for i in range(0, len(ef1), 2):
9 byte = int(ef1[i:i + 2], 16)
10 byte -= 1
11 f1 += hex(byte)[2:]
12 print(f1)
13 assert len(f1) == len(ef1)
14 r.sendline("2")
15 r.recvuntil("flag in hex:")
16 r.sendline(f1)
The flag: hello world! Lets get going
Level 1
The encrypted flag for this level is just a number, rather than a hex string like the previous. So, it’s likely that the message passed in is converted to a number and then something else is done to it. Sending 00
returns 0, so I suspected that it simply converts the hex to a number. Testing with a few more messages confirms this, so we just convert the given encrypted flag into hex and send it over.
1def level1(passed):
2 print("Level 1")
3 if not passed:
4 r.recvuntil("Level: 1, encrypted flag: ")
5 ef1 = r.recvline().decode()[:-1]
6 r.recvuntil(">>> ")
7 f1 = hex(int(ef1))[2:]
8 print(f1)
9 r.sendline("2")
10 r.recvuntil("flag in hex:")
11 r.sendline(f1)
The flag: Nothing fancy, just standard bytes_to_int
Level 2
This is where the levels start requiring more work and consideration. Once again, I sent 00
first(in general, this is a good practice), and got a hex string of the same length back. Sending 01
and 02
got two more different hex strings of length 2, and the three do not differ from each other by a constant amount(mod 256, since these are 1 byte each), meaning that there isn’t just simple addition happening.
Next, I tried sending varied lengths of 00
, such as 0000
, etc.., and noticed that it got me the same hex string from before, but multiple times. It seemed like the encryption maps each individual byte to another byte, regardless of where each byte is in the message. So, we can simply construct a table for all possible 256 bytes to see what bytes map to what bytes. We can do this by encrypting each of 00
, 01
, 02
, …, ff
. We can then decrypt the encrypted flag by reversing the mapping.
1def level2(passed):
2 print("Level 2")
3 if not passed:
4 r.recvuntil("Level: 2, encrypted flag: ")
5 ef2 = r.recvline().decode()[:-1]
6 table = [""]*256
7 for i in range(256):
8 r.recvuntil(">>> ")
9 r.sendline("1")
10 r.recvuntil("message in hex:")
11 r.sendline(hex(i)[2:].zfill(2))
12 key = int(r.recvline().decode()[:-1], 16)
13 table[key] = hex(i)[2:].zfill(2)
14 print(i)
15 f2 = ""
16 for i in range(0, len(ef2), 2):
17 f2 += table[int(ef2[i:i + 2], 16)]
18 assert len(f2) == len(ef2)
19 print(f2)
20 r.sendline("2")
21 r.recvuntil("flag in hex:")
22 r.sendline(f2)
The flag: mono substitutions arent that creative
It’s also worth noting here that re-opening the connection changed the encryption results for the same messages for level 2(and the remaining levels). However, the flags stay the same, so we can just send them over to regain our progress. That’s also what the passed
argument is for; once I got the flag once, I would just send the flag over for subsequent attempts.
Level 3
Once again, I started off by sending 00
. Just like before, the encryption was a new hex string of the same length. Sending 01
and 02
also gave the same pattern as with level 2. Sending them again gave the same result, so it seemed like a mapping just like before was happening.
However, sending 0000
showed a difference from before; the second byte returned was different from the first. It seemed to be more troublesome than level 2. At this point, I suspected 2 possibilities: the whole message is mapped to some other message of the same length, or each byte position(for example, the 1st vs the 2nd 00
in 0000
) has a different mapping table.
The first option would be very difficult to brute-force to decrypt the encrypted flag, so I tried testing for the second scenario by sending 0100
. The second returned byte was the same as in 0000
, showing me that the likelihood of the second scenario was high. Testing a few more messages in a similar manner confirmed this theory.
Then the question becomes, how could I figure out the flag in a timely manner? One option is to simply create a table like with level 2, except for each byte position. However, this is something of a waste, because each table would only be used to decrypt a single byte of the encrypted flag.
So instead, I just encrypted 00
, 01
, until I got the corresponding byte in the flag for each byte position. The byte that, when encrypted, matched the one in the encrypted flag, would be the original byte in the decrypted flag. To test the encryptions for byte position x + 1
, I first put x
00
s and then the byte I wanted to encrypt(For example, 000001
, 000002
, etc.).
Notably, in the worst case this still would take (# of bytes in the encrypted flag)*256
requests, and it’s possible for the connection to close itself in this time. However, this isn’t much of an issue because the flag itself never changes, even if the encryption does. So we can just keep track of which byte position we are on and start from there until we recover the entire flag. The following code simply assumes that this issue doesn’t occur.
1def level3(passed):
2 print("Level 3")
3 if not passed:
4 r.recvuntil("Level: 3, encrypted flag: ")
5 ef3 = r.recvline().decode()[:-1]
6 f3 = ""
7 for i in range(0, len(ef3), 2):
8 byte = ef3[i:i + 2]
9 buff = "00" * (i // 2)
10 for j in range(256):
11 r.recvuntil(">>> ")
12 r.sendline("1")
13 r.recvuntil("message in hex:")
14 r.sendline(buff + hex(j)[2:].zfill(2))
15 target = r.recvline().decode()[:-1][-2:]
16 if target == byte:
17 f3 += hex(j)[2:].zfill(2)
18 break
19 print(i)
20 print("flag so far: ", f3)
21 assert len(f3) == len(ef3)
22 print(f3)
23 r.sendline("2")
24 r.recvuntil("flag in hex:")
25 r.sendline(f3)
The flag: creating different substitutions for each char
Level 4
Once again, I sent 00
first. Different from before, I got a hex string of twice the original length. Sending 0000
gave me a hex string of length 8, so it seems that the encryption does something to double the length of the original message.
Then, I sent 01
and 02
again, with similar results. It seemed like it was a similar system to level 3, since the 2-byte blocks from 0000
were different from each other. So I thought that it was just an mapping from single bytes to 2-byte blocks instead of byte to byte like before.
However, sending 00
again revealed something troublesome; it gave me a result different from before, meaning that this encryption was non-deterministic. Even so, we are supposed to be able to decrypt the encrypted flag. Combining these two facts, I realized that the encrypted flag must be one of many possible encryptions of the original flag, using the same encryption system.
Based on this, the general approach would to be find which message the encrypted flag could be a possible encryption of. In order for this to be feasibly bruteable, I figured the encryption probably didn’t map entire messages to encrypted messages, but rather encrypted byte-by-byte like previous stages.
Additionally, after more various testing and thinking, I realized that it was likely that the system was not like in level 3 where the encryption was different per byte position, and instead a single mapping table was being used for all byte positions like in level 2. The main reasons I considered this where 1) with 256*256
possible 2-bytes, I would have to send up to 256*256*(# of 2-bytes in the encrypted flag)
requests, which would take a long time considering it’s a remote connection (likely not feasible, especially with 3 levels remaining after) and 2) When encrypting longer messages like 00000000000000000000
, I noticed that occasionally the 00
bytes in different positions encrypted to the same 2-byte blocks.
With this in mind, I figured the encryption scheme was likely: Encrypt each byte to 1 of x
2-bytes, for each byte in the message. This scheme also requires for no two bytes to encrypt to any same 2-bytes(for example, 00
and 01
cannot both encrypt to 0e5f
), or else we would not be able to uniquely decrypt some messages. To test this, I sent long strings of 00
s and 01
s, and checked if they shared any 2-byte blocks. They didn’t, confirming that my idea was likely correct.
Then, the approach would be pretty much the same as level 2: Build a table finding out which 2-byte blocks each byte encrypts to. The main problem with this is doing it the same way as in level 2 naively would be rather infeasible. For one, we don’t know how many 2-byte blocks each byte individually maps to; it could be the same for all, or it could be different for all, so sending only one byte at a time doesn’t seem like a good idea, since we wouldn’t know when to move on to the next byte.
Instead, I opted to send long strings of the same byte repeated, like I had done in my previous testing. Assuming my idea was correct, I could just use each individual message to collect a bunch of the mappings at a time, rather than wait in between requests. I did this a few times, and found that bytes seemed to map to 256 2-byte blocks each, which makes a lot of sense, since 256 * 256 / 256 = 256
.
So my approach became: For each byte, send a few long messages of the byte repeated, and use it collect the possible blocks that byte maps to. Build the table this way, and then look at each 2-byte block in the encrypted flag, reverse the mapping, and get the original flag. I opted to use messages of 512 bytes, with 3 messages per byte, since that seemed to consistently find 254-256 mappings per byte, which was generally enough.
1def level4(passed):
2 print("Level 4")
3 if not passed:
4 r.recvuntil("Level: 4, encrypted flag: ")
5 ef4 = r.recvline().decode()[:-1]
6 f4 = ""
7 table = {}
8 for n in range(256):
9 for i in range(3):
10 r.recvuntil(">>> ")
11 r.sendline("1")
12 r.recvuntil("message in hex:")
13 r.sendline(hex(n)[2:].zfill(2) * 512)
14 res = r.recvline().decode()[:-1]
15 for j in range(0, len(res), 4):
16 block = res[j:j + 4]
17 if block not in table:
18 table[block] = hex(n)[2:].zfill(2)
19 for i in range(0, len(ef4), 4):
20 block = ef4[i:i + 4]
21 f4 += table[block]
22 print(f4)
23 assert len(f4) == len(ef4) // 2
24 r.sendline("2")
25 r.recvuntil("flag in hex:")
26 r.sendline(f4)
The flag: Glad that you figured out the invariant
Level 5
Same idea as before, I first tried encrypting 00
. The encrypted result was a 10-byte long hex string, like so: 19b8803e8b19b5d39995
. Additionally, encrypting 00
a second time gave a different result, meaning the scheme was non-deterministic. Encrypting 0000
also gave a 10-byte hex string, which led me to think that there is some padding going on, since the encrypted flag was long(meaning it wasn’t going to be that all encryptions result in a 10-byte long string). Encrypting a longer string of 00
s seemed to confirm this idea. It also led to another discovery; when encryting with a lot of 00
s, there seemed to be repetition in the encrypted result, like so: e92583e282e92583e2abe92583e282
.
With this in mind, I then needed to figure out what the blocksize was. We can do this by just sending messages with an extra 00
at a time and checking for when the encryption result’s length increases, and doing this, I found that the blocksize seemed to be 5 bytes. Additionally, the encryption result increased in increments of 5 bytes rather than 10, which was the initial size, meaning it was likely there was some sort of IV block of length 5 bytes. Based on this and the previous discovery of the patterns showing up with encrypting long 00
strings, I figured the scheme went something like this: generate an IV block of 5 bytes at the front, and for all subsequent blocks, do a byte-wise addition of the message bytes in the block to the IV block.
After a while of more testing with other messages(sending messages like 0102030405
, aaaaaaaaaaa
, 1020304050
, and other messages of varying lengths), I found two more things; 1) Not only was there addition, but there was also subtraction sometimes; and 2) the IV block seemed to be the very last block, rather than the first.(However, the encrypted blocks were in the same order as in the message.) There happens to be a operation commonly used in cryptography that does byte-wise addition and subtraction at times: XOR. So, my idea for the scheme at this point was: Generate and IV block, and for each 5-byte block in the message(after padding), XOR that block with the IV block, and add it to the result. After all blocks are cleared, the IV block is appended to the end. This idea would also support decryption working with non-determinism, as we can always decrypt as long as we know the IV block.
1def level5(passed):
2 print("Level 5")
3 if not passed:
4 r.recvuntil("Level: 5, encrypted flag: ")
5 ef5 = r.recvline().decode()[:-1]
6 f5 = ""
7 base = ef5[-10:]
8 for i in range(0, len(ef5) - 10, 10):
9 block = ef5[i:i + 10]
10 for j in range(0, len(block), 2):
11 diff = int(base[j:j + 2], 16) ^ int(block[j:j + 2], 16)
12 if diff in range(32, 128):
13 f5 += hex(diff)[2:].zfill(2)
14 else:
15 break
16 r.recvuntil(">>> ")
17 r.sendline("2")
18 r.recvuntil("flag in hex:")
19 r.sendline(f5)
Note that my code does byte-wise XORs, which gives the same result as block-wise XORs(since everything is bitwise in the end). This was due to the fact that I was caught up with the addition and subtraction idea for a while… and once I realized it could be XOR, I just modified that single line.
The flag: Here we append the key with your shit, please dont tell anyone
Level 6
The first thing I noticed about this level was that it seemed to take a while for it to generate the encrypted flag. Sometimes the waiting time would be short, but other times it would be longer. Additionally, the encrypted flag was a number. Encrypting 00
, 01
, 02
gave 0
, 1
, and 8
respectively. It seems like the scheme was just to cube the message after converting it to an int. So, we could just take the cube root of the flag using sage. Or so I thought. Attempting to do so resulted in sage telling me that the encrypted flag was not a perfect cube.
When considering the time that it took to generate the encrypted flag, and the fact that the encrypted flag is different on each connection, this actually makes sense. The most likely scenario here is that the encryption does cubing modulo some large prime, where the prime generation is what took time. And with a different prime, if the original message is large enough, the encryption result will be different. Then, it’s just about finding what that prime is.
This is not difficult; we can just encrypt increasingly large messages until result != message^3
. I increased the value of my messages by appending a 00
byte at the end(the same as multiplying the int value by 256). Find two messages that have result != message^3
, take the differences of message1^3 - result1
and message2^3 - result2
, find their GCD, and factor to get the prime(since the GCD is likely to be a small multiple of the prime). Then, just used sage’s nth_root
function(have the encrypted flag as an element of GF(p)
) to get the flag. I was a bit lazy with this, so instead I only used 1 message, and, figuring that a cube is quite small, just directly factored the difference message^3 - result
to get the prime. The reason this(and the previous GCD method) works is because the encryption scheme is E(x) = x^3 (mod p)
, so x^3 - E(x) = 0 (mod p)
, meaning that the difference is a multiple of p
, the prime in question.
1def level6(passed):
2 print("Level 6")
3 if not passed:
4 r.recvuntil("Level: 6, encrypted flag: ")
5 ef6= int(r.recvline().decode()[:-1])
6 print(ef6)
7 pay = "10"
8 payint = int(pay, 16)
9 r.recvuntil(">>> ")
10 r.sendline("1")
11 r.recvuntil("message in hex:")
12 r.sendline(pay)
13 res = r.recvline()[:-1].decode()
14 while payint**3 == int(res):
15 pay += "00"
16 r.recvuntil(">>> ")
17 r.sendline("1")
18 r.recvuntil("message in hex:")
19 r.sendline(pay)
20 res = r.recvline()[:-1].decode()
21 payint = int(pay, 16)
22 mod = payint**3 - int(res)
23 print(mod)
24 # factor mod to get the prime(assume it was a prime modulo), then use sage nth_root to calculate root
25 f6 = input("gimme the flag in hex> ")
26 r.recvuntil(">>> ")
27 r.sendline("2")
28 r.recvuntil("flag in hex:")
29 r.sendline(f6)
The flag: Cube modulo prime, any guesses what might be coming next?
Level 7
Same as the previous level, there is a delay before we receive the encrypted flag, and the encrypted flag differs each time. Seems like there is prime generation going on in the background again. Once again, I tried encrypting 00
, 01
, and 02
again. The former resulted in 0
and 1
again like before, but the latter resulted in a rather large number. It seems like it’s still a power though, so we can take the log base 2 of the encryption of 2 to get that power. Trying this on multiple connections showed that this power varied, seemingly between around 60 and 150. In any case, it’s not huge, so sage’s nth_root
can handle it fine.
Once again, the task is then to find the prime which is the modulo. I tried using the same semi-lazy approach as before(in retrospect, it definitely would have been simpler to just use the GCD method), which failed since multiplying a number by 256 and raising that to a high power would, as expected, result in a huge number, so the difference message^3 - result
became very difficult to factor. I opted to just increase my messages’s values by a small value at a time(multiplying by 1.1, converting to int, then to hexstring) from a base value of 256 so that the encryption result would still be small. It’s not pretty, but it worked. The rest is the same as the previous level.
1def level7(passed):
2 print("Level 7")
3 if not passed:
4 r.recvuntil("Level: 7, encrypted flag: ")
5 ef7 = int(r.recvline().decode()[:-1])
6 print(ef7)
7 r.recvuntil(">>> ")
8 r.sendline("1")
9 r.recvuntil("message in hex:")
10 r.sendline("02")
11 power = int(log(int(r.recvline()[:-1].decode()), 2))
12 print(power)
13 pay = 256
14 r.recvuntil(">>> ")
15 r.sendline("1")
16 r.recvuntil("message in hex:")
17 r.sendline("0" + hex(pay)[2:])
18 res = r.recvline()[:-1].decode()
19 while pay**power == int(res):
20 pay = int(1.1 * pay)
21 r.recvuntil(">>> ")
22 r.sendline("1")
23 r.recvuntil("message in hex:")
24 payload = hex(pay)[2:]
25 if len(payload) % 2:
26 r.sendline("0" + payload)
27 else:
28 r.sendline(payload)
29 res = r.recvline()[:-1].decode()
30 mod = pay**power - int(res)
31 print(mod)
32 # factor mod to get the prime(assume it was a prime modulo), then use sage nth_root to calculate root
33 f7 = input("gimme the flag in hex> ")
34 r.recvuntil(">>> ")
35 r.sendline("2")
36 r.recvuntil("flag in hex:")
37 r.sendline(f7)
It turns out the flag for this level is just the flag: zh3r0{17_a1n7_much_bu7_1_4m_s0m37h1ng_0f_4_cryp74n4ly57_my53lf}
Here is the full solve script(change the boolean values at the end to have the script re-do the solve for a given level):
1from pwn import *
2from math import log
3
4host, port = "crypto.zh3r0.cf", 1111
5
6r = remote(host, port)
7r.send("")
8# Level 0
9def level0(passed):
10 print("Level 0")
11 if not passed:
12 r.recvuntil("Level: 1, encrypted flag: ")
13 ef1 = r.recvline().decode()[:-1]
14 r.recvuntil(">>> ")
15 f1 = ""
16 for i in range(0, len(ef1), 2):
17 byte = int(ef1[i:i + 2], 16)
18 byte -= 1
19 f1 += hex(byte)[2:]
20 print(f1)
21 assert len(f1) == len(ef1)
22 r.sendline("2")
23 r.recvuntil("flag in hex:")
24 r.sendline(f1)
25 else:
26 r.recvuntil(">>> ")
27 r.sendline("2")
28 r.recvuntil("flag in hex:")
29 r.sendline("68656c6c6f20776f726c6421204c6574732067657420676f696e67")
30
31
32# Level 1
33def level1(passed):
34 print("Level 1")
35 if not passed:
36 r.recvuntil("Level: 1, encrypted flag: ")
37 ef1 = r.recvline().decode()[:-1]
38 r.recvuntil(">>> ")
39 f1 = hex(int(ef1))[2:]
40 print(f1)
41 r.sendline("2")
42 r.recvuntil("flag in hex:")
43 r.sendline(f1)
44 else:
45 r.recvuntil(">>> ")
46 r.sendline("2")
47 r.recvuntil("flag in hex:")
48 r.sendline("4e6f7468696e672066616e63792c206a757374207374616e646172642062797465735f746f5f696e74")
49
50# Level 2
51def level2(passed):
52 print("Level 2")
53 if not passed:
54 r.recvuntil("Level: 2, encrypted flag: ")
55 ef2 = r.recvline().decode()[:-1]
56 table = [""]*256
57 for i in range(256):
58 r.recvuntil(">>> ")
59 r.sendline("1")
60 r.recvuntil("message in hex:")
61 r.sendline(hex(i)[2:].zfill(2))
62 key = int(r.recvline().decode()[:-1], 16)
63 table[key] = hex(i)[2:].zfill(2)
64 print(i)
65 f2 = ""
66 for i in range(0, len(ef2), 2):
67 f2 += table[int(ef2[i:i + 2], 16)]
68 assert len(f2) == len(ef2)
69 print(f2)
70 r.sendline("2")
71 r.recvuntil("flag in hex:")
72 r.sendline(f2)
73 else:
74 r.recvuntil(">>> ")
75 r.sendline("2")
76 r.recvuntil("flag in hex:")
77 r.sendline("6d6f6e6f20737562737469747574696f6e73206172656e742074686174206372656174697665")
78
79# Level 3
80def level3(passed):
81 print("Level 3")
82 if not passed:
83 r.recvuntil("Level: 3, encrypted flag: ")
84 ef3 = r.recvline().decode()[:-1]
85 f3 = ""
86 for i in range(0, len(ef3), 2):
87 byte = ef3[i:i + 2]
88 buff = "00" * (i // 2)
89 for j in range(256):
90 r.recvuntil(">>> ")
91 r.sendline("1")
92 r.recvuntil("message in hex:")
93 r.sendline(buff + hex(j)[2:].zfill(2))
94 target = r.recvline().decode()[:-1][-2:]
95 if target == byte:
96 f3 += hex(j)[2:].zfill(2)
97 break
98 print(i)
99 print("flag so far: ", f3)
100 assert len(f3) == len(ef3)
101 print(f3)
102 r.sendline("2")
103 r.recvuntil("flag in hex:")
104 r.sendline(f3)
105 else:
106 r.recvuntil(">>> ")
107 r.sendline("2")
108 r.recvuntil("flag in hex:")
109 r.sendline("6372656174696e6720646966666572656e7420737562737469747574696f6e7320666f7220656163682063686172")
110
111# Level 4
112def level4(passed):
113 print("Level 4")
114 if not passed:
115 r.recvuntil("Level: 4, encrypted flag: ")
116 ef4 = r.recvline().decode()[:-1]
117 f4 = ""
118 table = {}
119 for n in range(256):
120 for i in range(3):
121 r.recvuntil(">>> ")
122 r.sendline("1")
123 r.recvuntil("message in hex:")
124 r.sendline(hex(n)[2:].zfill(2) * 512)
125 res = r.recvline().decode()[:-1]
126 for j in range(0, len(res), 4):
127 block = res[j:j + 4]
128 if block not in table:
129 table[block] = hex(n)[2:].zfill(2)
130 for i in range(0, len(ef4), 4):
131 block = ef4[i:i + 4]
132 f4 += table[block]
133 print(f4)
134 assert len(f4) == len(ef4) // 2
135 r.sendline("2")
136 r.recvuntil("flag in hex:")
137 r.sendline(f4)
138 else:
139 r.recvuntil(">>> ")
140 r.sendline("2")
141 r.recvuntil("flag in hex:")
142 r.sendline("476c6164207468617420796f752066696775726564206f75742074686520696e76617269616e74")
143
144# Level 5
145def level5(passed):
146 print("Level 5")
147 if not passed:
148 r.recvuntil("Level: 5, encrypted flag: ")
149 ef5 = r.recvline().decode()[:-1]
150 f5 = ""
151 base = ef5[-10:]
152 for i in range(0, len(ef5) - 10, 10):
153 block = ef5[i:i + 10]
154 for j in range(0, len(block), 2):
155 diff = int(base[j:j + 2], 16) ^ int(block[j:j + 2], 16)
156 if diff in range(32, 128):
157 f5 += hex(diff)[2:].zfill(2)
158 else:
159 break
160 r.recvuntil(">>> ")
161 r.sendline("2")
162 r.recvuntil("flag in hex:")
163 r.sendline(f5)
164 else:
165 r.recvuntil(">>> ")
166 r.sendline("2")
167 r.recvuntil("flag in hex:")
168 r.sendline("4865726520776520617070656e6420746865206b6579207769746820796f757220736869742c20706c6561736520646f6e742074656c6c20616e796f6e65")
169
170# Level 6
171def level6(passed):
172 print("Level 6")
173 if not passed:
174 r.recvuntil("Level: 6, encrypted flag: ")
175 ef6= int(r.recvline().decode()[:-1])
176 print(ef6)
177 pay = "10"
178 payint = int(pay, 16)
179 r.recvuntil(">>> ")
180 r.sendline("1")
181 r.recvuntil("message in hex:")
182 r.sendline(pay)
183 res = r.recvline()[:-1].decode()
184 while payint**3 == int(res):
185 pay += "00"
186 r.recvuntil(">>> ")
187 r.sendline("1")
188 r.recvuntil("message in hex:")
189 r.sendline(pay)
190 res = r.recvline()[:-1].decode()
191 payint = int(pay, 16)
192 mod = payint**3 - int(res)
193 print(mod)
194 # factor mod to get the prime(assume it was a prime modulo), then use sage nth_root to calculate root
195 f6 = input("gimme the flag in hex> ")
196 r.recvuntil(">>> ")
197 r.sendline("2")
198 r.recvuntil("flag in hex:")
199 r.sendline(f6)
200 else:
201 r.recvuntil(">>> ")
202 r.sendline("2")
203 r.recvuntil("flag in hex:")
204 r.sendline("43756265206d6f64756c6f207072696d652c20616e7920677565737365732077686174206d6967687420626520636f6d696e67206e6578743f")
205
206# Level 7
207def level7(passed):
208 print("Level 7")
209 if not passed:
210 r.recvuntil("Level: 7, encrypted flag: ")
211 ef7 = int(r.recvline().decode()[:-1])
212 print(ef7)
213 r.recvuntil(">>> ")
214 r.sendline("1")
215 r.recvuntil("message in hex:")
216 r.sendline("02")
217 power = int(log(int(r.recvline()[:-1].decode()), 2))
218 print(power)
219 pay = 256
220 r.recvuntil(">>> ")
221 r.sendline("1")
222 r.recvuntil("message in hex:")
223 r.sendline("0" + hex(pay)[2:])
224 res = r.recvline()[:-1].decode()
225 while pay**power == int(res):
226 pay = int(1.1 * pay)
227 r.recvuntil(">>> ")
228 r.sendline("1")
229 r.recvuntil("message in hex:")
230 payload = hex(pay)[2:]
231 if len(payload) % 2:
232 r.sendline("0" + payload)
233 else:
234 r.sendline(payload)
235 res = r.recvline()[:-1].decode()
236 mod = pay**power - int(res)
237 print(mod)
238 # factor mod to get the prime(assume it was a prime modulo), then use sage nth_root to calculate root
239 f7 = input("gimme the flag in hex> ")
240 r.recvuntil(">>> ")
241 r.sendline("2")
242 r.recvuntil("flag in hex:")
243 r.sendline(f7) # got the flag
244 else:
245 r.recvuntil(">>> ")
246 r.sendline("2")
247 r.recvuntil("flag in hex:")
248 r.sendline("7a683372307b31375f61316e375f6d7563685f6275375f315f346d5f73306d333768316e675f30665f345f6372797037346e346c7935375f6d7935336c667d")
249
250level0(True)
251level1(True)
252level2(True)
253level3(True)
254level4(True)
255level5(True)
256level6(True)
257level7(True)
258r.interactive()
Also, I ran sage separate from this program, so that’s something to keep in mind.
Although there was a good amount of bruting and the challenge concept is somewhat centered around guessing, I’d say the challenge wasn’t bad for people looking to solidify some basic practices in cryptanalysis. My thanks to Zh3r0CTF.