第二届强网杯-Picturelock-文件AES加密解密
本帖最后由 skywilling 于 2018-5-28 21:46 编辑0x00前言
又一次遇到AES加密,莫名有种亲切感。这道题目是第二届强网杯逆向题目中的一道,应该是逆向题目中的压轴题吧(我只收到三个逆向题目,具体情况不是很清楚)。本篇文章带大家温习一下AES,长时间不接触真的会忘记,是时候复习一下以前的知识了!
0x01简单分析运行
题目拿到手里当然要看一下是什么类型的了。这是一道Android题目,直接以压缩包的方式打开:
在文件夹里我们发现了两个比较有用的文件,一个是后缀是.lock文件,另一个就是so文件,显然重要的处理方法保存在so文件中。
直接放到虚拟机中运行:
打开软件,界面很简洁,一个ENCRYPT(加密)按钮,一个REFRESH(刷新)按钮。点击第一个按钮后跳转到了另一个界面:
这是一个选择图片的界面,继续操作,又回到了主界面:
这时候界面多了一些信息,文本框多了刚才选中图片的文件名,需要注意的是,文件名后多了(.lock)后缀。同时,弹出了一个土司,里面显示了图片的绝对路径。了解了软件运行情况后,我们开始反编译。
0x02 Java层静态分析
反编译后,我们直接找到点击事件ENCRYPT按钮绑定的OnClick()方法:
对Android开发有一定了解的话,可以看出,代码首先检查了文件读写的权限,然后打开了图片选择的界面,选择图片后,会调用方法onActivityResult():
我们先不管这里具体进行了什么操作,可以看到这里有3个方法被调用了,分别是enc(),j(),i()。enc()是native层的方法,所以在这里就不说了,一会儿具体说,重点看一下i()和j()。
大概可以看出i()使用来显示文本框中的内容,对图片的处理没有影响,这里就不再详细说明。
这个j()方法就有点意思了,这个方法大概是获取APK包签名的MD5值,具体是什么,我们一会儿动态调试获取。
根据enc()方法的声明,可以知道,方法需要三个String类型的参数,这三个参数具体是什么,我们用动态调试的方法获取。
0x03 Java层动态分析
动态调试能够快速获取enc()方法的参数。在这里我们使用Android Studio和插件Smalidea。我们直接在方法调用处下断点:
查看寄存器:
我们看见第一个参数就是图片的绝对路径,第二个参数是加密后的文件的绝对路径,第三个参数就是方法j()获得的MD5值。需要注意的是我这里的APK包是修改过的,所以这里显示的MD5不是未修改包的MD5值,原包的MD5值应该是f8c49056e4ccf9a11e090eaf471f418d。到这里我们就弄清楚了enc()的三个参数,由此得到,enc(filepath,lockpath,signature)。filepath:图片的绝对路径,lockpath:加密后文件绝对路径,signature:APK包签名的MD5值。一般来说,CTF比赛题目的答案就是一个flag(若干个字符组成的字符串)。前面也提到APK包里有一个加密文件,那么flag极有可能就在解密后的文件中,那么我们就需要深入地分析加密过程了。
0x04Native层分析
直接把so文件拖入IDA分析。查看导出表:
很幸运,没有经过混淆,直接可以看到enc()方法的入口。直接F5还原成伪C代码:
在这里我修改了一些函数名,方便分析。通过分析,我们发现sub_51EC7A48是关键的代码段。
int __fastcall sub_51EC7A48(char *filepath, char *lockpath)
{
int *dwords; // r5
int bytes_temp; // r6
char *filepath1; // r9
char *lockpath1; // r8
_DWORD *temp; // r0
_DWORD *temp1; // r6
int signature_array1; // r0
int index; // r1
int v10; // r3
int index1; // r4
int a; // r0
int v13; // r5
int v14; // r1
int v15; // r2
int temp3; // r10
int temp_192; // r0
int signature_array2; // r1
int index2; // r2
int index3; // r4
int b; // r0
int v22; // r1
char i; // r9
char c; // r11
size_t readSize; // r0
size_t readSize1; // r4
_BYTE *readEnd; // r0
int readLast; // r8
int *v29; // r0
int *new_temp; // r11
signed int index4; // r1
int signature_array3; // r0
int result; // r0
FILE *inputstream; //
FILE *outputstream; //
char bytes_temp0; //
char bytes_temp4; //
char bytes_temp8; //
char bytes_temp12; //
char bytes_temp1; //
char bytes_temp5; //
char bytes_temp9; //
char bytes_temp13; //
char bytes_temp2; //
char bytes_temp6; //
char bytes_temp10; //
char bytes_temp14; //
char bytes_temp3; //
char bytes_temp7; //
char bytes_temp11; //
char bytes_temp15; //
int v52; //
filepath1 = filepath;
lockpath1 = lockpath;
if ( !temp2 )
{
temp = malloc(0x180u);
temp1 = temp;
temp2 = (int)temp;
signature_array1 = signature_array;
index = 0;
do
{
v10 = *(unsigned __int8 *)(signature_array1 + index * 4 + 3);
temp1 = _byteswap_ulong(*(_DWORD *)(signature_array1 + index * 4));
++index;
}
while ( index != 4 );
index1 = 0;
a = temp1;
do
{
if ( !((index1 + 4) & 3) )
{
v13 = *(int *)((char *)&dword_51EC9F20 + ((index1 + 3 + ((unsigned int)((index1 + 3) >> 31) >> 30)) & 0xFFFFFFFC));
a = func0(__ROR4__(a, 24)) ^ v13;
}
v14 = temp1;
v15 = (int)&temp1;
a ^= v14;
*(_DWORD *)(v15 + 16) = a;
}
while ( index1 != 40 );
temp3 = temp2;
temp_192 = temp2 + 192;
temp_192_1 = temp2 + 192;
signature_array2 = signature_array;
index2 = 0;
do
{
bytes_temp = (*(unsigned __int8 *)(signature_array2 + index2 + 17) << 16) | (*(unsigned __int8 *)(signature_array2 + index2 + 16) << 24) | (*(unsigned __int8 *)(signature_array2 + index2 + 18) << 8);
*(_DWORD *)(temp_192 + index2) = _byteswap_ulong(*(_DWORD *)(signature_array2 + index2 + 16));
index2 += 4;
}
while ( index2 != 16 );
index3 = 0;
b = *(_DWORD *)(temp3 + 204);
dwords = &dword_51EC9F20;
do
{
if ( !((index3 + 4) & 3) )
{
bytes_temp = *(int *)((char *)&dword_51EC9F20
+ ((index3 + 3 + ((unsigned int)((index3 + 3) >> 31) >> 30)) & 0xFFFFFFFC));
b = func0(__ROR4__(b, 24)) ^ bytes_temp;
}
v22 = temp3 + 4 * index3++;
b ^= *(_DWORD *)(v22 + 192);
*(_DWORD *)(v22 + 208) = b;
}
while ( index3 != 40 );
}
inputstream = fopen(filepath1, (const char *)&r);
if ( inputstream )
{
outputstream = fopen(lockpath1, (const char *)&w);
if ( outputstream )
{
bytes_temp = (int)malloc(0x100u);
lockpath1 = (char *)inputstream;
dwords = (int *)malloc(0x100u);
for ( i = 0; ; ++i )
{
c = *(_BYTE *)(signature_array + (i & 0x1F));
readSize = fread((void *)bytes_temp, 1u, *(unsigned __int8 *)(signature_array + (i & 0x1F)), (FILE *)lockpath1);
readSize1 = readSize;
if ( !readSize )
goto LABEL_31;
if ( readSize <= 0xF )
{
readEnd = (_BYTE *)(bytes_temp + readSize);
readLast = 16 - (readSize1 & 0xF);
if ( (readSize1 & 0xF) != 16 )
{
_aeabi_memset(readEnd, 16 - (readSize1 & 0xF), (unsigned __int8)readLast);
readEnd = (_BYTE *)(readLast + readSize1 + bytes_temp);
}
lockpath1 = (char *)inputstream;
readSize1 = 16;
*readEnd = 0;
}
v29 = &temp_192_1;
if ( !(c & 1) )
v29 = &temp2;
new_temp = (int *)*v29;
bytes_temp0 = *(_BYTE *)bytes_temp;
bytes_temp1 = *(_BYTE *)(bytes_temp + 1);
bytes_temp2 = *(_BYTE *)(bytes_temp + 2);
bytes_temp3 = *(_BYTE *)(bytes_temp + 3);
bytes_temp4 = *(_BYTE *)(bytes_temp + 4);
bytes_temp5 = *(_BYTE *)(bytes_temp + 5);
bytes_temp6 = *(_BYTE *)(bytes_temp + 6);
bytes_temp7 = *(_BYTE *)(bytes_temp + 7);
bytes_temp8 = *(_BYTE *)(bytes_temp + 8);
bytes_temp9 = *(_BYTE *)(bytes_temp + 9);
bytes_temp10 = *(_BYTE *)(bytes_temp + 10);
bytes_temp11 = *(_BYTE *)(bytes_temp + 11);
bytes_temp12 = *(_BYTE *)(bytes_temp + 12);
bytes_temp13 = *(_BYTE *)(bytes_temp + 13);
bytes_temp14 = *(_BYTE *)(bytes_temp + 14);
bytes_temp15 = *(_BYTE *)(bytes_temp + 15);
func1(&bytes_temp0, new_temp);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 4);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 8);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 12);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 16);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 20);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 24);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 28);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 32);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 36);
func2(&bytes_temp0);
func3(&bytes_temp0);
func1(&bytes_temp0, new_temp + 40);
*(_BYTE *)dwords = bytes_temp0;
*((_BYTE *)dwords + 1) = bytes_temp1;
*((_BYTE *)dwords + 2) = bytes_temp2;
*((_BYTE *)dwords + 3) = bytes_temp3;
*((_BYTE *)dwords + 4) = bytes_temp4;
*((_BYTE *)dwords + 5) = bytes_temp5;
*((_BYTE *)dwords + 6) = bytes_temp6;
*((_BYTE *)dwords + 7) = bytes_temp7;
*((_BYTE *)dwords + 8) = bytes_temp8;
*((_BYTE *)dwords + 9) = bytes_temp9;
*((_BYTE *)dwords + 10) = bytes_temp10;
*((_BYTE *)dwords + 11) = bytes_temp11;
*((_BYTE *)dwords + 12) = bytes_temp12;
*((_BYTE *)dwords + 13) = bytes_temp13;
*((_BYTE *)dwords + 14) = bytes_temp14;
*((_BYTE *)dwords + 15) = bytes_temp15;
if ( readSize1 >= 0x11 )
{
index4 = 16;
signature_array3 = signature_array;
do
{
*((_BYTE *)dwords + index4) = *(_BYTE *)(bytes_temp + index4) ^ *(_BYTE *)(signature_array3 + index4 % 32);
++index4;
}
while ( index4 < readSize1 );
}
if ( fwrite(dwords, 1u, readSize1, outputstream) != readSize1 )
break;
}
}
}
result = -1;
while ( _stack_chk_guard != v52 )
{
LABEL_31:
free((void *)bytes_temp);
free(dwords);
fclose((FILE *)lockpath1);
fclose(outputstream);
result = 0;
}
return result;
}
还原出来的伪C代码,并不是完全可信的,但是还是有很大参考价值的。
根据伪C代码我们可以还原出C代码:
int i = 0, j,k;
temp = (unsigned int*)malloc(0x180);
memset(temp,0,0x180);
do {
temp = _byteswap_ulong(*(unsigned int*)(signature + i * 4));
++i;
} while (i != 4);
i = 0;
a = temp;
do {
if (!((i + 4) & 3)) {
a = func0(ror(a, 24)) ^ *(unsigned int*)(dwords + ((i + 3 + ((unsigned int)((i + 3) >> 31) >> 30)) & 0xFFFFFFFC));
}
a ^= temp;
temp = a;
i++;
} while (i != 40);
这段代码是根据前16个字节扩展出40个双字。
还原出C代码:
i = 0;
do {
temp = _byteswap_ulong(*(unsigned int*)(signature + i*4 + 16));
i++;
} while (i != 4);
i = 0;
b = temp;
do {
if (!((i + 4) & 3)) {
b = func0(ror(b, 24)) ^ *(unsigned int*)(dwords + ((i + 3 + ((unsigned int)((i + 3) >> 31) >> 30)) & 0xFFFFFFFC));
}
b ^= temp;
temp = b;
i++;
} while (i != 40);
这段代码是根据后16个字节扩展出40个双字。
这就是密钥扩展出来的88个双字,继续往下看。
这里调用了fopen(),以读模式打开了图片文件,以写模式打开了加密文件。同样可以转换为C代码:
fopen_s(&inputstream,filepath, "rb");
fopen_s(&outputstream,lockpath, "wb");
接下来就是读取文件,在这里,根据signature数组获取特定长度的数据,保存到bytes_temp中。
还原出C代码:
if (readSize <= 0xF) {
readEnd = (unsigned char*)(bytes_temp + readSize);
readLast = 16 - (readSize & 0xF);
if ((readSize & 0xF) != 16) {
memset((void*)readEnd, readLast, readLast);
readEnd = (unsigned char*)readLast + readSize + (unsigned int)bytes_temp;
}
readSize = 16;
*readEnd = 0;
}
这里是处理了长度不足0x10的数据,该代码段的实际作用就是填充不足0x10的部分数据。
还原出C代码:
if (!(c & 1)) { //偶数
new_temp = temp;
}
else { //奇数
new_temp = &temp;
}
//转置
for (j=0;j<4;j++) {
for (k=0;k<4;k++) {
result=bytes_temp;
}
}
这里是根据读取数据长度的奇偶性来决定使用第一段密钥还是第二段密钥。接下来就是把读取数据的前16个字节保存到了新的地方,并进行了转置,也就是保存为了一个4x4矩阵,后面的操作都是基于这个矩阵来进行的。需要注意的是,要看出这里进行了转置,就需要动态调试了。接下来可以看到重复调用几个函数:
func1(&bytes_temp0, new_temp);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 4);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 8);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 12);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 16);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 20);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 24);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 28);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 32);
func2(&bytes_temp0);
func3(&bytes_temp0);
func4((unsigned __int8 *)&bytes_temp0);
func1(&bytes_temp0, new_temp + 36);
func2(&bytes_temp0);
func3(&bytes_temp0);
func1(&bytes_temp0, new_temp + 40);
整理成C代码是:
for (j = 0; j < 9; j++) {
func1(result, new_temp + 4 * j);
func2(result);
func3(result);
func4(result);
}
func1(result, new_temp + 4 * j++);
func2(result);
func3(result);
func1(result, new_temp + 4 * j);
先说func1(),伪C代码是:
整理后
unsigned char *func1(unsigned char* result, unsigned int*new_temp) {
unsigned int temp;
temp=new_temp;
result ^= (temp >> 24);
result ^= (temp >> 16);
result ^= (temp >> 8);
result ^= temp;
temp = new_temp;
result ^= (temp >> 24);
result ^= (temp >> 16);
result ^= (temp >> 8);
result ^= temp;
temp = new_temp;
result ^= (temp >> 24);
result ^= (temp >> 16);
result ^= (temp >> 8);
result ^= temp;
temp = new_temp;
result ^= (temp >> 24);
result ^= (temp >> 16);
result ^= (temp >> 8);
result ^= temp;
/*
简化后:
int i;
for (i=0;i<4;i++) {
result^=new_temp>>24;
result^=new_temp>>16;
result^=new_temp>>8;
result^=new_temp;
}
*/
return result;
}
很显然这里进行了异或操作。然后是func2(),伪C代码是:
整理后是:
unsigned char *func2(unsigned char*result) {
int i;
for (i = 0; i < 16; i++) {
result = s_box[(result & 0xF0) + (result & 0x0F)];
}
return result;
}这里就是S盒的置换操作了。func3()的伪C代码:
整理后:
unsigned char *func3(unsigned char*result) {
unsigned char temp;
temp = result;
result = result;
result = result;
result = result;
result = temp;
temp = result;
result = result;
result = temp;
temp = result;
result = result;
result = temp;
temp = result;
result = result;
result = result;
result = result;
result = temp;
return result;
}
这里进行了正向行移位操作。最后是func4(),伪c代码是:
unsigned __int8 *__fastcall func4(unsigned __int8 *result)
{
int result0; // r2
int result12; // r3
int result8; // lr
int result4; // r12
char result0_byte; // r10
char result12_byte; // r9
char v7; // r6
char v8; // r4
char v9; // r5
int result13; // r6
int result1; // r7
int result5; // r8
int result9; // r3
char result13_byte; // r9
char result13_byte_1; // r11
char result1_byte; // r10
int result14; // r6
int result2; // r7
int result6; // r8
int result10; // r3
char v21; // r9
char v22; // r11
char v23; // r10
int result15; // r6
int result3; // r7
int result7; // r2
int result11; // r3
char v28; // r9
char v29; // r10
result0 = *result;
result12 = result;
result8 = result;
result4 = result;
result0_byte = byte_51EC9920;
result12_byte = byte_51EC9920;
v7 = result12 ^ result8 ^ byte_51EC9920;
v8 = byte_51EC9920;
v9 = byte_51EC9920 ^ result0;
LOBYTE(result0) = result0 ^ result4 ^ byte_51EC9920 ^ byte_51EC9920;
*result = v7 ^ byte_51EC9920;
result = result12 ^ v9 ^ v8;
result = result0;
result = result8 ^ result4 ^ result0_byte ^ result12_byte;
result13 = result;
result1 = result;
result5 = result;
result9 = result;
result13_byte = byte_51EC9920;
result13_byte_1 = byte_51EC9920;
result1_byte = byte_51EC9920;
LOBYTE(result8) = byte_51EC9920;
LOBYTE(result4) = byte_51EC9920;
result = byte_51EC9920 ^ result13 ^ result9 ^ byte_51EC9920;
result = result8 ^ result1 ^ result13 ^ byte_51EC9920;
result = result5 ^ result1 ^ result4 ^ result13_byte_1;
result = result9 ^ result5 ^ result1_byte ^ result13_byte;
result14 = result;
result2 = result;
result6 = result;
result10 = result;
v21 = byte_51EC9920;
v22 = byte_51EC9920;
v23 = byte_51EC9920;
LOBYTE(result8) = byte_51EC9920;
LOBYTE(result4) = byte_51EC9920;
result = byte_51EC9920 ^ result14 ^ result10 ^ byte_51EC9920;
result = result8 ^ result2 ^ result14 ^ byte_51EC9920;
result = result6 ^ result2 ^ result4 ^ v22;
result = result10 ^ result6 ^ v23 ^ v21;
result15 = result;
result3 = result;
result7 = result;
result11 = result;
LOBYTE(result8) = byte_51EC9920;
v28 = byte_51EC9920;
LOBYTE(result6) = byte_51EC9920;
result = byte_51EC9920 ^ result15 ^ result11 ^ byte_51EC9920;
v29 = byte_51EC9920;
result = byte_51EC9920 ^ result3 ^ result15 ^ byte_51EC9920;
result = result7 ^ result3 ^ v29 ^ v28;
result = result11 ^ result7 ^ result6 ^ result8;
return result;
}
整理后:
unsigned char *func4(unsigned char*result) {
unsigned char a,b,c,d;
a=result;
b=result;
c=result;
d=result;
result = func5(c,d,a,b);
result = func5(a,d,b,c);
result = func5(a,b,c,d);
result = func5(b,c,d,a);
a=result;
b=result;
c=result;
d=result;
result = func5(c, d, a, b);
result = func5(a, d, b, c);
result = func5(a, b, c, d);
result = func5(b, c, d, a);
a = result;
b = result;
c = result;
d = result;
result = func5(c, d, a, b);
result = func5(a, d, b, c);
result = func5(a, b, c, d);
result = func5(b, c, d, a);
a = result;
b = result;
c = result;
d = result;
result = func5(c, d, a, b);
result = func5(a, d, b, c);
result = func5(a, b, c, d);
result = func5(b, c, d, a);
/*简化后
int i;
char a,b,c,d;
for (i=0;i<4;i++) {
a=result;
b=result;
c=result;
d=result;
result = func5(c, d, a, b);
result = func5(a, d, b, c);
result = func5(a, b, c, d);
result = func5(b, c, d, a);
}
*/
return result;
}
其实这里就是列混淆,只是这里的实现方法与以往的不同而已。
加密流程结束后,又进行了矩阵的转置操作。最后是:
整理后:
//转置
for (j = 0; j<4; j++) {
for (k = j; k<4; k++) {
if (j != k) {
unsigned char temp = result;
result = result;
result = temp;
}
}
}
if (fwrite(result, 1, readSize, outputstream) != readSize) {
break;
}
这里是对读取数据位置大于0x10的数据进行异或操作,最后写入到文件里。到这里这块数据的加密流程就分析完毕了。在这里我们总结一下加密的流程,首先是读取一个特定长度的数据块,然后取出前16个字节进行AES加密,再把剩余的数据进行异或操作,这样一块数据就加密完毕了。对每块数据进行相同的操作,最后得到加密文件。整个加密流程大致就是这样。我们已经知道了加密流程,那么解密方法就很容易得到了,这些代码我都会放到文章最后地附件中。
0x05写在最后
这样我们就可以解密出加密图片了,最后附上结果图:
附件:https://pan.baidu.com/s/1qrKwTZEqHuajOpADhaPk0A 密码:59dn版权声明:允许转载,但是一定要注明出处。
panwei103012571 发表于 2018-5-29 09:52
这个算不算是因写,如果是可以把图片导出来,再电脑16进制打开,16进制修改图片大小,有可能也可以,以前试 ...
这个不是隐写,程序是涉及到AES加密的 skywilling 发表于 2018-5-29 22:44
S盒只是识别特征的一种,有一些修改过的AES加密是会使用非标准S盒的(这个你可以参考我以前分析的题目) ...
小白请教一下:
能否用简短的语言(打比方也可以)描述一下此贴的AES加密和RAR包的加密方式有何不同?
如果有足够的人手,比如100人小团队,每个人能力都和你一样,是不是也能逐步分析并破解一个RAR包呢?(假设此RAR包用12位密码加密,有数字和大小写字母,不含特殊字符)
门外汉的一点好奇,如果问得不妥,请无视,谢谢。 楼主厉害啊,加油 不错不错 来学习下,写得很详细,感谢分享~{:301_978:} 看不懂 但是知道楼主是个高手 楼主高手哦 怎么详细的教程 写的很详细,学习了,谢谢! 大佬哇大佬 大佬哇大佬 完全看不懂啊