2021 Google CTF - ADSPAM

Thu, Sep 2, 2021 5-minute read

題目:ADSPAM

ADSPAM

Challenge Info

We’ve intercepted this demo build of a new ad spam bot, see if you can find anything interesting.

adspam.2021.ctfcompetition.com 1337

app-release.apk

Solution

實際執行只看到一條訊息:

apk 總之先反編譯(APKLab 還不錯用)

a/a/e.java 發現有用到 socket

tcpdump 聽封包

先搞好一台 root VM,ARM 架構的話,這邊可以直接下載,我的是 i386 只好自己編:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

export TCPDUMP=4.9.2
export LIBPCAP=1.9.0

wget http://www.tcpdump.org/release/tcpdump-$TCPDUMP.tar.gz
wget http://www.tcpdump.org/release/libpcap-$LIBPCAP.tar.gz

tar zxvf tcpdump-$TCPDUMP.tar.gz
tar zxvf libpcap-$LIBPCAP.tar.gz

export CC=i686-linux-gnu-gcc
cd libpcap-$LIBPCAP
./configure --host=x86-linux --with-pcap=linux
make -j
cd ..

cd tcpdump-$TCPDUMP
export ac_cv_linux_vers=2
export CFLAGS=-static
export CPPFLAGS=-static
export LDFLAGS=-static

./configure --host=x86-linux
make -j

cp tcpdump-$TCPDUMP/tcpdump ../

編好之後丟進 VM

1
adb push tcpdump /system/xbin/tcpdump

聽封包:

1
tcpdump -vv -i any -s 0 -w /data/local/tmp/dump.pcap

發現會跟 adspam.2021.ctfcompetition.com 1337 溝通,並且送了很 base64 的東西出去,但解出來不是明文:

靜態分析

ad/spam/NativeAdapter.java 看到有 load libnative-lib.so 進來,所以把 librart 丟 IDA:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package ad.spam;

public class NativeAdapter {
    static {
        System.loadLibrary("native-lib");
    }

    public static native byte[] declicstr(byte[] bArr);

    public static native byte[] decrypt(byte[] bArr);

    public static native byte[] encrypt(byte[] bArr);

    public static native boolean oktorun();

    public static native String transform(byte[] bArr);
}

順帶一提 load library 的方法有兩種,用第二種的話就要自己處理架構問題:

1
2
System.loadLibrary("calc");
System.load("lib/armeabi/libcalc.so")

但無論使用哪種方式,library load 進來之後會呼叫 JNI_OnLoad,並且要使用 library function 的話,宣告方式如下:

1
public native String natvieFunction(int var0);

而 function linking 有分兩種方式:

1. Dynamic Linking:使用 JNI Native Method Name Resolving

比如說 class com.android.interesting.Stuff 有個 function

1
public native String doThingsInNativeLibrary(int var0);

在 library 內的名稱要設定為 Java_com_android_interesting_Stuff_doThingsInNativeLibrary

2. Static Linking:使用 RegisterNatives API

API 長這樣,需要 class instance, function name, function pointer 還有 JNI Type Signatures

1
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);

實際逆向的時候 JNI Functions 會是 JNIEnv + offset 的方式呈現,不過 IDA 支援度很好,把 type 修正回 JNIEnv* 就可以了

找到相關 function 之後逆一逆,這邊就簡單說明結果:

  • rick_decode_445A0:用 'cFUgdW9ZIGV2aUcgYW5ub0cgcmV2ZU4=' 做 xor decode(base64 解碼後是倒過來的 rick roll pU uoY eviG annoG reveN = =)
  • declicstr:拿來 RSA pubkey 來解密
  • decryptAES/ECB/PKCS5Padding 解密
  • encryptAES/ECB/PKCS5Padding 加密
  • oktorun:沒認真看,可能是 anti-debug,檢查有沒有用 frida.server 之類的
  • transform:把 byte array 拿去 rick_decode_445A0

Java 加解密用 python 實作實在不習慣 QQ,這邊簡單紀錄一下

declicstr()

1
2
3
4
5
6
7
8
byte[] declicstr(byte[] enc_lic) {
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedPubKey);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PublicKey RSA_pubkey = keyFactory.generatePublic(keySpec);
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.DECRYPT_MODE, RSA_pubkey);
    return cipher.doFinal(enc_lic);
}

轉換成:

1
2
3
def declicstr(encodedPubKey):
    key = RSA.import_key(encodedPubKey)
    res = pow(int.from_bytes(secret, 'big'), key.e, key.n)

如此一來能解密(AES)封包內容了,發送的內容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "name": "1337_hacker",
  "is_admin": 0,
  "device_info": {
    "os_version": "3.10.0+",
    "api_level": 24,
    "device": "generic_x86"
  },
  "license": "QIknTsIjeUEF9yJjeZ\\/kPPfTlSm8vzMU4LWjzfSXvN+OSqBu3iNgZJgeW7fc8oltH9MprO9nI8vxgsjO\\/VA4t7YuNm16a7elPVAHqD4dXtzngnZPpsbek3Rc\\/We\\/WQ5YxXHgUt7YJ6tcd4wH3fhduC9tl\\/E5elwJL\\/YAcbD4mT8=::o9kjqYWCBKMgodl1JvDiscUeRjh9Ip9HcC7tHskoYqNQfAPE0XvSAKBSOFgleNHzVY9BVkfxmutgn\\/kVXUs3yl\\/qAurc4jokg0eA\\/v3flnnkWxqTOh4vv0yfr7PGXqwHk4qUFK1SldZ4VsLhd8PAb0aHj22E5b4U5jeJ16z187E=::gpDbCb0BmUZfdKVIZgF08lQ80K9SeUsRadZG+UUjE7wI1NRZ1evLk2GQ3sqskGHFKlPg8cTR2Xy69WedNu4QLboOWm\\/w13ocOvHwCoiQ1ZdmibgnhMQBznqpjpBnL083YMRYskcUX68R2PFaXY3taV7MoG1DyQWFRfdr\\/CnLyS8=::ZBLhwMu0DbgpUANm2ukYldrppJERiH1Tgp02CRB5I4dDP8n4+ZCv33ScspELtgAKHhiwIVksQVsnwDLsQRi6nqq9nrIwqSHMR0TwOe6UKTpAegbH53FXtriopPHfLuI2M45SzJ88GFjXy7wfOOjwDYe4KKO9KU8+LGD15Au73EM=::Hygv+bTtsnI9IBf44GkvoF38r3g5zBB7uyYT7PTlbjhCdgYRwRayutI3vY+n66xM7GOFgUFVIBI5+OBDnvazLNttjGomPED\\/OXlImndWvrZxYcaKaE3vYGPezorV0xwPahGGq\\/DWafPKdYxLxwICq1GXKYNAckCZIqfpGbJRRwg=::GARMZAX7fQN7i7Wnp4J6HxMTLe9+VM\\/wGJs+zN6b9IOmynh2gIkGjmssfOA9KdYydqBLEOJymayH8HeyrtInhhQNR3el8A5n8GMEMkyF1gUFAiSEPyhNeWWOj2IAHGNNwccmF7QywdfOUGjsTNFbrW6Yl5QLLAmMbA95qF0IERk=::YWlx8Cok1x\\/3ZsW9JKIsKj9UpBaCNkXSPiVXUrNX1IDZE0B8iNr3iliOr90TW0BvsIaFEwvDTlcESXJ8kLc3iZq0fm1lgujfM7Z156VdxEPjr9LplcEZ9ZVhYGNtVyGIRcouUDJHu3FVfXQ1XesaNlNHOb50hADprsw3RnTAGbU=::I3dsx2vSfXxZ1\\/QlMbwYPRFEZBtOuB8qLEY8cqFVtYjMluNWSkbHAYB+kwCBEv3yuoOjkdQEfqq4pS+K0ka1+pFDyss8sSbV3OiZdpRf40SS\\/pZxw2duJr9uDd1DdX8mST7fdjqj0V1a2ZBMpqaEI2gFlCwzXlfZBC47LKNiM+8=::ow7r5VJMGfSf0odNKxzBpUtSJdj8gHdt+Z7Xu54MAdsnUParSjrtRI4yJYzcW4toOFmDdSs5SERR289yohYI5hHSWLElv\\/44O+g4M08F5qpwCmOp5otW32qRG1RnhqR95evH44nOyK24UnpvWlebNwVhniSu4A7znjluGRrao\\/U=::TeGqGWv8ZmsY\\/rFq1puW9N+01TWTKJm8qzUuY\\/7JUCPDJ1AR6Y3XsPb73FuSVHPL63sjiuCTiKTRSUDzBE0VBfo59rtOKI05k64Jrz88nODD7BiK7ssacsOr2dAFGQKgBaWV2jitSAdxtCmh9sDpYsfs0\\/vXBBfVLqfVZDfAVGQ=::Al3QWY+nNFoLezt+rSdbWmqp7iZ+rR9pnM35IJNZ63bLQeM3CUvULVczhrM3toXNLCY7xmAT4jg+u0uDAjanaKMB+T1Tmym7aaCqwCfHYVFn5nw+tw54e13CLxj7OO+e847+XH8DtK\\/BiA+n03vPnt\\/cEDPvIM59sPsjHThJvpk=::VOGr60qxiO1r0YlKnrIWbQu7UhBmtBeNw2NDQnoNU3H1mjVEs\\/ji3AYuEGc2HGKINByq7Mpb4mWKD2oH5ii\\/UZDpxbzCFlJrjvjEG25c9Hhf2fiQHvRXmJd8iA8YdffBii3csCjaydLFSX6Vn7XPg+\\/PF\\/TdM1zUiLTJZX4LXRw=::ELL9maLDpdmmEgaT76qtw9IugtaQX2r7V7QVqMKXQcbwq7o0dvaO3+yMt6m5K5Milm4JSNwX\\/810YUaoAsHNuaIavuLRsxbP3b6KnKxaKz3EDgyhye2en3U1EZouiLljBB0bKz8rAtyGdolWDdNoKjvLhv7x2edc05HQZOt3aiA=::"
}

server 回傳資料:

1
2
3
4
{
  "cmd": -1,
  "data": "Extra data: line 1 column 2417 (char 2416)"
}

看來 data 是顯示通知時的訊息,嘗試改 name, is_admin 會回傳這類訊息,網址應該是隨機的:

1
2
3
4
{
  "cmd": 2,
  "data": "https://www.youtube.com/watch?v=6mNzitWIiuc"
}

程式核心邏輯在 ad/spam/MainActivity.javaa/a/f.java,可以知道要把 res/law/lic 每行取出來 base64 decode,然後 declicstr

res/raw/lic 長這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
QIknTsIjeUEF9yJjeZ/kPPfTlSm8vzMU4LWjzfSXvN+OSqBu3iNgZJgeW7fc8oltH9MprO9nI8vxgsjO/VA4t7YuNm16a7elPVAHqD4dXtzngnZPpsbek3Rc/We/WQ5YxXHgUt7YJ6tcd4wH3fhduC9tl/E5elwJL/YAcbD4mT8=
o9kjqYWCBKMgodl1JvDiscUeRjh9Ip9HcC7tHskoYqNQfAPE0XvSAKBSOFgleNHzVY9BVkfxmutgn/kVXUs3yl/qAurc4jokg0eA/v3flnnkWxqTOh4vv0yfr7PGXqwHk4qUFK1SldZ4VsLhd8PAb0aHj22E5b4U5jeJ16z187E=
gpDbCb0BmUZfdKVIZgF08lQ80K9SeUsRadZG+UUjE7wI1NRZ1evLk2GQ3sqskGHFKlPg8cTR2Xy69WedNu4QLboOWm/w13ocOvHwCoiQ1ZdmibgnhMQBznqpjpBnL083YMRYskcUX68R2PFaXY3taV7MoG1DyQWFRfdr/CnLyS8=
ZBLhwMu0DbgpUANm2ukYldrppJERiH1Tgp02CRB5I4dDP8n4+ZCv33ScspELtgAKHhiwIVksQVsnwDLsQRi6nqq9nrIwqSHMR0TwOe6UKTpAegbH53FXtriopPHfLuI2M45SzJ88GFjXy7wfOOjwDYe4KKO9KU8+LGD15Au73EM=
Hygv+bTtsnI9IBf44GkvoF38r3g5zBB7uyYT7PTlbjhCdgYRwRayutI3vY+n66xM7GOFgUFVIBI5+OBDnvazLNttjGomPED/OXlImndWvrZxYcaKaE3vYGPezorV0xwPahGGq/DWafPKdYxLxwICq1GXKYNAckCZIqfpGbJRRwg=
GARMZAX7fQN7i7Wnp4J6HxMTLe9+VM/wGJs+zN6b9IOmynh2gIkGjmssfOA9KdYydqBLEOJymayH8HeyrtInhhQNR3el8A5n8GMEMkyF1gUFAiSEPyhNeWWOj2IAHGNNwccmF7QywdfOUGjsTNFbrW6Yl5QLLAmMbA95qF0IERk=
YWlx8Cok1x/3ZsW9JKIsKj9UpBaCNkXSPiVXUrNX1IDZE0B8iNr3iliOr90TW0BvsIaFEwvDTlcESXJ8kLc3iZq0fm1lgujfM7Z156VdxEPjr9LplcEZ9ZVhYGNtVyGIRcouUDJHu3FVfXQ1XesaNlNHOb50hADprsw3RnTAGbU=
I3dsx2vSfXxZ1/QlMbwYPRFEZBtOuB8qLEY8cqFVtYjMluNWSkbHAYB+kwCBEv3yuoOjkdQEfqq4pS+K0ka1+pFDyss8sSbV3OiZdpRf40SS/pZxw2duJr9uDd1DdX8mST7fdjqj0V1a2ZBMpqaEI2gFlCwzXlfZBC47LKNiM+8=
ow7r5VJMGfSf0odNKxzBpUtSJdj8gHdt+Z7Xu54MAdsnUParSjrtRI4yJYzcW4toOFmDdSs5SERR289yohYI5hHSWLElv/44O+g4M08F5qpwCmOp5otW32qRG1RnhqR95evH44nOyK24UnpvWlebNwVhniSu4A7znjluGRrao/U=
TeGqGWv8ZmsY/rFq1puW9N+01TWTKJm8qzUuY/7JUCPDJ1AR6Y3XsPb73FuSVHPL63sjiuCTiKTRSUDzBE0VBfo59rtOKI05k64Jrz88nODD7BiK7ssacsOr2dAFGQKgBaWV2jitSAdxtCmh9sDpYsfs0/vXBBfVLqfVZDfAVGQ=
Al3QWY+nNFoLezt+rSdbWmqp7iZ+rR9pnM35IJNZ63bLQeM3CUvULVczhrM3toXNLCY7xmAT4jg+u0uDAjanaKMB+T1Tmym7aaCqwCfHYVFn5nw+tw54e13CLxj7OO+e847+XH8DtK/BiA+n03vPnt/cEDPvIM59sPsjHThJvpk=
VOGr60qxiO1r0YlKnrIWbQu7UhBmtBeNw2NDQnoNU3H1mjVEs/ji3AYuEGc2HGKINByq7Mpb4mWKD2oH5ii/UZDpxbzCFlJrjvjEG25c9Hhf2fiQHvRXmJd8iA8YdffBii3csCjaydLFSX6Vn7XPg+/PF/TdM1zUiLTJZX4LXRw=
ELL9maLDpdmmEgaT76qtw9IugtaQX2r7V7QVqMKXQcbwq7o0dvaO3+yMt6m5K5Milm4JSNwX/810YUaoAsHNuaIavuLRsxbP3b6KnKxaKz3EDgyhye2en3U1EZouiLljBB0bKz8rAtyGdolWDdNoKjvLhv7x2edc05HQZOt3aiA=

每行 license 會解成 4 bytes 資料,串起來長這樣:

1
b'\x0b1337_hacker$798b7dd4-d171-11eb-5149-1fa59603ced5\x010\x00'

這邊感覺要用通靈的?總之發現它是由長度跟資料組成:

1
2
3
4
5
[+] Decrypted lic: b'\x0b1337_hacker$798b7dd4-d171-11eb-5149-1fa59603ced5\x010\x00'
[+] Block: 11, b'1337_hacker'
[+] Block: 36, b'798b7dd4-d171-11eb-5149-1fa59603ced5'
[+] Block:  1, b'0'
[+] Terminate

看起來三組分別代表 name, 某個 ID 跟 is_admin,所以能利用現有的 license 片段把 is_admin 的值改成非 0

好像要黑箱測試 server 規則,在嘗試時有收到這個回傳訊息,所以能確定 is_admin 要是一個十進位數字

1
b'{"cmd": -1, "data": "invalid literal for int() with base 10: b\'49596059605960596059605960596059605960596059605\\\\x010\'"}'

我最後構造出來的 license 長這樣,讓第三個 block 是十進位數字就好,後面有沒有符合格式好像不影響,甚至內容跟 payload 的資料不一樣也沒關係,頗奇妙

1
2
3
4
5
6
[+] Decrypted lic: b'\x0b1337_hacker$798b7dd4-d171-11eb-5149-1fa59603ced51495960596059605960596059605960596059605960596059605\x010\x00'
[+] Block: 11, b'1337_hacker'
[+] Block: 36, b'798b7dd4-d171-11eb-5149-1fa59603ced5'
[+] Block: 49, b'4959605960596059605960596059605960596059605960596'
[+] Block: 48, b'5\x010\x00'
[+] Terminate

最後的腳本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES
from base64 import b64decode
from base64 import b64encode
from pwn import *
from binascii import unhexlify

encodedPubKey = b'0\x81\x9f0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x81\x8d\x000\x81\x89\x02\x81\x81\x00\xb0\x01\xbf1\xdb\xc6\xa2G\xca\xcc\xa8\xd2yU\x07 \xa0\xbf\x93WC^FU,e\xe56\xceqz\xb6\t\x9dJ\xaf\xc7\x9f\xfd\x19\xe4dD\x84kv\x136\x8e\xd6\xbfG\x86q3i\x1f5\xea\xb8\xe9\xe56\x88"\xee\xccs\xc4\x00\xd8\'"\xa8\xf3\x96\xd2!\xfe\xfeET7\xfaKY\xb0d\\\xeb]\x1eB\xdd\x97\xeb\xa8\x18+7DR\xaf\xc4\x1a\xc1\x9b\xb4Y\xb0\xd0-\xbc\xd5\xe9;|\xfbP\xcc\xe8\xa8\xaeM\xdd\x06\xc1w\x02\x03\x01\x00\x01'
keyPub = RSA.import_key(encodedPubKey)
AES_key = "eaW~IFhnvlIoneLl"
license = [
    "QIknTsIjeUEF9yJjeZ/kPPfTlSm8vzMU4LWjzfSXvN+OSqBu3iNgZJgeW7fc8oltH9MprO9nI8vxgsjO/VA4t7YuNm16a7elPVAHqD4dXtzngnZPpsbek3Rc/We/WQ5YxXHgUt7YJ6tcd4wH3fhduC9tl/E5elwJL/YAcbD4mT8=",
    "o9kjqYWCBKMgodl1JvDiscUeRjh9Ip9HcC7tHskoYqNQfAPE0XvSAKBSOFgleNHzVY9BVkfxmutgn/kVXUs3yl/qAurc4jokg0eA/v3flnnkWxqTOh4vv0yfr7PGXqwHk4qUFK1SldZ4VsLhd8PAb0aHj22E5b4U5jeJ16z187E=",
    "gpDbCb0BmUZfdKVIZgF08lQ80K9SeUsRadZG+UUjE7wI1NRZ1evLk2GQ3sqskGHFKlPg8cTR2Xy69WedNu4QLboOWm/w13ocOvHwCoiQ1ZdmibgnhMQBznqpjpBnL083YMRYskcUX68R2PFaXY3taV7MoG1DyQWFRfdr/CnLyS8=",
    "ZBLhwMu0DbgpUANm2ukYldrppJERiH1Tgp02CRB5I4dDP8n4+ZCv33ScspELtgAKHhiwIVksQVsnwDLsQRi6nqq9nrIwqSHMR0TwOe6UKTpAegbH53FXtriopPHfLuI2M45SzJ88GFjXy7wfOOjwDYe4KKO9KU8+LGD15Au73EM=",
    "Hygv+bTtsnI9IBf44GkvoF38r3g5zBB7uyYT7PTlbjhCdgYRwRayutI3vY+n66xM7GOFgUFVIBI5+OBDnvazLNttjGomPED/OXlImndWvrZxYcaKaE3vYGPezorV0xwPahGGq/DWafPKdYxLxwICq1GXKYNAckCZIqfpGbJRRwg=",
    "GARMZAX7fQN7i7Wnp4J6HxMTLe9+VM/wGJs+zN6b9IOmynh2gIkGjmssfOA9KdYydqBLEOJymayH8HeyrtInhhQNR3el8A5n8GMEMkyF1gUFAiSEPyhNeWWOj2IAHGNNwccmF7QywdfOUGjsTNFbrW6Yl5QLLAmMbA95qF0IERk=",
    "YWlx8Cok1x/3ZsW9JKIsKj9UpBaCNkXSPiVXUrNX1IDZE0B8iNr3iliOr90TW0BvsIaFEwvDTlcESXJ8kLc3iZq0fm1lgujfM7Z156VdxEPjr9LplcEZ9ZVhYGNtVyGIRcouUDJHu3FVfXQ1XesaNlNHOb50hADprsw3RnTAGbU=",
    "I3dsx2vSfXxZ1/QlMbwYPRFEZBtOuB8qLEY8cqFVtYjMluNWSkbHAYB+kwCBEv3yuoOjkdQEfqq4pS+K0ka1+pFDyss8sSbV3OiZdpRf40SS/pZxw2duJr9uDd1DdX8mST7fdjqj0V1a2ZBMpqaEI2gFlCwzXlfZBC47LKNiM+8=",
    "ow7r5VJMGfSf0odNKxzBpUtSJdj8gHdt+Z7Xu54MAdsnUParSjrtRI4yJYzcW4toOFmDdSs5SERR289yohYI5hHSWLElv/44O+g4M08F5qpwCmOp5otW32qRG1RnhqR95evH44nOyK24UnpvWlebNwVhniSu4A7znjluGRrao/U=",
    "TeGqGWv8ZmsY/rFq1puW9N+01TWTKJm8qzUuY/7JUCPDJ1AR6Y3XsPb73FuSVHPL63sjiuCTiKTRSUDzBE0VBfo59rtOKI05k64Jrz88nODD7BiK7ssacsOr2dAFGQKgBaWV2jitSAdxtCmh9sDpYsfs0/vXBBfVLqfVZDfAVGQ=",
    "Al3QWY+nNFoLezt+rSdbWmqp7iZ+rR9pnM35IJNZ63bLQeM3CUvULVczhrM3toXNLCY7xmAT4jg+u0uDAjanaKMB+T1Tmym7aaCqwCfHYVFn5nw+tw54e13CLxj7OO+e847+XH8DtK/BiA+n03vPnt/cEDPvIM59sPsjHThJvpk=",
    "VOGr60qxiO1r0YlKnrIWbQu7UhBmtBeNw2NDQnoNU3H1mjVEs/ji3AYuEGc2HGKINByq7Mpb4mWKD2oH5ii/UZDpxbzCFlJrjvjEG25c9Hhf2fiQHvRXmJd8iA8YdffBii3csCjaydLFSX6Vn7XPg+/PF/TdM1zUiLTJZX4LXRw=",
    "ELL9maLDpdmmEgaT76qtw9IugtaQX2r7V7QVqMKXQcbwq7o0dvaO3+yMt6m5K5Milm4JSNwX/810YUaoAsHNuaIavuLRsxbP3b6KnKxaKz3EDgyhye2en3U1EZouiLljBB0bKz8rAtyGdolWDdNoKjvLhv7x2edc05HQZOt3aiA="]
new_lic_idx = list(map(lambda _: _[0], enumerate(license)))
payload_format = '{{"name":"{name}","is_admin":{is_admin},"device_info":{{"os_version":"3.10.0+","api_level":24,"device":"generic_x86"}},"license":"{license}"}}'


def declicstr(key, secret):
    res = pow(int.from_bytes(secret, 'big'), key.e, key.n)
    return unhexlify(hex(res)[2:].rjust(8, '0'))


BLOCK_SIZE = 16  # Bytes
def pad(s): return s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
    chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)


def unpad(s): return s[:-ord(s[len(s) - 1:])]


def decrypt(key, enc):
    enc = b64decode(enc)
    cipher = AES.new(key.encode(), AES.MODE_ECB)
    return unpad(cipher.decrypt(enc))


def encrypt(key, raw):
    raw = pad(raw)
    cipher = AES.new(key.encode(), AES.MODE_ECB)
    return b64encode(cipher.encrypt(raw.encode()))


def send_payload(payload):
    payload = encrypt(AES_key, payload)
    r = remote('adspam.2021.ctfcompetition.com', 1337)
    r.sendlineafter('== proof-of-work: disabled ==\n', payload)
    recv = r.recvline()
    r.close()
    return decrypt(AES_key, recv)


def show_lic(license):
    full_lic = b''
    for lic in license:
        full_lic += declicstr(keyPub, b64decode(lic))

    print(f"[+] Decrypted lic: {full_lic}")
    while len(full_lic) > 0 and full_lic[0] != 0:
        size = full_lic[0]
        data = full_lic[1:size+1]
        full_lic = full_lic[size+1:]
        print(f"[+] Block: {str(size).rjust(2, ' ')}, {data}")
    print(f"[+] Terminate")


show_lic(license)
new_lic_idx = new_lic_idx[:-1] + [8] + [10] * (0x31 >> 2) + new_lic_idx[-1:]
new_lic_list = list(map(lambda _: license[_], new_lic_idx))
new_lic = ''.join(map(lambda _: _ + "::", new_lic_list))
payload = payload_format.format(
    name='1337_hacker', is_admin='0', license=new_lic)
show_lic(new_lic_list)

recv = send_payload(payload)
print(recv)

Flag

CTF{n0w_u_kn0w_h0w_n0t_t0_l1c3n53_ur_b0t}

意外的沒說到很難

Reference