2023年第一届长城杯信息安全铁人三项赛决赛WP(CTF部分题解)

这次长城杯的线下决赛的CTF几题质量还行,这里保存了几题赛后复现一下

1 aesweblog

题目提供了一个访问日志 access_access.log 和 数据库插入的数据的生成方式(也就是 flag 的加密方式)代码 initsecretandflagdata.py

#initsecretandflagdata.py
import sqlite3
import os
import random
import base64
from Crypto.Cipher import AES  
from Crypto.Util.Padding import pad, unpad  
import hashlib  

def md5_hash(input_rawstring):  
    # 创建一个md5 hash对象  
    m = hashlib.md5()  
      
    # 更新hash对象  
    m.update(input_rawstring)  
      
    # 获取散列值的16进制字符串表示(32个字节长度)
    hexdigest =  m.hexdigest()
    #将16进制字符串转换为16字节的字节串
    byte_digest = bytes.fromhex(hexdigest)
    return byte_digest
    #return (hex_dig)

def genpassphrase(username,number):
    passphrase = ""
    accvalue = 0
    
    # 这里省略了计算随机数种子的部分代码。其余代码都是okay的。

    random.seed(accvalue)
    total = random.randint(4,7)
    for i in range(total):
        str4bytes = base64.b64encode(os.urandom(3))
        passphrase = passphrase + str4bytes.decode("utf-8")
    
    return passphrase

def genflagstr(username,number):
    flagstr = f"flag{number}" + "{"
    
    accvalue = 0
    # 这里省略了计算随机数种子的部分代码。其余代码都是okay的。
    
    random.seed(accvalue)
    
    total = random.randint(7,15)
    base64str = base64.b64encode(os.urandom(total))

    return base64str

# 迭代次数,增加这个值会提高安全性,但也会降低性能  
iterations = 10000  
  
# 密钥长度(可以是16、24或32字节,对应AES-128、AES-192或AES-256)  
key_length = 16  
  
# 简化版的PBKDF2实现,这里仅使用HMAC-SHA256作为伪随机函数  
def pbkdf2(passphrase, salt, iterations, key_length):  
    h = hashlib.sha256()  
    u = passphrase.encode("utf-8") + salt  
    for _ in range(iterations):  
        h.update(u)  
        u = h.digest()
      
    return h.digest()[:key_length]  

conn = sqlite3.connect('customers.db')
cursor = conn.cursor()

# 先清除现有数据。
cursor.execute("delete from secrets")
conn.commit()

cursor.execute("delete from user_flag")
conn.commit()

sqlcmd = "SELECT username FROM customers"
cursor.execute(sqlcmd)
rows = cursor.fetchall()
usernames = [row[0] for row in rows]
count = len(usernames)
for username in usernames:
    passphrase = genpassphrase(username, count)

    insertsecretcmd = f"insert into secrets(username,passphrase) values('{username}','{passphrase}')"
    print(insertsecretcmd)
    
    flagname  = f"flag{count}"

    # AES加密用的salt从passphrase派生
    salt = md5_hash(passphrase.encode("utf-8") + b"saltseed")
    # 使用passphrase和salt通过PBKDF2派生密钥
    key = pbkdf2(passphrase, salt, iterations, key_length)
    # 初始化向量也从passphrase派生
    iv =  md5_hash(passphrase.encode("utf-8") + b"IVseed") 

    # 创建Cipher对象
    cipher = AES.new(key, AES.MODE_CBC, iv)  

    # 需要加密的数据
    original_flagdata = genflagstr(username,count)

    # 对数据进行填充,以符合AES的块大小(128位/16字节)
    padded_flagdata = pad(original_flagdata, AES.block_size)  

    # 加密数据 
    encrypted_flagdata = cipher.encrypt(padded_flagdata)

    # 使用相同的salt和passphrase通过PBKDF2派生密钥  
    key_decrypt = pbkdf2(passphrase, salt, iterations, key_length)  
  
    # 创建解密器对象  
    decipher = AES.new(key_decrypt, AES.MODE_CBC, iv)  
  
    # 解密数据  
    decrypted_padded_data = decipher.decrypt(encrypted_flagdata)  
  
    # 去除填充  
    decrypted_flagdata = unpad(decrypted_padded_data, AES.block_size)
    #decrypted_flagdatastr = decrypted_flagdata.decode("utf-8")
    if  original_flagdata == decrypted_flagdata:
        print(f"\r\n   congrutulations: {original_flagdata} matchs {decrypted_flagdata}\r\n")  
    
    flagvalue = base64.b64encode(encrypted_flagdata)
    base64flagdata = flagvalue.decode("utf-8")
    notestr = "encrypted with aes128. associated key, iv & salt are derived from passphrase."
    
    insertuser_flagcmd = f"insert into user_flag(username,flagname,flagvalue,note) values('{username}','{flagname}','{base64flagdata}','{notestr}')"
    print(insertuser_flagcmd)
    print("")
    count = count - 1
    
    cursor.execute(insertsecretcmd)
    cursor.execute(insertuser_flagcmd)
    conn.commit()

cursor.close()
conn.commit()
conn.close()

访问日志太大,就不贴了,上面是题目的脚本

首先第一步肯定是将日志文件进行清洗,观察下日志,其中包含了 400403404 等错误响应码,我们只需要 200 的日志即可,清洗脚本如下

import urllib.parse
import re

accesslogs = open('access_access.log','r').readlines()

washed = open('washed.log','a+');

errorline = re.compile('172.21.0.2:80 172.21.0.1 - - \[.*\].* (40[0348]|500)',re.I);

for line in accesslogs:
    if len(errorline.findall(line)) > 0:
        continue;
    
    washed.write(urllib.parse.unquote(line))

这里正则写复杂了,直接匹配 200 的响应码就可以了应该

最后能得到一个干净的日志

在日志中,其中 cookie 参数传入了一串 base64 编码数据

IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgc3FsaXRlMwoKY29ubiA9IHNxbGl0ZTMuY29ubmVjdCgnL3Zhci93d3cvZGIvY3VzdG9tZXJzLmRiJykKY3Vyc29yID0gY29ubi5jdXJzb3IoKQoKc3FsY21kID0gIiIiCkNSRUFURSBUQUJMRSBzZWNyZXRzCigKICAgaWQgIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT0lOQ1JFTUVOVCwKICAgdXNlcm5hbWUgdmFyY2hhcigyMCkgICAgTk9UIE5VTEwsCiAgIHBhc3NwaHJhc2UgdmFyY2hhcigyMCkgTk9UIE5VTEwsCiAgIGRhdGV0aW1lIFRJTUVTVEFNUCBERUZBVUxUIENVUlJFTlRfVElNRVNUQU1QLAogICBGT1JFSUdOIEtFWSAodXNlcm5hbWUpIFJFRkVSRU5DRVMgY3VzdG9tZXJzKHVzZXJuYW1lKQopOyIiIgoKY3Vyc29yLmV4ZWN1dGUoc3FsY21kKQoKc3FsY21kID0gIiIiCkNSRUFURSBUQUJMRSB1c2VyX2ZsYWcKKAogICBpZCAgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPSU5DUkVNRU5ULAogICB1c2VybmFtZSAgIHZhcmNoYXIoMjApICAgICAgIE5PVCBOVUxMLAogICBmbGFnbmFtZSAgICB2YXJjaGFyKDIwKSAgICAgICBOT1QgTlVMTCwKICAgZmxhZ3ZhbHVlICAgICB2YXJjaGFyKDIwKSAgICAgICBOT1QgTlVMTCwKICAgZW5jcnlwdGVkICAgaW50IERFRkFVTFQoMSksCiAgIG5vdGUgdmFyY2hhcigyMCkgREVGQVVMVCgnJyksCiAgIGRhdGV0aW1lIFRJTUVTVEFNUCBERUZBVUxUIENVUlJFTlRfVElNRVNUQU1QLAogICBGT1JFSUdOIEtFWSAodXNlcm5hbWUpIFJFRkVSRU5DRVMgY3VzdG9tZXJzKHVzZXJuYW1lKQopOyIiIgoKY3Vyc29yLmV4ZWN1dGUoc3FsY21kKQoKY3Vyc29yLmNsb3NlKCkKY29ubi5jb21taXQoKQpjb25uLmNsb3NlKCkK

解密后是一段 python 代码,用来为数据库创建表

#!/usr/bin/env python3
import sqlite3

conn = sqlite3.connect('customers.db')
cursor = conn.cursor()

sqlcmd = """
CREATE TABLE secrets
(
   id  INTEGER PRIMARY KEY AUTOINCREMENT,
   username varchar(20)    NOT NULL,
   passphrase varchar(20) NOT NULL,
   datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
   FOREIGN KEY (username) REFERENCES customers(username)
);"""

cursor.execute(sqlcmd)

sqlcmd = """
CREATE TABLE user_flag
(
   id  INTEGER PRIMARY KEY AUTOINCREMENT,
   username   varchar(20)       NOT NULL,
   flagname    varchar(20)       NOT NULL,
   flagvalue     varchar(20)       NOT NULL,
   encrypted   int DEFAULT(1),
   note varchar(20) DEFAULT(''),
   datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
   FOREIGN KEY (username) REFERENCES customers(username)
);"""

cursor.execute(sqlcmd)

cursor.close()
conn.commit()
conn.close()

这个文件的主要目的应该是告诉你表的字段,简化了做题流程,可有可无吧

下一步,也就是要提取这些流量包中的数据了,仔细观察流量,其实很明显是 sql注入 流量,且是用 sqlmap 跑出来的,每条流量后面携带的 User-Agant 头就能看出来

布尔盲注,写个脚本提取数据即可

import re

TABLENAME = 'user_flag'
FIELD = 'encrypted'

datas = open('washed.log','r').readlines()

passphrase = re.compile('.*COALESCE\(%s,CHAR\(32\)\) FROM %s LIMIT ([0-9]{1,2})\,1\),([0-9]{1,2}),1\)\>CHAR\(([0-9]{1,3})\) AND .* 200 (196|341).*' % (FIELD,TABLENAME),re.I)

passphraseData = {

}

for line in datas:
    matchs = passphrase.match(line)
    if matchs:
        find = passphrase.findall(line)[0]
        if passphraseData.get(find[0]) == None:
            passphraseData[find[0]] = {
                
            }
        if passphraseData[find[0]].get(find[1]) == None:
            passphraseData[find[0]][find[1]] = []

        passphraseData[find[0]][find[1]].append(
            [
                find[2],
                find[3]
            ]
        )

baseformat = "{:<4}{:<32}"
print(baseformat.format('id',FIELD))
for id in passphraseData:
    password = "";
    for e in passphraseData[id]:
        rightkey = -1;
        errorkey = -1;

        for i in passphraseData[id][e][::-1]:
            if i[1] == '341' and rightkey == -1:
                rightkey = int(i[0])
            if i[1] == '196' and errorkey == -1:
                errorkey = int(i[0])
        if rightkey == -1 or errorkey == -1:
            continue;
        if rightkey+1 == errorkey:
            password += chr(rightkey+1);

    print(baseformat.format(id,password));

通过修改脚本中的 TABLENAMEFIELD 变量可以提取不同的盲注数据

那么需要提取哪些数据我们可以先放一放,回到前面他提供的生成加密后 flag 数据的文件中,看一下代码,可以锁定这几行

insertsecretcmd = f"insert into secrets(username,passphrase) values('{username}','{passphrase}')"
insertuser_flagcmd = f"insert into user_flag(username,flagname,flagvalue,note) values('{username}','{flagname}','{base64flagdata}','{notestr}')"
cursor.execute(insertsecretcmd)
cursor.execute(insertuser_flagcmd)

而通过下面的这些代码,可以知道这些数据字段之间的关系

flagname  = f"flag{count}"

# AES加密用的salt从passphrase派生
salt = md5_hash(passphrase.encode("utf-8") + b"saltseed")
# 使用passphrase和salt通过PBKDF2派生密钥
key = pbkdf2(passphrase, salt, iterations, key_length)
# 初始化向量也从passphrase派生
iv =  md5_hash(passphrase.encode("utf-8") + b"IVseed") 

# 创建Cipher对象
cipher = AES.new(key, AES.MODE_CBC, iv)  

# 需要加密的数据
original_flagdata = genflagstr(username,count)

# 对数据进行填充,以符合AES的块大小(128位/16字节)
padded_flagdata = pad(original_flagdata, AES.block_size)  

# 加密数据 
encrypted_flagdata = cipher.encrypt(padded_flagdata)

# 使用相同的salt和passphrase通过PBKDF2派生密钥  
key_decrypt = pbkdf2(passphrase, salt, iterations, key_length)  

# 创建解密器对象  
decipher = AES.new(key_decrypt, AES.MODE_CBC, iv)  

# 解密数据  
decrypted_padded_data = decipher.decrypt(encrypted_flagdata)  

# 去除填充  
decrypted_flagdata = unpad(decrypted_padded_data, AES.block_size)
#decrypted_flagdatastr = decrypted_flagdata.decode("utf-8")
if  original_flagdata == decrypted_flagdata:
    print(f"\r\n   congrutulations: {original_flagdata} matchs {decrypted_flagdata}\r\n")  

    flagvalue = base64.b64encode(encrypted_flagdata)
    base64flagdata = flagvalue.decode("utf-8")

secrets 中的 username 用于生成了目标 flag , passphrase 字段派生了加密 flag 所使用的 AES 中的 keyiv 变量

AESkey 又通过函数 pbkdf2 生成,且携带了由 passphrase 派生的 salt

所以我们先通过上面的盲注提取脚本获取 usernamepassphraseflagvalue

最后得到的结果,经过整理后,如下

{0: {'username': 'Abraham', 'passphrase': 'lX2cl2FX5rWAvaF1', 'flagvalue': 'vsuhBrXLbl6z08OuMTpsjr14aBmip1RYcYV5mT6m3jY='}, 1: {'username': 'Akiko', 'passphrase': 'li23N1voIkniAwk5T7fTm0w1', 'flagvalue': 'ciz4vj/Knj7yR7ZqHDMtdbjrR1bIff7HgLdAHTGHC6mYOn1LJnYN0at7gA9m0+iW'}, 2: {'username': 'Albert', 'passphrase': '+ETF9yU36zgSy1/1u1Rq', 'flagvalue': 'zkkGriQEyEqj9FXYT2z/uynbkKBTrjfmZkjD0YxEp30='}, 3: {'username': 'Ann', 'passphrase': 'ki4qt4e6ymRedpvS5QjVqzaddLzU', 'flagvalue': 'bNnBB1c4Lce2DmdRkcN5U2R7t4oyqkfs1McVr5B9/xjalk2ZKUYpvdc1aglm6QbB'}, 4: {'username': 'Anne', 'passphrase': 'gY8w4pdXh0oW6++AzeVHveGRWkdP', 'flagvalue': 'BeBtptc/VAKsH+xYYy7Ppq3uRu7u2B57Pckz60AlWpDfIMo7myZiviQqjvGhkUTh'}, 5: {'username': 'Burt', 'passphrase': 'wuImkcqj1kVIrUC1+QkL9ET7wGys', 'flagvalue': 'hAMyf9Twj1bkmsmby21uujAIExv0atQ/gX6zdnO5RGv+XW/wA6tZn78j0LCG45u9'}, 6: {'username': 'Charlene', 'passphrase': 'U4D4HEXHkbX9jX8ykv8C', 'flagvalue': '5NZ+GDYsWjftT6ja8jH1IoD02F09KA7nTafuyhuhsEc='}, 7: {'username': 'Cheryl', 'passphrase': 'AxG7MVCVNtffr4ZYVErlEYLU', 'flagvalue': '2RGYYX451isKyXykksliKdqfqfZGBx9bKrnsMfJJynpoUvXHpWgpTYTV6W1hX1Rn'}, 8: {'username': 'Dean', 'passphrase': '+kZla/aX4bQy+Bgl+FqjiAIkel9R', 'flagvalue': 'O6kg8FJc3aOoOPsTz3WeW17p8nt8gMzHkqCe1kG1V6lkseMb9I5JMVF3PrBC067I'}, 9: {'username': 'Dirk', 'passphrase': 'Zglu/lEZPG1q9/PC6N8yVEJR3hiT', 'flagvalue': 'kiz9+8DwaJIwp74CwgiCRKAsJH8Icuo9qYaTnjS975hM3vBmWQZEoRfIV2Ht0+47'}, 10: {'username': 'Heather', 'passphrase': '+ROXoWcfiG1etDzI5Ce+IIma', 'flagvalue': 'be+4cGpopf2smgbGk7BsJtXbAVHnVEQmIEJaks5LS+o='}, 11: {'username': 'Innes', 'passphrase': 's/e8gc9O9tLVgnSu', 'flagvalue': 'TZp3Wngcls23O4doukmnTH0W3uZTRIQs3ih3c3G5hHU='}, 12: {'username': 'Johnson', 'passphrase': 'p4qnEU26lRL3rWihaeTltPWP', 'flagvalue': 'SmxLETbfO+BEeg3R1j+UWkc0f0X+faJm6Rv0ox4Covw='}, 13: {'username': 'Livia', 'passphrase': 'GHIPf43AUssFp9Pf', 'flagvalue': 'QNAsEwp7zeWlCjZQgbYoYCDE4DcIECke99cCW/mQgqM='}, 14: {'username': 'Marjorie', 'passphrase': 'EHpYh9ZwmvwDZNWWIObA', 'flagvalue': 'rMjnPlIf024hJuSct08fR6EerfM0YZI4JthJO4iWed4='}, 15: {'username': 'Meander', 'passphrase': 'KSOfdQPrmLXd+L/J4meO6zE1', 'flagvalue': 'zjPLgsu/riGxovEFhNdXSgbJeLI24hFsouajIwYiLuJm6STZi823EAUMGVM2T3TA'}, 16: {'username': 'Michael', 'passphrase': '5z4puHJAOUr30v5kUNKx/Q/s', 'flagvalue': 'n+m/7/d9w8bhF6J+vgwA7PeyDP0vKVsuXwV6fIkXOHM='}, 17: {'username': 'Michel', 'passphrase': 'Apwibg5LKhOOu4394eCo', 'flagvalue': 'Mg5d8cqX7ESyudDEujG8xFL0Gg7fcfGh76MMB++ij2g='}, 18: {'username': 'Morningstar', 'passphrase': 'TJOC6+L+vTajLOGqfJMweoLFZqvJ', 'flagvalue': 'PgH8ySWqsB1IJEradvOouNWEP5mpHZnu5e9ShkiS1LWLb81hRUjAZLD4zzzxO5Tn'}, 19: {'username': 'Reginald', 'passphrase': '8oO3XRquAlw69cTS', 'flagvalue': 'QnuOV76w2slE2KH1frVRQLYyXv1eVu1R8OQj/tJ0KwI='}, 20: {'username': 'Sheryl', 'passphrase': '3v4uHprHE/6+OzwiuDeGQ84z', 'flagvalue': '8jeIVEKbTQvSTt1mgWLbQ2BAcvSXXwdAcNSh6G7d9qQ='}, 21: {'username': 'Stearns', 'passphrase': 'l9+Lb9lMCHVDFc0Vg7b3', 'flagvalue': 'XBWdUHCfIDSQ8KETGkYo5gjuUFOKi4PYE4ugv7FwdPM='}, 22: {'username': 'Sylvia', 'passphrase': 'M9/QApJbT4aMHMrfVWv+', 'flagvalue': 'vuXmNsrtO6Jhk3rhLpFpgvX9JJd8VDlyav1OGnjbZl4='}, 23: {'username': 'admin', 'passphrase': '7xjo0DHFsoF1Jrus', 'flagvalue': 'lTr01ZNX8R9xotVanrPtKiBjCfQD5U5dD+KLRPgjnQ4='}}

编写解密脚本如下,其中一些迭代加密的函数题目已经给出,复制过来用即可

import hashlib
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

datalist = {0: {'username': 'Abraham', 'passphrase': 'lX2cl2FX5rWAvaF1', 'flagvalue': 'vsuhBrXLbl6z08OuMTpsjr14aBmip1RYcYV5mT6m3jY='}, 1: {'username': 'Akiko', 'passphrase': 'li23N1voIkniAwk5T7fTm0w1', 'flagvalue': 'ciz4vj/Knj7yR7ZqHDMtdbjrR1bIff7HgLdAHTGHC6mYOn1LJnYN0at7gA9m0+iW'}, 2: {'username': 'Albert', 'passphrase': '+ETF9yU36zgSy1/1u1Rq', 'flagvalue': 'zkkGriQEyEqj9FXYT2z/uynbkKBTrjfmZkjD0YxEp30='}, 3: {'username': 'Ann', 'passphrase': 'ki4qt4e6ymRedpvS5QjVqzaddLzU', 'flagvalue': 'bNnBB1c4Lce2DmdRkcN5U2R7t4oyqkfs1McVr5B9/xjalk2ZKUYpvdc1aglm6QbB'}, 4: {'username': 'Anne', 'passphrase': 'gY8w4pdXh0oW6++AzeVHveGRWkdP', 'flagvalue': 'BeBtptc/VAKsH+xYYy7Ppq3uRu7u2B57Pckz60AlWpDfIMo7myZiviQqjvGhkUTh'}, 5: {'username': 'Burt', 'passphrase': 'wuImkcqj1kVIrUC1+QkL9ET7wGys', 'flagvalue': 'hAMyf9Twj1bkmsmby21uujAIExv0atQ/gX6zdnO5RGv+XW/wA6tZn78j0LCG45u9'}, 6: {'username': 'Charlene', 'passphrase': 'U4D4HEXHkbX9jX8ykv8C', 'flagvalue': '5NZ+GDYsWjftT6ja8jH1IoD02F09KA7nTafuyhuhsEc='}, 7: {'username': 'Cheryl', 'passphrase': 'AxG7MVCVNtffr4ZYVErlEYLU', 'flagvalue': '2RGYYX451isKyXykksliKdqfqfZGBx9bKrnsMfJJynpoUvXHpWgpTYTV6W1hX1Rn'}, 8: {'username': 'Dean', 'passphrase': '+kZla/aX4bQy+Bgl+FqjiAIkel9R', 'flagvalue': 'O6kg8FJc3aOoOPsTz3WeW17p8nt8gMzHkqCe1kG1V6lkseMb9I5JMVF3PrBC067I'}, 9: {'username': 'Dirk', 'passphrase': 'Zglu/lEZPG1q9/PC6N8yVEJR3hiT', 'flagvalue': 'kiz9+8DwaJIwp74CwgiCRKAsJH8Icuo9qYaTnjS975hM3vBmWQZEoRfIV2Ht0+47'}, 10: {'username': 'Heather', 'passphrase': '+ROXoWcfiG1etDzI5Ce+IIma', 'flagvalue': 'be+4cGpopf2smgbGk7BsJtXbAVHnVEQmIEJaks5LS+o='}, 11: {'username': 'Innes', 'passphrase': 's/e8gc9O9tLVgnSu', 'flagvalue': 'TZp3Wngcls23O4doukmnTH0W3uZTRIQs3ih3c3G5hHU='}, 12: {'username': 'Johnson', 'passphrase': 'p4qnEU26lRL3rWihaeTltPWP', 'flagvalue': 'SmxLETbfO+BEeg3R1j+UWkc0f0X+faJm6Rv0ox4Covw='}, 13: {'username': 'Livia', 'passphrase': 'GHIPf43AUssFp9Pf', 'flagvalue': 'QNAsEwp7zeWlCjZQgbYoYCDE4DcIECke99cCW/mQgqM='}, 14: {'username': 'Marjorie', 'passphrase': 'EHpYh9ZwmvwDZNWWIObA', 'flagvalue': 'rMjnPlIf024hJuSct08fR6EerfM0YZI4JthJO4iWed4='}, 15: {'username': 'Meander', 'passphrase': 'KSOfdQPrmLXd+L/J4meO6zE1', 'flagvalue': 'zjPLgsu/riGxovEFhNdXSgbJeLI24hFsouajIwYiLuJm6STZi823EAUMGVM2T3TA'}, 16: {'username': 'Michael', 'passphrase': '5z4puHJAOUr30v5kUNKx/Q/s', 'flagvalue': 'n+m/7/d9w8bhF6J+vgwA7PeyDP0vKVsuXwV6fIkXOHM='}, 17: {'username': 'Michel', 'passphrase': 'Apwibg5LKhOOu4394eCo', 'flagvalue': 'Mg5d8cqX7ESyudDEujG8xFL0Gg7fcfGh76MMB++ij2g='}, 18: {'username': 'Morningstar', 'passphrase': 'TJOC6+L+vTajLOGqfJMweoLFZqvJ', 'flagvalue': 'PgH8ySWqsB1IJEradvOouNWEP5mpHZnu5e9ShkiS1LWLb81hRUjAZLD4zzzxO5Tn'}, 19: {'username': 'Reginald', 'passphrase': '8oO3XRquAlw69cTS', 'flagvalue': 'QnuOV76w2slE2KH1frVRQLYyXv1eVu1R8OQj/tJ0KwI='}, 20: {'username': 'Sheryl', 'passphrase': '3v4uHprHE/6+OzwiuDeGQ84z', 'flagvalue': '8jeIVEKbTQvSTt1mgWLbQ2BAcvSXXwdAcNSh6G7d9qQ='}, 21: {'username': 'Stearns', 'passphrase': 'l9+Lb9lMCHVDFc0Vg7b3', 'flagvalue': 'XBWdUHCfIDSQ8KETGkYo5gjuUFOKi4PYE4ugv7FwdPM='}, 22: {'username': 'Sylvia', 'passphrase': 'M9/QApJbT4aMHMrfVWv+', 'flagvalue': 'vuXmNsrtO6Jhk3rhLpFpgvX9JJd8VDlyav1OGnjbZl4='}, 23: {'username': 'admin', 'passphrase': '7xjo0DHFsoF1Jrus', 'flagvalue': 'lTr01ZNX8R9xotVanrPtKiBjCfQD5U5dD+KLRPgjnQ4='}}


def md5_hash(input_rawstring):  
    # 创建一个md5 hash对象  
    m = hashlib.md5()  
      
    # 更新hash对象  
    m.update(input_rawstring)  
      
    # 获取散列值的16进制字符串表示(32个字节长度)
    hexdigest =  m.hexdigest()
    #将16进制字符串转换为16字节的字节串
    byte_digest = bytes.fromhex(hexdigest)
    return byte_digest
    #return (hex_dig)

def pbkdf2(passphrase, salt, iterations, key_length):  
    h = hashlib.sha256()  
    u = passphrase.encode("utf-8") + salt  
    for _ in range(iterations):  
        h.update(u)  
        u = h.digest()
      
    return h.digest()[:key_length] 

iterations = 10000  
key_length = 16  
flag1 = ""
flag6 = ""
for index in datalist:
    e = datalist[index]
    username = e['username']
    passphrase = e['passphrase']
    flagvalue = e['flagvalue']
    
    salt = md5_hash(passphrase.encode("utf-8") + b"saltseed")
    key = pbkdf2(passphrase, salt, iterations, key_length)
    iv =  md5_hash(passphrase.encode("utf-8") + b"IVseed") 
    cipher = AES.new(key, AES.MODE_CBC, iv)

    flag = unpad(
        cipher.decrypt(
            b64decode(flagvalue)
        ),
        AES.block_size
    ).decode()
    
    print(flag)

"""
flag24{5be1fd0324f5d4c0}
flag23{f70a26e0c802b318191590d5}
flag22{6e3cf0e098f7891c51e0}
flag21{c793acc91e433349bcb413fde0}
flag20{a401647f22f5754229f8c737fc}
flag19{919fc8765f7dbebbfc9bb8cd1608}
flag18{79f3a122e01e2ce73c9a}
flag17{4ff7944842b0441fa6785264}
flag16{468bd9239567b536f194b7bdca9b}
flag15{ac07329898b13e965fc70adb06fcd8}
flag14{340f915dd1d175cb441d5a}
flag13{7244036fd7cf9c89}
flag12{5102b8cbd3a38ca73d2749}
flag11{1089f5382dc193}
flag10{bf6bd8307a5b5885402f}
flag9{34cab1ff059a3a1858fb1db96a598f}
flag8{f6daa6ea9a299e80d7285444}
flag7{6f28d6f16daad6440a39}
flag6{a85bcfc646ff161c71e520e7cba3}
flag5{f22e5ce320a95c02}
flag4{16bf4b02a8a5ff58e6b69d0c}
flag3{a70f5b89829e5ac8b9f5}
flag2{852e01448420410c26}
flag1{e6f1573b9496a7f3}
"""

然后题目说只关注 flag1flag6 ,那么按照题目拼接提交就好了

flag{e6f1573b9496a7f3-a85bcfc646ff161c71e520e7cba3}

2 crazyaes

本身对加密不是特别擅长,看这个看了挺久,又要去看AES的原理,终于搞清楚了改了哪里。。。

拿到的 exe 文件加了 UPX 的壳,而且修改了一些信息,需要手动改回来

共四处地方,改完后就能用 UPX 工具成功脱壳

image

脱壳后发现花指令

main 函数中的花指令

image

将这里的调用指令 nop 掉就能正常识别 main 函数

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  unsigned int j; // [esp+D0h] [ebp-60h]
  int i; // [esp+DCh] [ebp-54h]
  int v6; // [esp+E8h] [ebp-48h]
  char v7; // [esp+F7h] [ebp-39h]
  char v8[24]; // [esp+100h] [ebp-30h] BYREF
  char v9[20]; // [esp+118h] [ebp-18h]

  v9[0] = -76;
  v9[1] = 56;
  v9[2] = 54;
  v9[3] = 48;
  v9[4] = 30;
  v9[5] = 104;
  v9[6] = 72;
  v9[7] = 87;
  v9[8] = 81;
  v9[9] = 1;
  v9[10] = -73;
  v9[11] = 3;
  v9[12] = -101;
  v9[13] = -104;
  v9[14] = -29;
  v9[15] = 126;
  memset(v8, 17, 15);
  v8[15] = 1;
  v7 = 0;
  v6 = 0;
  sub_433B4F(&unk_4A2114);
  while ( v7 != 10 )
  {
    v7 = sub_433451();
    byte_4AEF38[v6++] = v7;
  }
  for ( i = 0; i < 16; ++i )
    v8[i] = byte_4AEF38[i];
  sub_43615B(0, v8, 16, &byte_4AE000);
  for ( j = 0; j < 0x10; ++j )
  {
    if ( v8[j] != v9[j] )
    {
      sub_433B4F("wrong!!!");
      Sleep(0x1388u);
      j___loaddll(0);
    }
  }
  sub_433B4F("WOW!!!");
  sub_4358A0("pause");
  return sub_433B4F("\n");
}

逻辑很简单,用户的输入被赋值给 v8 数据,然后传入到 sub_43615B 函数中,应该是进行加密,而同步传入的 byte_4AE000 是一串字符串,应该是密钥

函数 sub_43615B 调用的加密函数 loc_439F00 也被花指令干扰,nop 掉即可

// positive sp value has been detected, the output may be wrong!
int __cdecl sub_439F00(int a1, int a2, int a3, int a4)
{
  int v5; // [esp-4h] [ebp-220h]
  size_t v6; // [esp+0h] [ebp-21Ch]
  int j; // [esp+D0h] [ebp-14Ch]
  int m; // [esp+D0h] [ebp-14Ch]
  int n; // [esp+D0h] [ebp-14Ch]
  int i; // [esp+DCh] [ebp-140h]
  int k; // [esp+E8h] [ebp-134h]
  _BYTE v12[24]; // [esp+F4h] [ebp-128h]
  _DWORD v13[6]; // [esp+10Ch] [ebp-110h] BYREF
  _BYTE v14[244]; // [esp+124h] [ebp-F8h] BYREF

  j__memset(v14, v5, v6);
  memset(v13, 0, 16);
  v12[0] = 0;
  v12[1] = 1;
  v12[2] = 2;
  v12[3] = 3;
  v12[4] = 1;
  v12[5] = 0;
  v12[6] = 3;
  v12[7] = 2;
  v12[8] = 2;
  v12[9] = 3;
  v12[10] = 0;
  v12[11] = 1;
  v12[12] = 3;
  v12[13] = 2;
  v12[14] = 1;
  v12[15] = 0;
  sub_4354FE(a1, a4, (int)v14);
  for ( i = 0; i < a3; i += 4 * dword_4AE01C[a1] )
  {
    for ( j = 0; j < 4 * dword_4AE01C[a1]; ++j )
      *((_BYTE *)v13 + j) = *(_BYTE *)(a2 + j + i);
    for ( k = 0; k <= dword_4AE014[a1]; ++k )
    {
      if ( k > 0 )
      {
        sub_4365C0(a1, (int)v13);
        sub_434860(a1, (int)v13);
        if ( k < dword_4AE014[a1] )
          ((void (__cdecl *)(int, _DWORD *))sub_434D51)(a1, v13);
      }
      sub_435C01(a1, (int)v13, (int)v14, k);
      for ( m = 0; m < 4 * dword_4AE01C[a1]; ++m )
        *((_BYTE *)v13 + m) ^= v12[m];
    }
    for ( n = 0; n < 4 * dword_4AE01C[a1]; ++n )
      *(_BYTE *)(a2 + n + i) = *((_BYTE *)v13 + n);
  }
  return 0;
}

通过加密函数可以判断出来是 AES 加密,但是对加密进行了魔改

  • 字节替换 函数 sub_4365C0 中,在字节替换最后还进行了异或 0xA1

  • 列混合 函数 sub_434D51 中,在每次的 伽罗华域加法和乘法 结束后进行了异或 0x54 ,这个函数里面也有花指令,要去花的话把 jz 去掉,然后其他转为数据即可

  • 每轮运算结束前都会进行一个异或处理

还有就是在 TLS 的回调函数里面修改了 key[2] 的值为 0x68

脚本比较长,网上找个 AES 的模板改了一下,下面就贴魔改的部分

  • 字节替换
void inv_sub_bytes(uint8_t *state) {
    for (int i = 0; i < 16; i++) {
        state[i] ^= 0xA1u;
        state[i] = inv_sbox[state[i]];
    }
}
  • 列混合
uint8_t GMul(uint8_t v ,uint8_t u) {
    uint8_t p = 0;

    for (int i = 0; i < 8; ++i) {
        if (u & 0x01) {    //
            p ^= v;
        }

        int flag = (v & 0x80);
        v <<= 1;
        if (flag) {
            v ^= 0x1B; /* x^8 + x^4 + x^3 + x + 1 */
        }

        u >>= 1;
    }

    return p;
}

void inv_mix_columns(uint8_t *state) {
    uint8_t a, b, c, d;
    for (int i = 0; i < 4; i++) {
        a = state[i*4+0] ^ 0x54;
        b = state[i*4+1] ^ 0x54;
        c = state[i*4+2] ^ 0x54;
        d = state[i*4+3] ^ 0x54;

        state[i*4+0] = GMul(0x0e , a) ^ GMul(0x0b , b) ^ GMul(0x0d , c) ^ GMul(0x09 , d);
        state[i*4+1] = GMul(0x09 , a) ^ GMul(0x0e , b) ^ GMul(0x0b , c) ^ GMul(0x0d , d);
        state[i*4+2] = GMul(0x0d , a) ^ GMul(0x09 , b) ^ GMul(0x0e , c) ^ GMul(0x0b , d);
        state[i*4+3] = GMul(0x0b , a) ^ GMul(0x0d , b) ^ GMul(0x09 , c) ^ GMul(0x0e , d);
    }
}
  • 解密主体
void aes_decrypt(uint8_t *ciphertext, uint8_t *key, uint8_t *plaintext) {
    uint8_t state[16];
    uint8_t expanded_key[176];
    uint8_t key2[16] = {0,1,2,3,1,0,3,2,2,3,0,1,3,2,1,0};
    
    memcpy(state, ciphertext, 16);
    
    key_expansion(key, expanded_key);
    add_round_key(state, expanded_key + 160);

    for(int i=0;i<16;++i){
        ((unsigned char*)state)[i] ^= key2[i];
    }
    for (int round = 9; ;round--) {
        inv_shift_rows(state);
        inv_sub_bytes(state);
        add_round_key(state, expanded_key + round * 16);
        for(int i=0;i<16;++i){
            ((unsigned char*)state)[i] ^= key2[i];
        }
        if ( round == 0){
            break;
        }else{
            inv_mix_columns(state);
        }
    }
    
    memcpy(plaintext, state, 16);
}
int main() {
    uint8_t key[] = { 0x67,0x61,0x21,0x34,0x33,0x6A,0x4A,0x4B,0x67,0x66,0x6A,0x47,0x4D,0x65,0x41,0x52 };

    key[2] = 0x68;

    unsigned char flag[] = { 0xb4,0x38,0x36,0x30,0x1e,0x68,0x48,0x57,0x51,0x01,0xb7,0x03,0x9b,0x98,0xe3,0x7e };

    
    unsigned char plaintext[16];
    
    aes_decrypt(flag, key, plaintext);
    
    printf("flag: ");
    for (int i = 0; i < 16; i++) {
        printf("%c", plaintext[i]);
    }
    printf("\n");
    
    return 0;
}

最后 flag

flag{Re_1ts_f0n}

3 坚守要地

反编译 apk 后,查看具体逻辑,其中在初始化的时候,调用了 init 函数

private void init() {
    File file = new File(getDataDir().getPath() + File.separator + DATABASE);
    if (!file.exists()) {
        file.mkdir();
        file.getPath();
        FileUtil.copyRawFileToDirectory(this, R.raw.information, "information.db", file);
    }
    File file2 = new File(getDataDir().getPath() + File.separator + CODE);
    if (file2.exists()) {
        return;
    }
    file2.mkdir();
    file2.getPath();
    FileUtil.copyRawFileToDirectory(this, R.raw.wcmm5ukk, "wcmm5ukk", file2);
}

其目的是将本地 apk 中的资源 information.dbwcmm5ukk 文件复制到安装的路径

题目还给了一个模拟器的镜像,应该是将镜像中的 information.db 文件提取,然后里面就是密文了,对密文进行解密即可

其实镜像没什么用,都是从资源文件里面复制出来了,我们提取 apk 中的 db 文件就可以了,但是里面是假的 flag

apk 代码中,还存在三个变量

private static final String CODE = "code";
private static final String DATABASE = "databases";
private static final String KEY = "VWt1OEhOMkpnbU1VSnhzQw==";

其中这个 KEY 变量是 base64 编码,且没有地方调用,联想到 information.db 文件是假的数据库文件,所以猜测 wcmm5ukk 文件是加密的 sqlite 文件,而这个 KEY 便是解密密钥

image

将密钥解密后得到 Uku8HN2JgmMUJxsC 作为密码进行解密

image

能够得到一串加密的 flag

而在 apk 代码中,虽然 onClick 函数是直接显示提示,并未调用解密函数,但是我们可以通过 hook 进行代替调用

private void decrypt() {
    this.mEtFlag.setText(Encryption.decrypt(this.mEtFlag.getText().toString(), "FCAr62VFBF9SRw8Z"));
}

通过 hook 调用 Encryption.decrypt 函数即可

Java.perform(function(){
    let MainActivity = Java.use("com.information.app1.MainActivity"),
        Encryption = Java.use("com.information.ndk.Encryption");
        MainActivity["showToast"].implementation = function () {
            console.log(`clicked`);
            console.log(Encryption['decrypt']("a9f3718960505f6bba12e85b550db764922b4b97","FCAr62VFBF9SRw8Z"))
        };
})

image

flag{119.21803,2604085}
0%