This page has been machine-translated from the original page.
I participated in the SECCON CTF 2023 qualifying round (held from 9/16 to 9/17) as part of the team 0nePadding.
Individually, I solved 2 Rev challenges, finishing 35th domestically and 87th overall.
The top 10 domestic teams that qualify for the SECCON national finals are still a high wall to clear, but I intend to keep grinding.
Since reviewing the unsolved problems will take time, I’ll write up the ones I actually solved for now.
Table of Contents
jumpout(Rev)
Sequential execution
This was the Warmup Rev challenge of the contest.
In terms of difficulty of obtaining the flag it was relatively easy, but it was an extremely interesting problem that gave me the strong impression that the author is deeply familiar with binaries and debuggers. (I’d love to see the source code for this — how was it even made…?)
The binary itself appeared to be a simple program that just validates a flag received as input, so I analyzed it with Ghidra.
Following the usual approach of analyzing the binary from entry and identifying the main function, I could see that it was calling something using the text FLAG:, but the Listing window output was corrupted and the decompilation was not working correctly.
However, looking at the function referenced in main at address 0x1011d0, while the decompilation was still broken, I could tell that the (probably) input characters on the stack were being passed to a check function. (The check function name was one I renamed myself.)
The check function looked like this:
undefined8 check(char *param_1)
{
long lVar1;
size_t sVar2;
ulong uVar3;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
sVar2 = strlen(param_1);
if (sVar2 == 0x1d) {
do {
uVar3 = 0;
do {
FUN_00101360(param_1[uVar3],uVar3 & 0xffffffff);
uVar3 = uVar3 + 1;
} while (uVar3 != 0x1d);
} while( true );
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 1;
}From this code, we can see that the correct flag length is 0x1d.
We can also see that it calls a function called FUN_00101360 with a single input character and its index as arguments.
FUN_00101360 is a function like the following — it returns the XOR of four values: one input character, its index, 0x55, and (&DAT_00104010)[index].
uint FUN_00101360(uint param_1,uint param_2)
{
long in_FS_OFFSET;
if (*(long *)(in_FS_OFFSET + 0x28) != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail((long)(int)param_2,param_2,7);
}
return param_1 ^ param_2 ^ 0x55 ^ (uint)(byte)(&DAT_00104010)[(int)param_2];
}However, as you can see from the decompilation of check we looked at earlier, there is no code in the decompiled output that handles the return value of FUN_00101360.
My hypothesis was that it probably compares the returned value against some hardcoded value to perform verification, but that function’s decompilation also appeared broken and was not in a state where the correct behavior could be read.
Since that wasn’t working, I read the assembly of the check function and found that it was loading data from DAT_00104030, which seemed likely to be used later.
There was some guesswork involved, but I created the following solver using that data as the comparison target and was able to obtain the flag.
d = [ 0xf6, 0xf5, 0x31, 0xc8, 0x81, 0x15, 0x14, 0x68, 0xf6, 0x35, 0xe5, 0x3e, 0x82, 0x09, 0xca, 0xf1, 0x8a, 0xa9, 0xdf, 0xdf, 0x33, 0x2a, 0x6d, 0x81, 0xf5, 0xa6, 0x85, 0xdf, 0x17 ]
e = [ 0xf0, 0xe4, 0x25, 0xdd, 0x9f, 0x0b, 0x3c, 0x50, 0xde, 0x04, 0xca, 0x3f, 0xaf, 0x30, 0xf3, 0xc7, 0xaa, 0xb2, 0xfd, 0xef, 0x17, 0x18, 0x57, 0xb4, 0xd0, 0x8f, 0xb8, 0xf4, 0x23, 0x00 ]
for i in range(0x1d):
print(chr(0x55^ i ^ d[i] ^ e[i]), end="")During the contest I reached the above solver smoothly, but looking back at it afterward, the binary was quite difficult to read and seemed to assume dynamic analysis.
That said, the broad strokes don’t change — once you’ve identified the check function, following the detailed behavior with a debugger will lead you to the same conclusion.
Sickle(Rev)
Pickle infected with COVID-19
Due to my limited skill, Rev challenges requiring careful manual reading like this one take me a long time, and I only barely managed to submit the flag right at the very end of the contest.
The following script was given as the challenge binary:
import pickle, io
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
f = io.BytesIO(payload)
res = pickle.load(f)
if isinstance(res, bool) and res:
print("Congratulations!!")
else:
print("Nope")Running this, I found that the hardcoded binary is executed during pickle.load(f) deserialization, and it performs flag string prompting and validation.
I had not used the pickle library before, but it is a module that allows serialization and deserialization of Python objects.
Furthermore, when deserializing with the pickle.load method, it actually runs internally as a VM, giving it the ability to execute arbitrary code.
In this challenge, loading the series of binary data defined as payload triggers the logic that accepts a flag input and validates it.
References:
To analyze it, the payload must first be disassembled.
Disassembly can be performed using pickletools.dis().
import pickletools
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
pickletools.dis(payload, annotate=True)Reference: pickletools --- Tools for pickle developers — Python 3.11.5 documentation
However, because a STOP instruction is present partway through, pickletools can only read up to offset 0x105.
Moreover, even if you specify the payload range with a slice, pickletools will fail to interpret the POP and GET instructions and throw an error, causing disassembly to fail.
I therefore searched for another tool that could disassemble this payload and found r2pickledec, a radare2 plugin.
Reference: doyensec/r2pickledec: Pickle decompiler plugin for Radare2
This tool had many rough edges and various issues arose both during and after installation, but by first building the latest radare2 from source and then installing the plugin, I was able to successfully disassemble the entire payload.
The disassembly output with my analysis comments added is as follows:
0x000 8c086275696c. short_binunicode "builtins" ; 0x2
0x00a 8c0767657461. short_binunicode "getattr" ; 0xc
0x013 93 stack_global
0x014 94 memoize
0x015 32 dup
memo0: getattr()
// getattr()
// getattr()
0x016 8c086275696c. short_binunicode "builtins" ; 0x18
0x020 8c05696e7075. short_binunicode "input" ; 0x22 ; 2'"'
0x027 93 stack_global
0x028 8c06464c4147. short_binunicode "FLAG> " ; 0x2a ; 2'*'
0x030 85 tuple1
0x031 52 reduce
// getattr()
// getattr()
// input("FLAG> ")
0x032 8c06656e636f. short_binunicode "encode" ; 0x34 ; 2'4'
0x03a 86 tuple2
0x03b 52 reduce
0x03c 29 empty_tuple
0x03d 52 reduce
0x03e 94 memoize
0x03f 30 pop
memo0: getattr()
memo1: Flag
0x040 67300a get "0" ; 0x41 ; 2'A'
0x043 8c086275696c. short_binunicode "builtins" ; 0x45 ; 2'E'
0x04d 8c0464696374 short_binunicode "dict" ; 0x4f ; 2'O'
0x053 93 stack_global
0x054 8c03676574 short_binunicode "get" ; 0x56 ; 2'V'
0x059 86 tuple2
0x05a 52 reduce
0x05b 8c086275696c. short_binunicode "builtins" ; 0x5d ; 2']'
0x065 8c07676c6f62. short_binunicode "globals" ; 0x67 ; 2'g'
0x06e 93 stack_global
0x06f 29 empty_tuple
0x070 52 reduce
0x071 8c0166 short_binunicode "f" ; 0x73 ; 2's'
0x074 86 tuple2
0x075 52 reduce
0x076 8c047365656b short_binunicode "seek" ; 0x78 ; 2'x'
0x07c 86 tuple2
0x07d 52 reduce
// dict.get(builtins.globals(), f).seek()
0x07e 94 memoize
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
0x07f 67300a get "0" ; 0x80
0x082 8c086275696c. short_binunicode "builtins" ; 0x84
0x08c 8c03696e74 short_binunicode "int" ; 0x8e
0x091 93 stack_global
// getattr()
// builtins.int()
// __add__
0x092 8c075f5f6164. short_binunicode "__add__" ; 0x94
0x09b 86 tuple2
// getattr()
// (builtins.int(), __add__)
0x09c 52 reduce
0x09d 94 memoize
0x09e 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
memo3: add()
0x09f 67300a get "0" ; 0xa0
0x0a2 8c086275696c. short_binunicode "builtins" ; 0xa4
0x0ac 8c03696e74 short_binunicode "int" ; 0xae
0x0b1 93 stack_global
0x0b2 8c075f5f6d75. short_binunicode "__mul__" ; 0xb4
0x0bb 86 tuple2
0x0bc 52 reduce
0x0bd 94 memoize
0x0be 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
memo3: add()
memo4: mul()
0x0bf 67300a get "0" ; 0xc0
0x0c2 8c086275696c. short_binunicode "builtins" ; 0xc4
0x0cc 8c03696e74 short_binunicode "int" ; 0xce
0x0d1 93 stack_global
0x0d2 8c065f5f6571. short_binunicode "__eq__" ; 0xd4
0x0da 86 tuple2
0x0db 52 reduce
0x0dc 94 memoize
0x0dd 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
0x0de 67330a get "3" ; 0xdf
0x0e1 67350a get "5" ; 0xe2
0x0e4 8c086275696c. short_binunicode "builtins" ; 0xe6
0x0ee 8c036c656e short_binunicode "len" ; 0xf0
0x0f3 93 stack_global
0x0f4 67310a get "1" ; 0xf5
0x0f7 85 tuple1
0x0f8 52 reduce
// add()
// eq()
// builtins.len(flag)
0x0f9 4d4000 binint2 0x40 ; '@'
0x0fc 86 tuple2
// add()
// eq()
// (builtins.len(flag), 0x40)
0x0fd 52 reduce
0x0fe 4d0501 binint2 0x105
0x101 86 tuple2
// add()
// (bool, 0x015)
0x102 52 reduce
// add(bool, 0x105)
0x103 85 tuple1
// jmp
// 0x106
0x104 52 reduce
0x105 2e stop -> Fail
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
0x106 30 pop
0x107 67300a get "0" ; 0x108
0x10a 67310a get "1" ; 0x10b
0x10d 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x10f
// getattr()
// Flag
// __getitem__
0x11a 86 tuple2
0x11b 52 reduce
// getattr(Flag, __getitem__)
0x11c 94 memoize
0x11d 30 pop
0x11e 4d0000 binint2 0x0
0x121 94 memoize
# loop start
0x122 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0
0x123 67320a get "2" ; 0x124
0x126 67330a get "3" ; 0x127
0x129 67300a get "0" ; 0x12a
0x12c 67360a get "6" ; 0x12d
0x12f 67370a get "7" ; 0x130
// jmp
// add()
// getattr()
0x132 85 tuple1
0x133 52 reduce
// jmp
// add()
// getattr()
// Flag[0], __le__
0x134 8c065f5f6c65. short_binunicode "__le__" ; 0x136
0x13c 86 tuple2
0x13d 52 reduce
// jmp
// add()
// getattr((Flag[0], __le__))
0x13e 4d7f00 binint2 0x7f ; '\x7f'
0x141 85 tuple1
0x142 52 reduce
// jmp
// add()
// (Flag[0] <= 0x7f)
// 0x14a
0x143 4d4a01 binint2 0x14a
0x146 86 tuple2
0x147 52 reduce
0x148 85 tuple1
0x149 52 reduce
0x14a 2e stop
// jmp
// add()
// (Flag[0] <= 0x7f)
// 0x14a
0x14b 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0
0x14c 67320a get "2" ; 0x14d
0x14f 67330a get "3" ; 0x150
0x152 67340a get "4" ; 0x153
0x155 67350a get "5" ; 0x156
0x158 67330a get "3" ; 0x159
0x15b 67370a get "7" ; 0x15c
0x15e 4d0100 binint2 0x1
0x161 86 tuple2
0x162 52 reduce
0x163 70370a put "7" ; 0x164
// jmp
// add()
// mul()
// eq()
// 1
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 1
0x166 4d4000 binint2 0x40 ; '@'
0x169 86 tuple2
// jmp
// add()
// mul()
// eq()
// (1, 0x40)
0x16a 52 reduce
0x16b 4d5500 binint2 0x55 ; 'U'
0x16e 86 tuple2
0x16f 52 reduce
// jmp
// add()
// mul((eq(1, 0x40), 0x55))
0x170 4d2201 binint2 0x122
0x173 86 tuple2
// jmp
// add()
// (mul((eq(1, 0x40), 0x55)), 0x122)
# Loop until i reaches 0x40
# Verifies all characters are in the ASCII range
0x174 52 reduce
0x175 85 tuple1
0x176 52 reduce
// jmp
// add((mul((eq(1, 0x40), 0x55)), 0x122))
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0x40
0x177 30 pop
0x178 67300a get "0" ; 0x179
0x17b 67300a get "0" ; 0x17c
0x17e 5d empty_list
0x17f 94 memoize
// getattr()
// getattr()
// empty_list
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0x40
memo8: []
0x180 8c0661707065. short_binunicode "append" ; 0x182
0x188 86 tuple2
0x189 52 reduce
// getattr()
// getattr((empty_list, append))
0x18a 94 memoize
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0x40
memo8: []
memo9: append to memo8
0x18b 30 pop
// getattr()
0x18c 67380a get "8" ; 0x18d
0x18f 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x191
0x19c 86 tuple2
0x19d 52 reduce
0x19e 94 memoize
// getattr([], __getitem__)
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0x40
memo8: []
memo9: append to memo8
memo10: fetch value from memo8 by index
0x19f 30 pop
0x1a0 67300a get "0" ; 0x1a1
0x1a3 8c086275696c. short_binunicode "builtins" ; 0x1a5
0x1ad 8c03696e74 short_binunicode "int" ; 0x1af
0x1b2 93 stack_global
0x1b3 8c0a66726f6d. short_binunicode "from_bytes" ; 0x1b5
0x1bf 86 tuple2
0x1c0 52 reduce
// getattr(int, from_bytes)
0x1c1 94 memoize
0x1c2 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0x40
memo8: []
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
0x1c3 4d0000 binint2 0x0
0x1c6 70370a put "7" ; 0x1c7
# loop
0x1c9 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0
memo8: []
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
0x1ca 67390a get "9" ; 0x1cb
0x1cd 6731310a get "11" ; 0x1ce
0x1d1 67360a get "6" ; 0x1d2
0x1d4 8c086275696c. short_binunicode "builtins" ; 0x1d6
0x1de 8c05736c6963. short_binunicode "slice" ; 0x1e0
0x1e5 93 stack_global
0x1e6 67340a get "4" ; 0x1e7
0x1e9 67370a get "7" ; 0x1ea
0x1ec 4d0800 binint2 0x8
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul()
// i = 0
// 0x8
0x1ef 86 tuple2
0x1f0 52 reduce
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul(i, 0x8)
0x1f1 67340a get "4" ; 0x1f2
0x1f4 67330a get "3" ; 0x1f5
0x1f7 67370a get "7" ; 0x1f8
0x1fa 4d0100 binint2 0x1
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul(i, 0x8)
// mul()
// add()
// i = 0
// 1
0x1fd 86 tuple2
0x1fe 52 reduce
0x1ff 4d0800 binint2 0x8
0x202 86 tuple2
0x203 52 reduce
0x204 86 tuple2
0x205 52 reduce
0x206 85 tuple1
// m8.append()
// int.from_bytes()
// flag[x]
// slice[mul(i, 0x8), mul(add(i + 1), 8)]
0x207 52 reduce
// m8.append()
// int.from_bytes()
// flag[i*8:(i+1)*8]
0x208 8c066c697474. short_binunicode "little" ; 0x20a
0x210 86 tuple2
0x211 52 reduce
// m8.append()
// int.from_bytes(flag[i*8:(i+1)*8], little)
0x212 85 tuple1
0x213 52 reduce
0x214 30 pop
# Extract 8 bytes of flag at a time, convert to int, and append to m8
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0
memo8: []
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
0x215 67320a get "2" ; 0x216
0x218 67330a get "3" ; 0x219
0x21b 67340a get "4" ; 0x21c
0x21e 67350a get "5" ; 0x21f
0x221 67330a get "3" ; 0x222
0x224 67370a get "7" ; 0x225
0x227 4d0100 binint2 0x1
0x22a 86 tuple2
0x22b 52 reduce
// jmp
// add()
// mul()
// eq()
// add(i + 1)
0x22c 70370a put "7" ; 0x22d
0x22f 4d0800 binint2 0x8
0x232 86 tuple2
0x233 52 reduce
0x234 4d7700 binint2 0x77 ; 'w'
0x237 86 tuple2
0x238 52 reduce
0x239 4dc901 binint2 0x1c9
0x23c 86 tuple2
0x23d 52 reduce
# Overwrite i
// jmp
// add(mul(eq(add(i + 1), 0x8), 0x77), 0x1c9)
0x23e 85 tuple1
0x23f 52 reduce
# Extract 8 bytes of flag at a time, convert to int, and append to m8
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 8
memo8: [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
# If i == 8, exit the loop
0x240 30 pop
0x241 67300a get "0" ; 0x242
0x244 5d empty_list
0x245 94 memoize
// getattr()
// empty_list
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 8
memo8: arr1 [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
memo12: arr2 []
0x246 8c0661707065. short_binunicode "append" ; 0x248
0x24e 86 tuple2
0x24f 52 reduce
// getattr(empty_list, append)
0x250 94 memoize
0x251 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 8
memo8: arr1 [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
memo12: arr2 []
memo13: append to memo12
0x252 67300a get "0" ; 0x253
0x255 6731320a get "12" ; 0x256
0x259 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x25b
0x266 86 tuple2
0x267 52 reduce
0x268 94 memoize
0x269 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 8
memo8: arr1 [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
memo12: arr2 []
memo13: append to memo12
memo14: fetch value from memo12 by index
0x26a 67300a get "0" ; 0x26b
0x26d 8c086275696c. short_binunicode "builtins" ; 0x26f
0x277 8c03696e74 short_binunicode "int" ; 0x279
0x27c 93 stack_global
0x27d 8c075f5f786f. short_binunicode "__xor__" ; 0x27f
0x286 86 tuple2
0x287 52 reduce
// getattr(int, xor)
0x288 94 memoize
0x289 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 8
memo8: arr1 [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
memo12: arr2 []
memo13: append to memo12
memo14: fetch value from memo12 by index
memo15: int.xor()
0x28a 493132343434. int "1244422970072434993" ; 0x28b
0x29f 94 memoize
0x2a0 30 pop
0x2a1 4d0000 binint2 0x0
0x2a4 70370a put "7" ; 0x2a5
0x2a7 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> likely fetches input character
memo7: i = 0
memo8: arr1 [<flag 8 bytes each, little-endian>]
memo9: append to memo8
memo10: fetch value from memo8 by index
memo11: int.from_bytes()
memo12: arr2 []
memo13: append to memo12
memo14: fetch value from memo12 by index
memo15: int.xor()
memo16: key = 1244422970072434993
0x2a8 6731330a get "13" ; 0x2a9
0x2ac 8c086275696c. short_binunicode "builtins" ; 0x2ae
0x2b6 8c03706f77 short_binunicode "pow" ; 0x2b8
0x2bb 93 stack_global
// arr2.append()
// pow
0x2bc 6731350a get "15" ; 0x2bd
0x2c0 6731300a get "10" ; 0x2c1
0x2c4 67370a get "7" ; 0x2c5
0x2c7 85 tuple1
0x2c8 52 reduce
0x2c9 6731360a get "16" ; 0x2ca ; "16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01"
0x2cd 86 tuple2
0x2ce 52 reduce
// arr2.append()
// pow()
// xor(arr1[i], 1244422970072434993))
0x2cf 493635353337. int "65537" ; 0x2d0
0x2d6 493138343436. int "18446744073709551557" ; 0x2d7
0x2ec 87 tuple3
0x2ed 52 reduce
// arr2.append()
// pow(
(xor(arr1[i], 1244422970072434993), 65537, 18446744073709551557)
)
0x2ee 85 tuple1
0x2ef 52 reduce
0x2f0 30 pop
# Append result to memo12
0x2f1 6731340a get "14" ; 0x2f2 ; "14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01"
0x2f5 67370a get "7" ; 0x2f6
0x2f8 85 tuple1
// fetch value from memo12 by index
// i = 0
0x2f9 52 reduce
0x2fa 7031360a put "16" ; 0x2fb
0x2fe 30 pop
# Use the result of the previous encryption as the next XOR key
0x2ff 67320a get "2" ; 0x300
0x302 67330a get "3" ; 0x303
0x305 67340a get "4" ; 0x306
0x308 67350a get "5" ; 0x309
0x30b 67330a get "3" ; 0x30c
0x30e 67370a get "7" ; 0x30f
0x311 4d0100 binint2 0x1
0x314 86 tuple2
0x315 52 reduce
0x316 70370a put "7" ; 0x317
0x319 4d0800 binint2 0x8
0x31c 86 tuple2
0x31d 52 reduce
0x31e 4d8300 binint2 0x83
0x321 86 tuple2
0x322 52 reduce
0x323 4da702 binint2 0x2a7
0x326 86 tuple2
0x327 52 reduce
0x328 85 tuple1
0x329 52 reduce
0x32a 30 pop
0x32b 67300a get "0" ; 0x32c
0x32e 6731320a get "12" ; 0x32f
0x332 8c065f5f6571. short_binunicode "__eq__" ; 0x334 ; "__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x33a 86 tuple2
0x33b 52 reduce
0x33c 28 mark
0x33d 493832313533. int "8215359690687096682" ; 0x33e ; "8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x352 493138363236. int "1862662588367509514" ; 0x353 ; "1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x367 493833353037. int "8350772864914849965" ; 0x368 ; "8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x37c 493131363136. int "11616510986494699232" ; 0x37d ; "11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x392 493337313136. int "3711648467207374797" ; 0x393 ; "3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x3a7 493937323231. int "9722127090168848805" ; 0x3a8 ; "9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x3bc 493136373830. int "16780197523811627561" ; 0x3bd ; "16780197523811627561\nI18138828537077112905\nl\x85R."
0x3d2 493138313338. int "18138828537077112905" ; 0x3d3
0x3e8 6c list
0x3e9 85 tuple1
0x3ea 52 reduce
0x3eb 2e stopDeserialization via pickle operates as a VM.
The basic operations are only four: “push a value onto the stack”, “tuple-ify one or two stack values”, “execute code using two stack values (a callable tuple and an argument tuple)”, and “save a stack value into memo”.
As noted in my analysis comments above, by patiently working through the pattern of “push data → tupleify → call function” from top to bottom, I was ultimately able to identify that the code performs the following operations:
- Split the 64-character flag into 8-byte chunks, convert each chunk to an int in little-endian byte order, and store them in an array.
-
Process each of the 8 int values with the following formula. (The initial key is 1244422970072434993; for subsequent iterations, the key is the result of the previous encryption.)
pow((xor(arr1[i], key), 65537, 18446744073709551557)) - If the correct flag is entered, the computed results will match the hardcoded long values.
From the above, if we can decrypt the plaintext from the 8 hardcoded long values, we can obtain the flag.
The challenge here was how to determine the private key.
From the encryption logic, it is clearly RSA encryption.
However, the private key is not given, and 18446744073709551557 (acting as n) is not a product of primes, so I could not factor it into p and q.
After consulting with a team member, I got the spot-on suggestion to try pow(e, -1, n) as the private key for decryption.
Based on this idea, I created the following solver and successfully decrypted the ciphertext to obtain the correct flag.
from Crypto.Util.number import long_to_bytes
ans = [
8215359690687096682,
1862662588367509514,
8350772864914849965,
11616510986494699232,
3711648467207374797,
9722127090168848805,
16780197523811627561,
18138828537077112905
]
key = 1244422970072434993
e = 65537
n = 18446744073709551557
d = pow(65537, -1, 18446744073709551556)
flag = []
for i in range(1, 8):
m = pow(ans[i], d, n)
flag.append((m ^ ans[i-1]).to_bytes(8,'little'))
# SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}Incidentally, reading other people’s writeups, I found that using a tool called flickling yields a decompiled result like the following.
This is overwhelmingly easier to read, and I found myself wondering what all my hard work had been for.
_var0 = input('FLAG> ')
_var1 = getattr(_var0, 'encode')
_var2 = _var1()
_var3 = getattr(dict, 'get')
_var4 = globals()
_var5 = _var3(_var4, 'f')
_var6 = getattr(_var5, 'seek')
_var7 = getattr(int, '__add__')
_var8 = getattr(int, '__mul__')
_var9 = getattr(int, '__eq__')
_var10 = len(_var2)
_var11 = _var9(_var10, 64)
_var12 = _var7(_var11, 261)
_var13 = _var6(_var12)
_var14 = getattr(_var2, '__getitem__')
_var15 = _var14(0)
_var16 = getattr(_var15, '__le__')
_var17 = _var16(127)
_var18 = _var7(_var17, 330)
_var19 = _var6(_var18)
_var20 = _var7(0, 1)
_var21 = _var9(_var20, 64)
_var22 = _var8(_var21, 85)
_var23 = _var7(_var22, 290)
_var24 = _var6(_var23)
_var25 = getattr([], 'append')
_var26 = getattr([], '__getitem__')
_var27 = getattr(int, 'from_bytes')
_var28 = _var8(0, 8)
_var29 = _var7(0, 1)
_var30 = _var8(_var29, 8)
_var31 = slice(_var28, _var30)
_var32 = _var14(_var31)
_var33 = _var27(_var32, 'little')
_var34 = _var25(_var33)
_var35 = _var7(0, 1)
_var36 = _var9(_var35, 8)
_var37 = _var8(_var36, 119)
_var38 = _var7(_var37, 457)
_var39 = _var6(_var38)
_var40 = getattr([], 'append')
_var41 = getattr([], '__getitem__')
_var42 = getattr(int, '__xor__')
_var43 = _var26(0)
_var44 = _var42(_var43, 1244422970072434993)
_var45 = pow(_var44, 65537, 18446744073709551557)
_var46 = _var40(_var45)
_var47 = _var41(0)
_var48 = _var7(0, 1)
_var49 = _var9(_var48, 8)
_var50 = _var8(_var49, 131)
_var51 = _var7(_var50, 679)
_var52 = _var6(_var51)
_var53 = getattr([], '__eq__')
_var54 = _var53([8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905])
result0 = _var54Reference: SECCON CTF 2023 Quals Writeup - Qiita
That said, working through the hard-to-read assembly did feel like a small level-up, so I’ll call it a win.
Summary
35th domestically — the road to the national finals is still long, but I’ll keep grinding.