首页
论坛
课程
招聘
通过2015年AliCrackMe学习Android逆向(一)
2021-7-26 12:45 11818

通过2015年AliCrackMe学习Android逆向(一)

2021-7-26 12:45
11818

总感觉以前学的 Android 逆向不扎实,现在通过这五道题复习一下。

AliCrackme_1

1. 运行题目

MuMu模拟器

 

需要输入正确的密码。

 

img

2. 反编译分析

首先对apk文件进行反编译,找到登录按钮所在Activity(MainActivity)的代码并分析。

 

img

  • table 是使用方法 getTableFromPic 得到的一张字符表;
  • pw 是使用方法 getPwdFromPic 通过 table 进行编码后的正确密码

  • enPassword 是对在输入框中输入的字符串进行编码的结果。

3. 泄漏 tablepw

方法一:ddms/adb logcat

它们都会通过Log打印出来,如上图中红框所示。所以,我们可以利用 DDMS 或者 adb logcat 查看log 。通过在APP中随便输入字符串,点击“登录”按钮,就能从Logcat中查看到打印值。如下图所示,输入“123456”。

 

img

 

后面写一个脚本,使用MainActivity中本身提供的解码方法,传入参数 tablepw ,就能得到正确密码。

方法二:逆向分析

如果没有Log.d方法,该怎么办呢?

 

前面已经分析出 tablepw 分别是通过方法 getTableFromPicgetPwdFromPic得到的,那么就分析这两个方法。它们都是通过对assets目录下的logo.png图片进行计算得到返回值。那么就可以创建一个Android项目,把logo.png拷贝到新项目的assets目录下。然后将getTableFromPicgetPwdFromPic拷贝到AndroidTest目录下的ExampleInstrumentedTest文件中。

 

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.lzx.ufo;
 
import android.content.Context;
import android.util.Log;
 
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
 
import org.junit.Test;
import org.junit.runner.RunWith;
 
import java.io.IOException;
import java.io.InputStream;
 
import static org.junit.Assert.*;
 
/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
        assertEquals("com.lzx.ufo", appContext.getPackageName());
 
        Log.d("UFO======",getTableFromPic(appContext));
        Log.d("UFO======",getPwdFromPic(appContext));
    }
 
    protected String getTableFromPic(Context appContext) {
        InputStream is = null;
        String value = "";
        try {
            is = appContext.getResources().getAssets().open("logo.png");
            int lenght = is.available();
//            Log.d("UFO","Table-lenght:"+lenght);
 
            byte[] b = new byte[lenght];
            is.read(b, 0, lenght);
            byte[] data = new byte[768];
            System.arraycopy(b, 89473, data, 0, 768);
            String value2 = new String(data, "utf-8");
            if (is == null) {
                return value2;
            }
            try {
                is.close();
                return value2;
            } catch (IOException e) {
                return value2;
            }
        } catch (Exception e2) {
            e2.printStackTrace();
            if (is == null) {
                return value;
            }
            try {
                is.close();
                return value;
            } catch (IOException e3) {
                return value;
            }
        } catch (Throwable th) {
            if (is != null) {
                try {
                    is.close();
 
                } catch (IOException e4) {
 
                }
            }
        }
        return value;
    }
 
    protected String getPwdFromPic(Context appContext) {
 
        InputStream is = null;
        String value = "";
        try {
            is = appContext.getResources().getAssets().open("logo.png");
            int lenght = is.available();
 
//            Log.d("UFO","Pwd-lenght:"+lenght);
 
            byte[] b = new byte[lenght];
            is.read(b, 0, lenght);
            byte[] data = new byte[18];
            System.arraycopy(b, 91265, data, 0, 18);
            String value2 = new String(data, "utf-8");
            if (is == null) {
                return value2;
            }
            try {
                is.close();
                return value2;
            } catch (IOException e) {
                return value2;
            }
        } catch (Exception e2) {
            e2.printStackTrace();
            if (is == null) {
                return value;
            }
            try {
                is.close();
                return value;
            } catch (IOException e3) {
                return value;
            }
        } catch (Throwable th) {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e4) {
                }
            }
        }
        return value;
    }
}

img

4. 编写脚本计算flag

方法一:拷贝题目中的解码方法,使用Java代码解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Answer{
    public static void main(String[] args){
 
        String table = "一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐";
        String pwd = "义弓么丸广之";
        System.out.println(aliCodeToBytes(table,pwd));
    }    
    private static String aliCodeToBytes(String codeTable, String strCmd) {
        StringBuilder sb = new StringBuilder();
        byte[] cmdBuffer = new byte[strCmd.length()];
        for (int i = 0; i < strCmd.length(); i++) {
                cmdBuffer[i] = (byte) codeTable.indexOf(strCmd.charAt(i));
                sb.append((char)cmdBuffer[i]);
        }
        return sb.toString();
    }
}

img

方法二:根据解码方法,编写对应的Python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
table = [
    '一', '乙', '二', '十', '丁', '厂', '七', '卜', '人', '入', '八', '九', '几', '儿', '了', '力',
    '乃', '刀', '又', '三', '于', '干', '亏', '士', '工', '土', '才', '寸', '下', '大', '丈', '与',
    '万', '上', '小', '口', '巾', '山', '千', '乞', '川', '亿', '个', '勺', '久', '凡', '及', '夕',
    '丸', '么', '广', '亡', '门', '义', '之', '尸', '弓', '己', '已', '子', '卫', '也', '女', '飞',
    '刃', '习', '叉', '马', '乡', '丰', '王', '井', '开', '夫', '天', '无', '元', '专', '云', '扎',
    '艺', '木', '五', '支', '厅', '不', '太', '犬', '区', '历', '尤', '友', '匹', '车', '巨', '牙',
    '屯', '比', '互', '切', '瓦', '止', '少', '日', '中', '冈', '贝', '内', '水', '见', '午', '牛',
    '手', '毛', '气', '升', '长', '仁', '什', '片', '仆', '化', '仇', '币', '仍', '仅', '斤', '爪',
    '反', '介', '父', '从', '今', '凶', '分', '乏', '公', '仓', '月', '氏', '勿', '欠', '风', '丹',
    '匀', '乌', '凤', '勾', '文', '六', '方', '火', '为', '斗', '忆', '订', '计', '户', '认', '心',
    '尺', '引', '丑', '巴', '孔', '队', '办', '以', '允', '予', '劝', '双', '书', '幻', '玉', '刊',
    '示', '末', '未', '击', '打', '巧', '正', '扑', '扒', '功', '扔', '去', '甘', '世', '古', '节',
    '本', '术', '可', '丙', '左', '厉', '右', '石', '布', '龙', '平', '灭', '轧', '东', '卡', '北',
    '占', '业', '旧', '帅', '归', '且', '旦', '目', '叶', '甲', '申', '叮', '电', '号', '田', '由',
    '史', '只', '央', '兄', '叼', '叫', '另', '叨', '叹', '四', '生', '失', '禾', '丘', '付', '仗',
    '代', '仙', '们', '仪', '白', '仔', '他', '斥', '瓜', '乎', '丛', '令', '用', '甩', '印', '乐'
]
 
pw= ['义','弓','么','丸','广','之']
 
flag = ''
pw_len = 0
 
print(len(table))
print(len(pw))
 
for i in range(len(pw)):
    for j in range(len(table)):
        if table[j] == pw[i]:
            flag += chr(j)
            break
 
print(flag)

img

AliCrackme_2

1. 运行题目

Nexus 5 , Android 4.4.4

 

image-20210726123525520

2. 反编译分析

jadx反编译,发现校验方法 securityCheck 是一个native方法。

 

img

 

于是,使用ida打开文件 libcrackme.so ,找到securityCheck对应的JNI函数 Java_com_yaotong_crackme_MainActivity_securityCheck 。修改一些变量名和变量类型(a1的类型修改为JNIEnv*)。

 

img

 

从返回值往前分析,我们需要返回true,也就是1,那就需要输入的字符串和off_628C存储的字符串相等。

 

img

 

img

 

wojiushidaan 输入,显示输入密码错误。因此,可以猜测某个地方在执行while循环之前修改了变量 aWojiushidaan 的值。

 

到这,目的就很明确了,找到变量aWojiushidaan 变换后的值。

3. 泄漏aWojiushidaan变换后的值,得到flag

方法一:frida hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// readflag.js
function hook_lib(){
    var so_name = "libcrackme.so"; // lib名称
    var flag_offset = 0x00004450// flag,也就是aWojiushidaan变量的地址
    var so_base = Module.findBaseAddress(so_name); // lib基地址
    var security_check = Module.findExportByName(so_name,"Java_com_yaotong_crackme_MainActivity_securityCheck"); // jni函数地址
    var flag_addr = parseInt(so_base, 16)+flag_offset; // 计算flag的真实地址
    var flag_ptr = new NativePointer(flag_addr);      // 转换为nativepointer
    console.log("libcrackme.so base addr: ",so_base);
    console.log("flag addr: ", flag_addr);
    console.log("function security_check addr: ",security_check);
 
    Interceptor.attach(security_check,{
        onEnter: function(args){    // jni函数进入时
            console.log("---- enter ----");
            var flag0 = flag_ptr.readByteArray(0x20); // 打印内存中的值
            console.log(hexdump(flag0,{
                offset: 0,
                length: 0x20,
                header: true,
                ansi: false
            }))
            console.log("---- enter end ----");
        },
        onLeave: function(retval){  // jni函数返回时
            var flag = flag_ptr.readByteArray(0x20);  //打印内存中的值
 
            console.log("---- leave ----");
            console.log(hexdump(flag,{
                offset: 0,
                length: 0x20,
                header: true,
                ansi: false
            }));
            console.log("---- leave end ----");
        }
    });
}
 
function main(){
    hook_lib();
}
 
setImmediate(main);
 
// frida -R -l readflag.js com.yaotong.crackme

img

 

得到结果: aiyou,bucuoo

P.S. 通过结果可以看出:变量aWojiushidaan 在进入函数 Java_com_yaotong_crackme_MainActivity_securityCheck 之前就已经被改变了。和参考文献中https://www.secpulse.com/archives/5731.html中所说的是函数 Java_com_yaotong_crackme_MainActivity_securityCheck中的前面部分改变了变量 aWojiushidaan 有出入。而在后面的动态调试过程中可以看到,aWojiushidaan的值确实在执行函数Java_com_yaotong_crackme_MainActivity_securityCheck 之前就已经被改变。

方法二:patch so文件

函数Java_com_yaotong_crackme_MainActivity_securityCheck中,比较之前有一个 _android_log_print 函数。可以修改打印的内容,直接利用这个函数输出此时 aWojiushidaan 的值。如下图所示,当点击按钮时,会输出: I yaotong : SecurityCheck Started...

 

img

 

查看汇编代码,如下图所示。在0x12A4地址处正好将 aWojiushidaan 的值赋给寄存器R2,所以将0x1284~0x129C的内容(也就是调用GetStringUTFChars部分)修改为NOP。然后因为R1是第二个参数,为了在输出的时候Log的TAG不变,这里将0x12A0和0x12A4中的R1修改为R3。

 

img

 

下面开始patch:

 

1.使用apktool反编译

 

img

 

2.在IDA中F2修改,确认修改正确后,使用010Editor进行patch。

 

img

 

img

 

3.回编译,签名

1
2
3
4
apktool b AliCrackme_2_patch # 回编译
cd AliCrackme_2_patch/dist/
keytool -genkey -alias test1.keystore -keyalg RSA -validity 1000000 -keystore test.keystore # 生成keystore文件
jarsigner -verbose -keystore test.keystore -signedjar AliCrackme_2_signed.apk AliCrackme_2.apk test1.keystore # 签名

4.卸载原来的app,重新安装,运行,查看日志。

1
2
3
adb install AliCrackme_2_signed.apk
adb shell ps | grep com.yaotong.crackme
adb logcat --pid=xxx

image.png

方法三:frida hook2

做完之后,搜了一下这一题的答案,发现不用这么复杂。。。

 

https://blog.csdn.net/weixin_42011443/article/details/105897429

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook(){
 
    Java.perform(function(){
        var so_name = "libcrackme.so"; // lib名称
        var flag_offset = 0x0000628C// off_628C的地址
        var so_base = Module.findBaseAddress(so_name); // lib基地址
        var flag = Memory.readUtf8String(Memory.readPointer(so_base.add(flag_offset)));
        console.log(flag);
    });
}
 
function main(){
    hook();
}
 
setImmediate(main);

img

方法四:动态调试(过反调试)

1.首先从AndroidManifest.xml中发现没有将android:debuggable属性设置为true。这里不再用apktool等命令行工具反编译、回编译和签名了。用AndroidKiller工具来操作:a)添加android:debuggable属性,并设置为true;2)菜单->Android->编译。

 

img

 

生成的新apk如下:

 

img

 

2.安装新apk

1
adb install AliCrackme_2_killer.apk

3.push android_server,修改权限,运行

1
2
3
4
5
6
adb push android_server /data/local/tmp/
adb shell
su # 注意需要切换root用户,不然后面attach process的时候只会有两个进程,会找不到目标进程
cd /data/local/tmp/
chmod 777 android_server
./android_server

4.另开一个终端,转发端口

1
adb forward tcp:23946 tcp:23946

5.用IDA打开libcrackme.so,设置调试选项

 

Debugger -> select debugger...

 

img

 

Debugger->Debugger options... 勾选三个

 

img

  • Suspend on process entry point :在进程入口点挂起
  • Suspend on thread start/exit:在线程开始/退出时挂起

  • Suspend on library load/unload:在库加载/卸载时挂起

Debugger->Process options...

 

img

 

6.在手机上启动app,然后在IDA中attach进程。

 

IDA:Debugger->Attach to process ... ,选择要调试的进程名,确认。会弹出一个对话框,让你确认so文件是否一样,选择“same”。

 

img

 

进入调试界面后,它会断在libc.so中。attach之后,总是会停在这里的,因为libc.so 是 native层中最基本的函数库,所有上层的调用都会经过libc。

 

img

 

7.找到函数Java_com_yaotong_crackme_MainActivity_securityCheck。找这个函数的方法:

 

在Module list窗口(Debugger->Debugger windows->Module list)中找到libcrackme.so,双击它。

 

img

 

然后在打开的新窗口里面找到这个函数,双击它。

 

img

 

双击之后进入函数实现。

 

img

 

找到函数Java_com_yaotong_crackme_MainActivity_securityCheck地址的另一个方法是另开一个ida实例,找到该函数的偏移地址,然后通过ctrl + s找到libcrackme.so的基址,两者相加就得到了它的地址。但是不知道为什么,我这里ctrl + s 找不到该so文件的基址。

 

img

 

8.找到while循环要比较的地方,发现flag。下图红框框中的指令对应while循环中第一行代码,取v6中的内容,也就是flag。

 

img

 

虽然这里已经拿到flag了,但是这一题有反调试,为了学习的目的,还是继续看看怎么过反调试。

过反调试

1.查看反调试效果。在上图红框地方(0x750982A8 LDRB R3, [R2])下断点,按三下F9按钮,程序直接退出了,所以这里是做了反调试检测。

 

img

 

2.so文件加载的过程如下所示。在加载so文件时,.init和.init_array两个section会做初始化工作:先执行.init段中的代码,然后顺序执行.init_array中的函数。.init_array是函数指针数组,会按照在源码中声明的顺序将各个相应的函数填到该数组中。接着,JNI_OnLoad在so被 System.loadLibrary 调用的时候执行,它的执行时刻晚于.init_array,早于native方法的执行。

1
.init -> .init_array -> JNI_Onload -> java_com_xxx

先在JNI_Onload下断点,看看反调试检测是不是在这里做的。调试的前面几步准备步骤和之前一样:

 

a.root权限执行android_server

 

b.端口转发:adb forward tcp:23946 tcp:23946

 

c.ida中和前面一样设置调试选项,注意Debugger options中三个选项要勾上

 

3.接下来就不同了,因为要在JNI_OnLoad下断点,而被调试程序如果一运行就会执行 static 中的语句,所以需要以调试模式启动app,让程序停在加载so文件之前。

1
adb shell am start -D -n com.yaotong.crackme/.MainActivity

img

 

4.attach进程,在module list窗口搜索libcrackme.so,发现没有,说明程序确实停在加载该so文件之前。

 

img

 

5.连接jdb,然后 F9 运行。

1
2
3
adb shell ps | grep com.yaotong.crackme # 查看pid
adb forward tcp:8700 jdwp:4134 # 端口转发,jdwp:上一步找到的pid
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 # jdb连接

img

 

运行程序,连接成功的同时,IDA会成功加载程序,PC跳转到linker模块:

 

img

 

6.同样,通过module list窗口,找到so文件,然后找到JNI_Onload函数,下断点,然后F9运行到这里。

 

img

 

7.F8 单步步出 调试,发现每次在执行到BLX R7的时候,程序就会退出,所以反调试检测应该在这。查看此时的R7寄存器,它的值是pthread_create函数,所以是创建了一个新线程来进行检测。

 

img

 

点击这条指令,tab键(F5也可以,我更喜欢tab)切换到C伪代码。所以,off_754512B4是pthread_create函数,那么第三个参数sub_7544C6A4就是线程执行的函数,反调试基本就在这里面了。继续分析下去。

 

双击off_754512B4,发现它是个函数指针?没有啥想法,先放放,继续往下走。

 

img

 

这里因为经过了动态调试,所以函数名中sub后面接的地址很大,如果是另开一个ida实例进行静态分析,sub后面接的就是该函数在so文件中的偏移地址,比如这里sub_7544C6A4就会是sub_16A4。

 

img

 

8.结束调试,开始静态分析。进入函数sub_7544C6A4,是个死循环,里面执行两个函数。

 

img

 

双击进入sub_7544C30C,我懵了,除了_aeabi_memset 其他都不认识。。。然后双击进入函数off_754512B0,又是个函数指针。我知道反调试应该就在sub_7544C6A4,但是我看不懂。。。回到JNI_Onload函数中,再看看有没有别的相关函数,下一个执行的函数是sub_7544C7F4,双击进去,还是看不懂,但是发现很多jolin函数。这时候我知道靠我自己这个菜鸡进行静态分析是不行了,那就试试动态调试吧。

 

9.动态调试。从BLX R7 F7进去,一步一步调试,看看到底是哪里退出程序的。经过两次调试,发现在函数sub_7544C30C执行完loc_75245600这个块之后就会退出程序,可以看到这个块最后是跳转到kill函数去执行。

 

img

 

对应伪代码如下图所示,所以,如果我们将BGE loc_75245600改成BLT loc_75245600 或者 nop就可以改变流程,不执行kill函数,而是直接返回。

 

img

 

选中该指令,切换到Hex View,F2修改,将指令改成nop( 00 00 A0 E1),然后F2保存。同时,另开一个IDA实例,找到该指令的偏移0x15D8(因为原来那个经过动态调试,指令地址已经不是偏移了)。用010Editor修改,然后回编译、签名、重新安装,在函数Java_com_yaotong_crackme_MainActivity_securityCheck里循环中第一条指令LDRB R3, [R2]处下断点,attach进程之后,点击“输入密码”按钮,然后F9运行到断点,如下图所示,说明成功绕过反调试,并可以看到此时变量aWojiushidaan的值为flag。

 

img

 

img

 

10.静态分析。找到一篇大佬写的 2015年AliCrackMe第二题的分析之人肉过反调试 ,分析得特别好。

 

原来题目在.init_array段中通过dlsym获取动态库的函数指针,将所有用到的函数都用函数指针保存起来,隐藏了函数本身的样式。解密出这些函数之后,可以在sub_7544C30C函数中发现getpid、kill,结合pthread_cretae函数,可以推断出是通过检测TracerPid来进行反调试。值得一提的是在函数中kill的实现是这样的:向对应pid发送9(SIGKILL)信号,以干掉进程。

1
2
3
(*(void (__fastcall **)(int, signed int))((char *)&GLOBAL_OFFSET_TABLE_ + (_DWORD)v3 + (unsigned int)&dword_1C))(
  v0,
  9);

jolin函数的作用就是修改了真正的密码,将wojiushidaan改成了flag。但是它被加密了,而JNI_Onload中pthread_create函数后面的sub_7544C7F4的作用就是解密执行它。

 

简单说一下这种反调试检测的原理:当一个进程没有被调试时,它对应/proc/<pid>/status文件中TracerPid字段的值是0;当该进程被调试时,TracerPid会被写入调试进程的pid,当然这里就是android_server的pid了。

 

对安卓反调试和校验检测的一些实践与结论 总结了很多反调试手段。

 

现在程序反调试的过程很清晰了,就是在sub_7544C6A4里面,只要不执行它,也能绕过反调试。所以,将BLX R7修改成nop( 00 00 A0 E1),找到该指令的偏移地址0x1C58,然后用010Editor修改它。

 

接着,回编译,签名,重新安装。在函数Java_com_yaotong_crackme_MainActivity_securityCheck里循环中第一条指令LDRB R3, [R2]处下断点,attach进程之后,点击“输入密码”按钮,然后F9运行到断点。同样绕过反调试成功。

 

img

参考文献


【公告】【iPhone 13、ipad、iWatch】11月15日中午12:00,看雪·众安 2021 KCTF秋季赛 正式开赛【攻击篇】!!!文末有惊喜~

最后于 2021-7-27 09:52 被直木编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (6)
雪    币: 10515
活跃值: 活跃值 (2748)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
LowRebSwrd 活跃值 4 2021-7-26 19:37
2
0
雪    币: 213
活跃值: 活跃值 (693)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2021-7-27 13:29
3
0
感谢总结分享,收藏了
雪    币: 0
活跃值: 活跃值 (225)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
菜鸟也想飞 活跃值 2021-7-28 17:37
4
0
感谢分享
雪    币: 9
活跃值: 活跃值 (241)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
ele7enxxh 活跃值 1 2021-7-30 14:43
5
0
青春呀,当年熬通宵做题
雪    币: 183
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
猫的男人 活跃值 2021-8-18 17:00
6
0
感谢总结分享,收藏了
游客
登录 | 注册 方可回帖
返回