只允许64位的ELF可执行程序,总共有三个赛道,x86-64,arm64,mips64。不太熟悉汇编,三个赛道基本上都是用gcc来完成,辅助少量的汇编代码修改。
最终成绩
x86-64 472字节 第15名
arm64 816字节 第8名
mips63 1024字节 第7名
一开始有想过可否用碰撞实现,研究了几天后发现毫无头绪,就先用普通的方法尝试啦。
x86-64
主要几个点
不依赖任何库进行独立编译,对于程序
打印
和退出
的操作,用内联汇编调用系统中断完成,计算md5算法中需要用到绝对值计算和sin函数,都diao y直接在内存中读取自身,省掉了读取文件操作的步骤
计算md5的时候,根据md5算法的原理,以每64字节为一块进行计算,下一块的hash是根据上一块的hash算出的,我们可以直接算出第一块至倒数第二块的hash,硬编码写入,程序只用进行最后一次块的计算,节省体积。
对elf header处理,elf header有些标识位置可以利用
- 可以根据指令大小来做对应填充
对汇编指令的简单处理
先用gcc生成汇编文件,然后对它做一些简单的处理
一个简单处理的脚本
sed -i "s/.section.*//g" my_3_amd.s sed -i "s/.ident.*//g" my_3_amd.s sed -i "s/pushq.*//g" my_3_amd.s sed -i "s/.size _start.*//g" my_3_amd.s sed -i "s/.align 4//g" my_3_amd.s sed -i "s/movl \$1, %edx/mov \$1, %dl/g" my_3_amd.s sed -i "s/.align 16//g" my_3_amd.s sed -i "s/movsbq %r11b, %r11//g" my_3_amd.s sed -i "s/movslq %ecx, %rcx//g" my_3_amd.s
手动处理
- 开头的
xorl %ecx, %ecx
可删除,程序初始化时寄存器默认会为0
- 开头的
程序参考源码
#define SYS_write 1 #define SYS_exit 60 #define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c)))) static void write(char *d, int l) { __asm__ volatile("syscall" : : "a"(SYS_write), "D"(1), "S"((long)d), "d"(l) : "rcx", "r11", "memory"); } static long double fsin_my(long double a) { long double res; asm __volatile__("fsin;\n\t" "fabs;\n\t" : "=t"(res) : "0"(a) : "memory"); return res; } void _start(void) { unsigned char *msg = (unsigned char *)0x400000; const short initial_len =466; // 固定 需要填写 const short new_len = ((((initial_len + 8) / 64) + 1) * 64) - 8; short offset = new_len - (new_len % 64); // 最后一个块的位置 通过计算获得 static unsigned int digest[4] = {1, 2, 3, 4}; // 需要填写 msg[initial_len] = 0x80; *(long *)(msg + new_len) = initial_len << 3; //固定 需要填写 long 改成 char能节省4字节,未测试 unsigned int *w = (unsigned int *)(msg + offset); unsigned int a = digest[0]; unsigned int b = digest[1]; unsigned int c = digest[2]; unsigned int d = digest[3]; for (int i= 0; i < 64; i++) { unsigned int f; int g; if (i < 16) { f = (b & c) | ((~b) & d); g = i; } else if (i < 32) { f = (d & b) | ((~d) & c); g = (5 * i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } else { f = c ^ (b | (~d)); g = (7 * i) % 16; } long double t1 = fsin_my(i + 1); const unsigned long long t2 = (unsigned long long)1<<32; unsigned int k = (unsigned int)(t2 * t1); f += a + k + w[g]; a = d; d = c; c = b; const char r[] = {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; b = b + LEFTROTATE(f, r[i / 16 * 4 + i % 4]); } digest[0] += a; digest[1] += b; digest[2] += c; digest[3] += d; unsigned char *md = (unsigned char *)&digest[0]; char x[32]; for (int i = 0; i < 32; i++) { int a = (md[i / 2] >> (4 * (1 - i % 2))) & 0xF; char c = a >= 10 ? a + ('a' - 10) : a + '0'; x[i] = c; } write(&x, 32); __asm__ volatile("syscall" : : "a"(SYS_exit), "D"(0) : "rcx", "r11", "memory"); }
编译流程
生成汇编代码
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ -fdata-sections" gcc $CFLAGS my_3_amd.c -S -mavx -msse -mavx2 -ffast-math # -fverbose-asm ./change_asm.sh # 自动优化汇编代码那部分
手动优化汇编代码
汇编生成二进制
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -Wl,--build-id=none -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ " gcc $CFLAGS my_3_amd.s -o test_amd64 objdump -S test_amd64 strip -s test_amd64 # strip 减少 1088-704=384 ../../sstrip test_amd64 # sstri 减少 704-492=212 ls -l|grep test ./test_amd64 echo md5sum test_amd64
改造elf
rm selfmd5-test ../../elftoc -E test_amd64 > selfmd5.h mv selfmd5.h ../../ g++ -g -o ../../trim-elf ../../trim-src.c && ../../trim-elf ls -l|grep self chmod 777 selfmd5-test ./selfmd5-test echo md5sum selfmd5-test
trim-src.c
#include "selfmd5.h" #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> //typedef struct { // unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ // Elf64_Half e_type; /* Object file type */ // Elf64_Half e_machine; /* Architecture */ // Elf64_Word e_version; /* Object file version */ // Elf64_Addr e_entry; /* Entry point virtual address */ // Elf64_Off e_phoff; /* Program header table file offset */ // Elf64_Off e_shoff; /* Section header table file offset */ // Elf64_Word e_flags; /* Processor-specific flags */ // Elf64_Half e_ehsize; /* ELF header size in bytes */ // Elf64_Half e_phentsize; /* Program header table entry size */ // Elf64_Half e_phnum; /* Program header table entry count */ // Elf64_Half e_shentsize; /* Section header table entry size */ // Elf64_Half e_shnum; /* Section header table entry count */ // Elf64_Half e_shstrndx; /* Section header string table index */ //} Elf64_Ehdr; //typedef struct //{ // Elf64_Word p_type; /* Segment type */ // Elf64_Word p_flags; /* Segment flags */ // Elf64_Off p_offset; /* Segment file offset */ // Elf64_Addr p_vaddr; /* Segment virtual address */ // Elf64_Addr p_paddr; /* Segment physical address */ // Elf64_Xword p_filesz; /* Segment size in file */ // Elf64_Xword p_memsz; /* Segment size in memory */ // Elf64_Xword p_align; /* Segment alignment */ //} Elf64_Phdr; int main(int argc, char *argv[]) { size_t size = sizeof(foo); // - sizeof(foo._end); printf("foo size:%d\n",size); for (int i = 8; i < 16; ++i) { foo.ehdr.e_ident[i] = 0xFF; // 10可修改,留2位跳转 } foo.ehdr.e_version = 0xAAAAAAAA; // 2 可修改,留2位跳转 foo.ehdr.e_shoff = 0xFFFFFFFFFFFFFFFF; // 6可修改,留2跳转 foo.ehdr.e_flags = 0xCCCCCCCC; //2 可修改,留2跳转 // long code_text = offsetof(elf, text) + ADDR_TEXT; // printf("代码段偏移位置 offset %d,%d\n", offsetof(elf, text), code_text); // foo.ehdr.e_entry = code_text; // foo.ehdr.e_ident[8] = 0xEB; // foo.ehdr.e_ident[9] = offsetof(elf, text) - 10; // *(long *)(foo.ehdr.e_ident + 9) =code_text ; // foo.ehdr.e_ident[9] = offsetof(elf, text); // foo.ehdr.e_ident[10] = code_text<<4; // printf("new entry %d\n", entry); foo.ehdr.e_entry = ADDR_TEXT + 8; // copy 6 bytes foo.ehdr.e_ident[8] = foo.text[0]; foo.ehdr.e_ident[9] = foo.text[1]; foo.ehdr.e_ident[10] = foo.text[2]; foo.ehdr.e_ident[11] = foo.text[3]; foo.ehdr.e_ident[12] = foo.text[4]; foo.ehdr.e_ident[13] = foo.text[5]; foo.ehdr.e_ident[14] = 0xEB; foo.ehdr.e_ident[15] = 24; // foo.ehdr.e_ident[16] = 0xEB; // foo.ehdr.e_ident[13] = 106;//26 printf("jmp %d\n", foo.ehdr.e_ident[15]); for (int i = 0; i < sizeof(foo.text) - 6; ++i) { foo.text[i] = foo.text[i + 6]; } size -= 6;
// copy 8 bytes ((char *)(&foo.ehdr.e_shoff))[0] = foo.text[0]; ((char *)(&foo.ehdr.e_shoff))[1] = foo.text[1]; ((char *)(&foo.ehdr.e_shoff))[2] = foo.text[2]; ((char *)(&foo.ehdr.e_shoff))[3] = foo.text[3]; ((char *)(&foo.ehdr.e_shoff))[4] = foo.text[4]; ((char *)(&foo.ehdr.e_shoff))[5] = foo.text[5]; ((char *)(&foo.ehdr.e_shoff))[6] = foo.text[6]; ((char *)(&foo.ehdr.e_shoff))[7] = foo.text[7]; ((char *)(&foo.ehdr.e_flags))[0] = 0xEB; ((char *)(&foo.ehdr.e_flags))[1] = 70; // ((char *)(&foo.ehdr.e_flags))[2] = 70; // ((char *)(&foo.ehdr.e_shoff))[7] = 0xEB; // ((char *)(&foo.ehdr.e_flags))[0] = 71; for (int i = 0; i < sizeof(foo.text) - 8; ++i) { foo.text[i] = foo.text[i + 8]; } size -= 8; // output FILE *fd = fopen("selfmd5-test", "wb"); size_t n = fwrite(&foo, size, 1, fd); printf("最终大小:%d\n", size); return 0; }
有些部分参考了知乎上的一篇文章,有修改 ## arm64 arm64和上面有些许不一样 1. 没有省掉md5最后一轮的运算,因为发现省不省大小一样。没有从汇编层面去研究原因 - =。 2. md5中的k值是硬编码上去的,发现用c语言实现一个sin函数体积很大,没有找到优化的方法 代码 ```c #define SYS_write 0x40 //64 #define SYS_exit 0x5d // 93 static void write(const void *buf, long size) { register long x8 __asm__("x8") = SYS_write; register long x0 __asm__("x0") = 1; register long x1 __asm__("x1") = (long)buf; register long x2 __asm__("x2") = size; __asm__ __volatile__( "svc 0" : : "r"(x8), "r"(x0), "r"(x1), "r"(x2) : "memory", "cc"); } static void _exit() { register long x8 __asm__("x8") = SYS_exit; register long x0 __asm__("x0") = 0; __asm__ __volatile__( "svc 0" : : "r"(x8), "r"(x0) : "memory", "cc"); } // leftrotate function definition #define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c)))) void _start(void) { const short initial_len = 824; unsigned char *msg = (unsigned char *)0x400000; static const char r[] = { 7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; static const unsigned int k[64] = { 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 }; unsigned int a, b, c, d; unsigned int h0, h1, h2, h3; // Initialize variables - simple count in nibbles: h0 = 0x67452301; h1 = 0xefcdab89; h2 = 0x98badcfe; h3 = 0x10325476; const int new_len = ((((initial_len + 8) / 64) + 1) * 64) - 8; msg[initial_len] = 0x80; *(unsigned long long *)(msg + new_len) = initial_len << 3; for (int offset = 0; offset < new_len; offset += 64) { unsigned int *w = (unsigned int *)(msg + offset); // Initialize hash value for this chunk: a = h0; b = h1; c = h2; d = h3; // Main loop: for (int i = 0; i < 64; i++) { unsigned int f; int g; if (i < 16) { f = (b & c) | ((~b) & d); g = i; } else if (i < 32) { f = (d & b) | ((~d) & c); g = (5 * i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } else { f = c ^ (b | (~d)); g = (7 * i) % 16; } f += a + k[i] + w[g]; a = d; d = c; c = b; b = b + LEFTROTATE(f, r[i / 16 * 4 + i % 4]); } h0 += a; h1 += b; h2 += c; h3 += d; } unsigned int digest[4] = {h0, h1, h2, h3}; unsigned char *md = (unsigned char *)&digest; char buf[32]; for (int i = 0; i < 32; i++) { char a = (md[i / 2] >> (4 * (1 - i % 2))) & 0xF; char c = a >= 10 ? a + ('a' - 10) : a + '0'; buf[i] = c; } write(&buf, 32); _exit(); }
编译
生成汇编代码
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ -fdata-sections" gcc $CFLAGS my_3_arm.c -S -ffast-math -fverbose-asm # -fverbose-asm
- 对arm的汇编不太熟悉,就删掉了exit后面的,可以优化两字节
汇编代码生成二进制
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -Wl,--build-id=none -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ " gcc $CFLAGS my_3_arm.s -o test_arm64 objdump -S test_arm64 strip -s test_arm64 ls -l|grep test_arm
mips64
mips64同arm64代码差不多,优化一下就可以直接用了。同样是sin函数无法实现,k值硬编码进去,占用了好多字节。
#define SYS_exit 5058 #define SYS_write 5001 static void _exit() { register unsigned long __a0 asm("$4") = 0; __asm__ __volatile__( " li $2, %0 \n" " syscall \n" " .set pop " : : "i"(SYS_exit), "r"(__a0) : "$2", "$8", "$9", "$10", "$11", "$12", "$13", "$14", "$15", "$24", "memory"); } static inline int _write(char *buf, int len) { register unsigned long __a0 asm("$4") = (unsigned long)1; register unsigned long __a1 asm("$5") = (unsigned long)buf; register unsigned long __a2 asm("$6") = (unsigned long)len; register unsigned long __a3 asm("$7"); unsigned long __v0; __asm__ __volatile__( " li $2, %2 \n" " syscall \n" " move %0, $2 \n" : "=r"(__v0), "=r"(__a3) : "i"(SYS_write), "r"(__a0), "r"(__a1), "r"(__a2) : "$2", "$8", "$9", "$10", "$11", "$12", "$13", "$14", "$15", "$24", "memory"); } #define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c)))) void __start(void) { const short initial_len = 1056; unsigned char *msg = (unsigned char *)0x120000000; static const char r[] = {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; static const unsigned int k[64] = { 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}; unsigned int a, b, c, d; // Initialize variables - simple count in nibbles: // h0 = 0x67452301; // h1 = 0xefcdab89; // h2 = 0x98badcfe; // h3 = 0x10325476; static unsigned int hash[4] = {1,2,3,4}; const int new_len = ((((initial_len + 8) / 64) + 1) * 64) - 8; msg[initial_len] = 0x80; *(unsigned long long *)(msg + new_len) = initial_len << 3; int offset = 1233; unsigned int *w = (unsigned int *)(msg + offset); // Initialize hash value for this chunk: a = hash[0]; b = hash[1]; c = hash[2]; d = hash[3]; // Main loop: for (int i = 0; i < 64; i++) { unsigned int f; int g; if (i < 16) { f = (b & c) | ((~b) & d); g = i; } else if (i < 32) { f = (d & b) | ((~d) & c); g = (5 * i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } else { f = c ^ (b | (~d)); g = (7 * i) % 16; } f += a + k[i] + w[g]; a = d; d = c; c = b; b = b + LEFTROTATE(f, r[i / 16 * 4 + i % 4]); } hash[0] += a; hash[1] += b; hash[2] += c; hash[3] += d; // unsigned int digest[] = {h0, h1, h2, h3}; unsigned char *md = (unsigned char *)&hash; for (int i = 0; i < 32; i++) { char a = (md[i / 2] >> (4 * (1 - i % 2))) & 0xF; char c = a >= 10 ? a + ('a' - 10) : a + '0'; _write(&c, 1); } _exit(); }
编译
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -ffast-math -Wl,--build-id=none -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ -fdata-sections" mips64el-linux-gnuabi64-gcc -mno-abicalls $CFLAGS my_3_mips.c -S -fverbose-asm # -fverbose-asm
CFLAGS="-static -Os -Wl,--omagic -ffunction-sections -ffast-math -Wl,--build-id=none -Wl,--gc-sections -nostartfiles -nodefaultlibs -nostdlib -nostdinc -fomit-frame-pointer -fno-stack-protector -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops -fmerge-all-constants -fno-ident -finline-functions-called-once -I ./ -fdata-sections" mips64el-linux-gnuabi64-gcc $CFLAGS -mno-abicalls my_3_mips.s -o test_mips mips64el-linux-gnuabi64-objdump -S test_mips ls -l |grep test_mips ./test_mips echo md5sum test_mips
复盘
对于x86-64的程序,感觉用c语言是已经优化到了极限,在接下来就是对汇编进行优化,将那些占用空间的寄存器统统替换。
对于arm64和mips64,就是sin函数的最优化写法的问题了。
这些是我想到的最后再优化的解决方法了,不过因为时间原因,还都未能实现。
比赛结束后,群里面各路大神放出了自己的思路,让我深知,自己的差距并不在那里。。很多大佬的奇淫巧技让人目瞪狗呆。。对于汇编代码的优化也是让人望尘莫及。。
详情见:https://mp.weixin.qq.com/s/zQu8YPwKZevBu8zx7jMzYg
有些方法和我的类似,说说和我不一样的做法。。
碰撞最后iv为0 (来自第一名思路)
前面说过可以提前计算md5第(n-1)个块,最后结果会有4个数值,利用elf中的缝隙字节做碰撞,将最后一个iv爆破为0,可以节省代码占用的4个字节。
eg:
$ nasm ./zero_res.asm && chmod +x ./zero_res && ./zero_res && echo # 编译运行,最后在echo一个回车 96935fafb893d3325b244bfe2ca98908 $ ./md5 ./zero_res # 这个程序用来计算目标文件n-1个block后的结果,并输出完整程序的md5值 0xc718942d, 0x27848216, 0x26f99fc3, 0x00000000 96935fafb893d3325b244bfe2ca98908
碰撞md5纯数字输出(来自出题人思路)
- 也是利用缝隙字节做碰撞,让程序输出全为数字,即省去了处理输出字母的代码。
sin的计算
使用泰勒展开式
因为不需要计算任意角,只需要计算1-64的整数,所以最后采用了两角和公式:
sin1、cos1作为常数,循环依次加到64即可
s[0] = 0.8414709848078965; c[0] = 0.5403023058681398; for (i=1;i<64;i++) { s[i] = s[i-1] * c[0] + c[i-1] * s[0]; c[i] = c[i-1] * c[0] - s[i-1] * s[0]; }
另一个实现思路
void cal_sin(int n) { // 角度转换 n = n * (3.142 / 180.0); // 等差数列 float x1 = n; float sinx = n; int i = 1; do { x1 = (-x1 * n * n) / (2 * i * (2 * i + 1)); sinx = sinx + x1; } while (++i<6); return sinx; }
最后
很感谢出题人的提供的比赛机会,这次比赛感觉到收获满满也很快乐~比赛途中好几次优化后,排名直冲第一,高兴没几个小时,就被别人比下去了,然后我又继续优化几个字节,再把他给比下去,哈哈哈哈,一来一往好几个回合,最后汇编大佬们都纷纷出场,然后我就再也没有机会了。。哭)
发表评论