Joints 2018 Final - Crypto & Reversing

Go-Block 125

Challenge

Diberikan kode python go-block.py sebagai berikut.

#!/usr/bin/env python
import hashlib
from Crypto.Cipher import AES
import os
import sys

BLOCK_SIZE = 16
LIMIT_LEN_BAWAH = 16
LIMIT_LEN_ATAS = 48
key = os.urandom(16)
iv = os.urandom(16)

flag = open('flag.txt').read()

len_pad = lambda tes : (BLOCK_SIZE - (len(tes) % BLOCK_SIZE))
pad = lambda tes : chr(len_pad(tes))*len_pad(tes) + tes
unpad = lambda tes : tes[ord(tes[0]):]

def encrypt(plain,ivs,key):
    obj = AES.new(key,AES.MODE_CBC,ivs)
    paded_plaintext = pad(plain)
    return obj.encrypt(paded_plaintext)

def decrypts(ciphers,ivs,key):
    obj = AES.new(key,AES.MODE_CBC,ivs)
    unpad_plaintext = unpad(obj.decrypt(ciphers))
    return unpad_plaintext

if __name__ == "__main__":
    encrypted_flag = encrypt(flag,iv,key)
    decrypted_flag = decrypts(encrypted_flag,iv,key)
    sys.stdout.write('--WELCOME TO EZ CHALLENGE---\n')
    sys.stdout.write('\nIni hasil enkripsi flag-nya : ')
    sys.stdout.write(encrypted_flag.encode('hex') + '\n')
    sys.stdout.write("\nBuka Jasa Dekrip Gan\n")
    sys.stdout.flush()
    for i in range(300):
        sys.stdout.write("\nMasukin Cipher : ")
        sys.stdout.flush()
        enc = raw_input()
        try:
            enc = enc.decode('hex')
        except:
            sys.stdout.write("\nMasukin dalam bentuk hex gan\n")
            sys.stdout.flush()
            break
    sys.stdout.write("\nMasukan indeks IV yang mau diganti nilainya : ")
    sys.stdout.flush()
    IV_lamaa = raw_input()

    sys.stdout.write("\nMasukan nilai IV yang baru (hex) : ")
    sys.stdout.flush()
    IV_baruu = raw_input()
    try:
        IV_lamaa = int(IV_lamaa)
        IV_baruu = IV_baruu.decode('hex')
    except:
        sys.stdout.write("\nJangan aneh2 gan!\n")
        sys.stdout.flush()
        break
        if len(enc) > LIMIT_LEN_ATAS:
            sys.stdout.write("\nMelewati batas gan !")
            sys.stdout.flush()
            break
        elif len(enc) < LIMIT_LEN_BAWAH:
            sys.stdout.write("\nKurang gan len-nya")
            sys.stdout.flush()
            break
        else:
            iv_baru = iv[:IV_lamaa] + IV_baruu + iv[IV_lamaa+1:]
            dec_str = decrypts(enc,iv_baru,key)
            dec_str_wrap = hashlib.sha256(dec_str).hexdigest()
            sys.stdout.write("Hasil dekrip : " + dec_str_wrap + '\n')
            sys.stdout.flush()

Solution

Flag (flag.txt) dienkripsi menggunakan AES mode CBC dengan IV dan key acak (diambil dari os.urandom), lalu disimpan sebagai encrypted_flag, dan ditampilkan dalam bentuk hex.

sys.stdout.write("\nMasukan indeks IV yang mau diganti nilainya : ")
sys.stdout.flush()
IV_lamaa = raw_input()

sys.stdout.write("\nMasukan nilai IV yang baru (hex) : ")
sys.stdout.flush()
IV_baruu = raw_input()

Dari potongan kode tersebut, terlihat bahwa kita bisa memodifikasi satu karakter pada IV, dan IV tersebut digunakan untuk mendekrip encrypted_flag. Kita coba bruteforce semua kemungkinan karakter sehingga iv_baru == IV_lamaa. Catatan: buat flag palsu agar lebih mudah memahami cara kerja enkripsi.

Berikut kodenya.

#!/usr/bin/env python
# socat TCP-LISTEN:5000,reuseaddr,fork EXEC:./go-block.py
import hashlib
from Crypto.Cipher import AES
import os
import sys

BLOCK_SIZE = 16
LIMIT_LEN_BAWAH = 16
LIMIT_LEN_ATAS = 48
key = os.urandom(16)
iv = os.urandom(16)

# flag = open('flag.txt').read()
flag = 'JOINTS18{abcdefghijklmnoprstuvw}'

len_pad = lambda tes : (BLOCK_SIZE - (len(tes) % BLOCK_SIZE))
pad = lambda tes : chr(len_pad(tes))*len_pad(tes) + tes
unpad = lambda tes : tes[ord(tes[0]):]

def encrypt(plain,ivs,key):
    obj = AES.new(key,AES.MODE_CBC,ivs)
    paded_plaintext = pad(plain)
    return obj.encrypt(paded_plaintext)

def decrypts(ciphers,ivs,key):
    obj = AES.new(key,AES.MODE_CBC,ivs)
    unpad_plaintext = unpad(obj.decrypt(ciphers))
    return unpad_plaintext

if __name__ == "__main__":
    print 'iv:', repr(iv)
    encrypted_flag = encrypt(flag,iv,key)
    decrypted_flag = decrypts(encrypted_flag,iv,key)
    sys.stdout.write('--WELCOME TO EZ CHALLENGE---\n')
    sys.stdout.write('\nIni hasil enkripsi flag-nya : ')
    sys.stdout.write(encrypted_flag.encode('hex') + '\n')
    sys.stdout.write("\nBuka Jasa Dekrip Gan\n")
    sys.stdout.flush()
    for i in range(300):
        sys.stdout.write("\nMasukin Cipher : ")
        sys.stdout.flush()
        enc = raw_input()
        try:
            enc = enc.decode('hex')
        except:
            sys.stdout.write("\nMasukin dalam bentuk hex gan\n")
            sys.stdout.flush()
            break
    sys.stdout.write("\nMasukan indeks IV yang mau diganti nilainya : ")
    sys.stdout.flush()
    IV_lamaa = raw_input()

    sys.stdout.write("\nMasukan nilai IV yang baru (hex) : ")
    sys.stdout.flush()
    IV_baruu = raw_input()
    try:
        IV_lamaa = int(IV_lamaa)
        IV_baruu = IV_baruu.decode('hex')
    except:
        sys.stdout.write("\nJangan aneh2 gan!\n")
        sys.stdout.flush()
        break
        if len(enc) > LIMIT_LEN_ATAS:
            sys.stdout.write("\nMelewati batas gan !")
            sys.stdout.flush()
            break
        elif len(enc) < LIMIT_LEN_BAWAH:
            sys.stdout.write("\nKurang gan len-nya")
            sys.stdout.flush()
            break
        else:
            iv_baru = iv[:IV_lamaa] + IV_baruu + iv[IV_lamaa+1:]
            dec_str = decrypts(enc,iv_baru,key)

            print 'iv_baru:', repr(iv_baru)
            print 'dec_str:', dec_str

            dec_str_wrap = hashlib.sha256(dec_str).hexdigest()
            sys.stdout.write("Hasil dekrip : " + dec_str_wrap + '\n')
            sys.stdout.flush()
$ python go-block.py
iv: '\xe6S\xc3\xc0,\xff\x13\x7f1h[8\xa8\xf9w\x99'
--WELCOME TO EZ CHALLENGE---

Ini hasil enkripsi flag-nya : 39cb9664fc092cd11180e1f56f638305151ff6ce03608cf11144684f9206c4d42a1d768029985a27c75b4408c63a8078

Buka Jasa Dekrip Gan

Masukin Cipher : 39cb9664fc092cd11180e1f56f638305151ff6ce03608cf11144684f9206c4d42a1d768029985a27c75b4408c63a8078

Masukan indeks IV yang mau diganti nilainya : 0

Masukan nilai IV yang baru (hex) : c0
iv_baru: '\xc0S\xc3\xc0,\xff\x13\x7f1h[8\xa8\xf9w\x99'
dec_str:
Hasil dekrip : e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Masukin Cipher : 39cb9664fc092cd11180e1f56f638305151ff6ce03608cf11144684f9206c4d42a1d768029985a27c75b4408c63a8078

Masukan indeks IV yang mau diganti nilainya : 0

Masukan nilai IV yang baru (hex) : d0
iv_baru: '\xd0S\xc3\xc0,\xff\x13\x7f1h[8\xa8\xf9w\x99'
dec_str: noprstuvw}
Hasil dekrip : 3aa0a1b8aa77457f0af54bca6368adf7130dd85beceef2377d83942fbdd95c76

Masukin Cipher :

Terlihat jika IV baru yang kita masukkan jauh dari IV sebelumnya, maka variabel dec_str berisi string kosong.

hashlib.sha256('').hexdigest() == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

Jika nilai IV baru mendekati nilai IV lama, maka variabel dec_str mulai diisi dengan flag (diisi dari belakang).

Untuk solve challenge ini, ambil semua kemungkinan karakter untuk IV baru (dari 0x00 sampai 0xff) untuk dapatkan karakter IV lama yang tepat. Berikut kodenya.

Implementation

from pwn import *
import hashlib, string

p = remote('localhost', 5000)

p.recvuntil('flag-nya :')
enc = p.recvline().strip()
print enc

array = []

for i in range(256):
    p.recvuntil('Masukin Cipher : ')
    p.sendline(enc)

    p.recvuntil('yang mau diganti nilainya : ')
    p.sendline('0')

    payload = hex(i)[2:]
    if len(payload) == 1:
        payload = '0' + payload

    p.recvuntil('yang baru (hex) : ')
    p.sendline(payload)

    p.recvuntil('Hasil dekrip : ')
    wow = p.recvline().strip()
    array.append(wow)

z = '\n'.join(array)
print z

flag = ''
sp = string.printable

while 'JOINTS18' not in flag:
    for c in sp:
        pload = c + flag
        check = hashlib.sha256(pload).hexdigest()
        if check in z:
            flag = pload
            print flag
            break

p.close()

Dan jalankan.

$ python solve.py
[+] Opening connection to localhost on port 5000: Done
55031aaa5b40fa1116173f29da1df8bf205329b14180f13ca5292c1017135b56b854e55a34023ec6360031281b31093a
b3c07498e30335589c7c77755ec1f190a86b56e08d987496f7deb814a7c48690
4c38cf07f2ab2d4a9b3ba7962dd23bc4eac7a7ea04739ac9c26156c1accbda4a
68bb7d57ac71bb3fb3cc4bc1a93f66de7f780448b0c83c19a2a317539c33a8d7
1347fcf97fbdb742721c2ef7d7152555f25b35ef3fc7cfcd6dd303a2e8d3a0ee
7252b378f7597b475da57273813fb3c3f914aba9f58d83181e4c21801bb678b7
d10b36aa74a59bcf4a88185837f658afaf3646eff2bb16c3928d0e9335e945d2
da31d55feaad2618db500df2c7ccaa8275d85fa85df4007b67e27ac11954d213
b4740c98a06c28250da978252bd5b6de87fd876e0dd3be4445e3cf142e2f27e1
e94ee8976aaea89f583edd26dff11f9490844c20cad1c362c33c69821a9026b1
e7c5b8b20f55f1bbf909779285f6fb350dd79c85b910989411b623405de22e28
16267e0f935f653aada05990596ea5e1720632e84c40f5b0e14e70f3a26a5b4c
0cf11bb8fb4447627f1ebf9b036e47164410ccf92195b99ad2cd42aa6c27226f
6f221f7318b1343a5bf38ee3a9adaa17ee4f4d38a37e1abb9448503aef9e16b0
fa5704f2cb3d4768a2301c3927634ae5d9ffcfb02488cdea694cdd2d253032e1
8f9386501cf3a7f2d48cbfd4107b9db6cd03bdcc37ff12f0b6c69d513829df0f
3942703bc096c943bc1c6a057265d0a6d89e8f56b4d64d4f51d6d77e8d9171e8
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
...
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
463f448015f9bef25ea0f73a802de0c314343675efc2e0ab6bdfc3b6658c39f3
f13e3f85c50cf7173c660eeed5dc6bb94f3d379cca8309bdbdb2432af76abf79
137989cf5994c5cbf8dde2df6e78b1fd60a1fed3bfaa34257d491e85055c65d8
c15a6348320d1dff99d312b89acfba54872e8f5fccd979a3be163ffe52147479
1cd8ba08847bd07d7051c4200946c3016c613b2757fadbe1884dc00edfa42401
a83656151bc4ce2d67728d6978cec5f96a31a0279ec53df24640d5718b161fd8
5eacdcfcec38a19c416bea7275e0bfa27e26a3a9d0ccbc413d7bef35310d73d7
f749b164ce5cfe394b3d29c0b989023bf7e5b7bb8ccaec311af54d2826ca0b10
8977835389751dba682c27675e3cf957128da4e7af80c4a343715c2e3bc6684d
90a10d73f9e87e2a0b9b3dcd082039c77eead0fd3a2152d4f5a601c8668ad95f
4ac938eaccd73a3b6fd971780dd6a1c1f499edfda2a401157bc3f0bc35e185bf
d50245383f5a5acb54046a20204557089e6388d866670e6b8babf5efbfa9e840
707d4cf71df3cf61a805fe8f03aa02a0cf58a6b554b5dc81d11b883afececcb9
df3e46f3c538b696eb20bbebe81f85623c8f0dc4ec77bd9728598c3b4e912a16
ae1a6315f57e71461534d3b6dc0ceae44846b48ecb2f286a4c014e6950faa483
6e2800aeae0ac06ca204a9dc4f34c9a5272340b8cea9463768df0b863a8d0e39
0cc20b4105bda6f2250d23266ca12cf6bb32a2f119d1a3fed039f95facbb15a2
423dd1c20add77bf42cf31744c977dc7d1016215d726e02a98c578b1065cd191
76427140fb07186e935270b56c2f7dee04484b4676c8b52003484a609c766fb0
8d420c2cdf2963c977b3143a4a9aed266283684cf7295bccdd91697b398f8c9c
78350640060e7fe73db86c515365e300cd7a9550243a62d50b76311c0e8ed60d
4ba4217c65d6f82e71f3c1a4f4a7e9ba46a3636b31c74f47dbb5299a894db263
333b81eefd0bce4173acc77ae9d1258f8beaf1a7c3664d9fc30843a9879d793e
a8aeeaaa09c4894343c8e017fc2c3e08ade3c931eb867c4e27a6681a880d64ef
2ede224557e2d6a121cf185745140d3cfba99fde8a07db366f45fef9113edc40
915471c5624e3558de67bc75e661e66c5ae318cdcde78d26308ec733f58f8095
8b1c9f8efdba4f90b1c1efb16f041a55caf0c1bdc23b44f4b98f117ec2638cfb
9e64a80d3b45c198927a1dcc75125d55320e8b30d724dba790135f0afa1986f8
7b957596a1e5146ffe90c3b03d85aaa3be1cd5a8af5c1664a2847bcfaf5ec4d2
3036f7e9767a6f6dd6e7e8f171e6e1e76089d1ad04d6f243cf1c0266129d944b
0504a40b89b045ba2090d1c982d3e2290621987ffa17724d57e5632904c61871
7067bd162acdcb26f50d9eeb917335f353a3a94b36115aed09b7d11e71378865
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
...
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
}
!}
s!}
ps!}
1ps!}
C1ps!}
_C1ps!}
z_C1ps!}
iz_C1ps!}
ziz_C1ps!}
yziz_C1ps!}
lyziz_C1ps!}
4lyziz_C1ps!}
n4lyziz_C1ps!}
an4lyziz_C1ps!}
_an4lyziz_C1ps!}
Z_an4lyziz_C1ps!}
EZ_an4lyziz_C1ps!}
_EZ_an4lyziz_C1ps!}
y_EZ_an4lyziz_C1ps!}
ry_EZ_an4lyziz_C1ps!}
3ry_EZ_an4lyziz_C1ps!}
v3ry_EZ_an4lyziz_C1ps!}
{v3ry_EZ_an4lyziz_C1ps!}
8{v3ry_EZ_an4lyziz_C1ps!}
18{v3ry_EZ_an4lyziz_C1ps!}
S18{v3ry_EZ_an4lyziz_C1ps!}
TS18{v3ry_EZ_an4lyziz_C1ps!}
NTS18{v3ry_EZ_an4lyziz_C1ps!}
INTS18{v3ry_EZ_an4lyziz_C1ps!}
OINTS18{v3ry_EZ_an4lyziz_C1ps!}
JOINTS18{v3ry_EZ_an4lyziz_C1ps!}
[*] Closed connection to localhost port 5000

Flag

JOINTS18{v3ry_EZ_an4lyziz_C1ps!}

Kopi 75

Challenge

Diberikan file executable 64-bit yang meminta kita memasukkan password yang valid.

$ ./kopi
[+] Kapal air  kopi
[+] Paswordnya : deomkicer
[-] N00B

Solution

Gunakan IDA untuk melihat disassembly file, lalu lihat fungsi ngebell, cek_kiwo, dan cek_tengen dari program tersebut.

__int64 ngebell()
{
  signed int v1; // [sp+8h] [bp-8h]@1

  puts("[+] Kapal air  kopi");
  printf("[+] Paswordnya : ");
  __isoc99_scanf("%s", password);
  v1 = strlen(password);
  for ( i = 0; v1 / 2 > i; ++i )
    flag_kiwo[i] = password[i];
  flag_kiwo[i] = 0;
  i = v1 / 2;
  k = 0;
  while ( v1 >= i )
    flag_tengen[k++] = password[i++];
  cek_kiwo();
  cek_tengen();
  puts("[+] G00D Sangad!!!!!");
  puts("[+] Dapet flag nih");
  printf("[+] Flag : JOINTS18{\%s}\n", password);
  return 0LL;
}
__int64 cek_kiwo()
{
  __int64 result; // rax@7
  __int64 v1; // rcx@7
  int i; // [sp+Ch] [bp-74h]@1
  int v3; // [sp+10h] [bp-70h]@1
  int v4; // [sp+14h] [bp-6Ch]@1
  int v5; // [sp+18h] [bp-68h]@1
  int v6; // [sp+1Ch] [bp-64h]@1
  int v7; // [sp+20h] [bp-60h]@1
  int v8; // [sp+24h] [bp-5Ch]@1
  int v9; // [sp+28h] [bp-58h]@1
  int v10; // [sp+2Ch] [bp-54h]@1
  int v11; // [sp+30h] [bp-50h]@1
  int v12; // [sp+34h] [bp-4Ch]@1
  int v13; // [sp+38h] [bp-48h]@1
  int v14; // [sp+3Ch] [bp-44h]@1
  int v15; // [sp+40h] [bp-40h]@1
  int v16; // [sp+44h] [bp-3Ch]@1
  int v17; // [sp+48h] [bp-38h]@1
  int v18; // [sp+4Ch] [bp-34h]@1
  int v19; // [sp+50h] [bp-30h]@1
  int v20; // [sp+54h] [bp-2Ch]@1
  int v21; // [sp+58h] [bp-28h]@1
  int v22; // [sp+5Ch] [bp-24h]@1
  int v23; // [sp+60h] [bp-20h]@1
  __int64 v24; // [sp+68h] [bp-18h]@1

  v24 = *MK_FP(__FS__, 40LL);
  v3 = 34;
  v4 = 28;
  v5 = 8;
  v6 = 29;
  v7 = 54;
  v8 = 45;
  v9 = 0;
  v10 = 5;
  v11 = 8;
  v12 = 2;
  v13 = 6;
  v14 = 7;
  v15 = 0;
  v16 = 54;
  v17 = 39;
  v18 = 12;
  v19 = 2;
  v20 = 54;
  v21 = 27;
  v22 = 8;
  v23 = 54;
  for ( i = 0; i < strlen(flag_kiwo); ++i )
  {
    if ( (char)(flag_kiwo[i] ^ 0x69) != *(&v3 + i) )
    {
      puts("[-] N00B");
      exit(0);
    }
  }
  result = 0LL;
  v1 = *MK_FP(__FS__, 40LL) ^ v24;
  return result;
}
__int64 cek_tengen()
{
  __int64 result; // rax@7
  __int64 v1; // rsi@7
  int i; // [sp+Ch] [bp-74h]@1
  int v3; // [sp+10h] [bp-70h]@1
  int v4; // [sp+14h] [bp-6Ch]@1
  int v5; // [sp+18h] [bp-68h]@1
  int v6; // [sp+1Ch] [bp-64h]@1
  int v7; // [sp+20h] [bp-60h]@1
  int v8; // [sp+24h] [bp-5Ch]@1
  int v9; // [sp+28h] [bp-58h]@1
  int v10; // [sp+2Ch] [bp-54h]@1
  int v11; // [sp+30h] [bp-50h]@1
  int v12; // [sp+34h] [bp-4Ch]@1
  int v13; // [sp+38h] [bp-48h]@1
  int v14; // [sp+3Ch] [bp-44h]@1
  int v15; // [sp+40h] [bp-40h]@1
  int v16; // [sp+44h] [bp-3Ch]@1
  int v17; // [sp+48h] [bp-38h]@1
  int v18; // [sp+4Ch] [bp-34h]@1
  int v19; // [sp+50h] [bp-30h]@1
  int v20; // [sp+54h] [bp-2Ch]@1
  int v21; // [sp+58h] [bp-28h]@1
  int v22; // [sp+5Ch] [bp-24h]@1
  int v23; // [sp+60h] [bp-20h]@1
  __int64 v24; // [sp+68h] [bp-18h]@1

  v24 = *MK_FP(__FS__, 40LL);
  v3 = 0;
  v4 = 0;
  v5 = 0;
  v6 = 0;
  v7 = 0;
  v8 = 0;
  v9 = 0;
  v10 = 24;
  v11 = 8;
  v12 = 5;
  v13 = 8;
  v14 = 9;
  v15 = 8;
  v16 = 51;
  v17 = 17;
  v18 = 43;
  v19 = 12;
  v20 = 111;
  v21 = 66;
  v22 = 17;
  v23 = 54;
  for ( i = 0; i < strlen(flag_tengen); ++i )
  {
    if ( (char)(flag_kiwo[i] ^ flag_tengen[i]) != *(&v3 + i) )
    {
      puts("[-] N00B");
      exit(0);
    }
  }
  result = 0LL;
  v1 = *MK_FP(__FS__, 40LL) ^ v24;
  return result;
}

Pada fungsi ngebell, inputan kita dibagi menjadi dua bagian yang sama besar disimpan di array flag_kiwo (bagian kiri) dan flag_tengen (bagian kanan). Pada fungsi cek_kiwo, setiap nilai pada flag_kiwo dixor dengan 0x69, dan dibandingkan dengan nilai pada pointer v3. Begitu juga fungsi cek_tengen, setiap nilai pada flag_kiwo dixor dengan flag_tengen, lalu dibandingkan dengan nilai pada pointer v3.

Inputan kita harus melewati semua pengecekan tersebut tanpa masuk ke kondisi dimana fungsi exit berada. Berikut kode untuk mendapatkan password yang tepat.

Implementation

kiwo = [34, 28, 8, 29, 54, 45, 0, 5, 8, 2, 6, 7, 0, 54, 39, 12, 2, 54, 27, 8, 54]
# print(len(kiwo))
tengen = [0, 0, 0, 0, 0, 0, 0, 24, 8, 5, 8, 9, 8, 51, 17, 43, 12, 111, 66, 17, 54]
# print(len(tengen))

kiri = ''
kanan = ''
flag = []

for i in kiwo:
  flag.append(i ^ 0x69)
  kiri += chr(i ^ 0x69)

for j in range(len(tengen)):
  kanan += chr(flag[j] ^ tengen[j])

print('Flag: JOINTS18{\%s}' % (kiri+kanan))

Dan jalankan.

$ python3 kopi.py
Flag: JOINTS18{Kuat_Dilakoni_Nek_ra_Kuat_Ditinggal_Ng00pi}

Flag

JOINTS18{Kuat_Dilakoni_Nek_ra_Kuat_Ditinggal_Ng00pi}