首页
论坛
课程
招聘
从inlinehook角度检测frida
2021-10-19 14:54 22212

从inlinehook角度检测frida

2021-10-19 14:54
22212

从inlinehook角度检测frida

总述

在使用frida的过程中,在我的认知里其实frida只做了2件事情,一件是注入,一件是hook,作为注入的特征我们可以通过ptrace(PTRACE_TRACEME,NULL,0,0),或者从文件里面索引有关frida字符串这样的方式来检测frida,那么作为hook特征是否也能够检测frida呢?

frida inline hook写法

答案是肯定的,既然frida是能够定向跳转从而更改内存中的代码,那么一定是用到了inlinehook的技术,那么它是怎么使用的inlinehook呢,我们可以使用ida(当然直接看frida源码,但是那样真的很麻烦就连我自己写的inlinehook都很难快速的分析清楚)去看一下,我随便hook了一个函数比如art::ClassLinker::LoadMethod,注意要先附加frida然后再使用ida链接,否则frida无法附加已经被ptrace附加的进程,然后,结果如下图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function main() {
    var libart = Module.enumerateSymbols("libart.so");
    var addr = NULL;
    for (var n in libart) {
        if (libart[n].name.indexOf("lassLinker10LoadMethodERKNS") >= 0) {
            addr = libart[n].address;
            break;
        }
    }
    Interceptor.attach(addr, {
        onEnter: function (arg) {
 
        }
 
    })
 
}


可以看到frida的hook原理就是将函数的开头改成这样的一串16进制,0xd61f020058000050和我之前写的inlinehook差不多,只是它用了x16寄存器我用了x17寄存器,那么这里就有一个思路了,我们能否通过so中是否有这一段来判断用户使用了frida呢?答案是肯定的,但是需要我们稍作调整,就是在每个函数的开头检测是否有0xd61f020058000050这样的一段代码,创建线程即可,那么下面就开始实现,首先试一下单个函数能不能成功,我使用了上篇文章提到的,提取符号首地址的方式findsym

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void anti1(){
while (1) {
    sleep(1);
    int so = findsym("/system/lib64/libart.so",
                     "_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE");
 
    long long as = *(long long *) reinterpret_cast<long>((char *) startr - 0x25000 + so);
 
    if(as==0xd61f020058000050) {
        __android_log_print(6, "r0ysue", "i find frida %p",(long long *) reinterpret_cast<long>((char *) startr - 0x25000 + so));
    }
}
 
}
    pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(anti1), nullptr);

检测libart是否使用inlinehook

可以看到当我以attach的方式链接上去的时候,成功的检测到了frida,那么其实我们就可以遍历符号表来获得一个so中所有的函数首地址来检测是否使用了frida,由于之前也说过,某些函数不在导出表中,而在符号表中,所以可能没有办法通过程序头获取,所以系统so这方面用节头获取,自身so方面用程序头获取(自身so路径不太好确定,所以直接用内存中的就好),这里以libart.so和libnative-lib.so来做一个例子。首先就是libart.so,这里我封装了2个函数一个是获得所有符号的首地址,一个是获得大小都是通过节头表索引得到的,其中enumsym这个函数用到了2个int,第一个size是最大值,第二个start1是最小值,通过外部传入,因为有些符号表的值大于文件所以容易溢出

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
int* enumsym(const char* lib,int size,int start1){
    int fd;
    void *start;
    struct stat sb;
    fd = open(lib, O_RDONLY);
    fstat(fd, &sb);
    start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    Elf64_Ehdr header;
    memcpy(&header, start, sizeof(Elf64_Ehdr));
    int secoff = header.e_shoff;
    int secsize = header.e_shentsize;
    int secnum = header.e_shnum;
    int secstr = header.e_shstrndx;
    Elf64_Shdr strtab;
    memcpy(&strtab, (char *) start + secoff + secstr * secsize, sizeof(Elf64_Shdr));
    int strtaboff = strtab.sh_offset;
    char strtabchar[strtab.sh_size];
    memcpy(&strtabchar, (char *) start + strtaboff, strtab.sh_size);
    Elf64_Shdr enumsec;
    int gotoff = 0;
    int gotsize = 0;
    int strtabsize = 0;
    int stroff = 0;
    for (int n = 0; n < secnum; n++) {
        memcpy(&enumsec, (char *) start + secoff + n * secsize, sizeof(Elf64_Shdr));
        if (strcmp(&strtabchar[enumsec.sh_name], ".symtab") == 0) {
            gotoff = enumsec.sh_offset;
            gotsize = enumsec.sh_size;
        }
        if (strcmp(&strtabchar[enumsec.sh_name], ".strtab") == 0) {
            stroff = enumsec.sh_offset;
            strtabsize = enumsec.sh_size;
 
        }
    }
    int realoff=0;
    char relstr[strtabsize];
    Elf64_Sym tmp;
    memcpy(&relstr, (char *) start + stroff, strtabsize);
int* sdc= static_cast<int *>(malloc(gotsize / sizeof(Elf64_Sym)*sizeof(int *)));//存储返回值数组
    for (int n = 0; n < gotsize; n = n + sizeof(Elf64_Sym)) {
        memcpy(&tmp, (char *)start + gotoff+n, sizeof(Elf64_Sym));
//        __android_log_print(6, "r0ysue", "%x",gotoff+n);
            sdc[n/sizeof(Elf64_Sym)]=tmp.st_value;
            if(tmp.st_value>size||tmp.st_value<start1)
                sdc[n/sizeof(Elf64_Sym)]=start1;
    }
    return sdc;
}
 
int getsymsize(const char* lib){
    int fd;
    void *start;
    struct stat sb;
    fd = open(lib, O_RDONLY); /*打开/etc/passwd */
    fstat(fd, &sb); /* 取得文件大小 */
    start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    Elf64_Ehdr header;
    memcpy(&header, start, sizeof(Elf64_Ehdr));
    int secoff = header.e_shoff;
    int secsize = header.e_shentsize;
    int secnum = header.e_shnum;
    int secstr = header.e_shstrndx;
    Elf64_Shdr strtab;
    memcpy(&strtab, (char *) start + secoff + secstr * secsize, sizeof(Elf64_Shdr));
    int strtaboff = strtab.sh_offset;
    char strtabchar[strtab.sh_size];
    memcpy(&strtabchar, (char *) start + strtaboff, strtab.sh_size);
    Elf64_Shdr enumsec;
    int gotoff = 0;
    int gotsize = 0;
    int strtabsize = 0;
    int stroff = 0;
    for (int n = 0; n < secnum; n++) {
 
        memcpy(&enumsec, (char *) start + secoff + n * secsize, sizeof(Elf64_Shdr));
 
 
        if (strcmp(&strtabchar[enumsec.sh_name], ".symtab") == 0) {
            gotoff = enumsec.sh_offset;
            gotsize = enumsec.sh_size;
 
        }
        if (strcmp(&strtabchar[enumsec.sh_name], ".strtab") == 0) {
            stroff = enumsec.sh_offset;
            strtabsize = enumsec.sh_size;
 
        }
 
 
    }
    return gotsize/sizeof(Elf64_Sym);
 
}

之后把这两个函数调用写入线程函数就好了,我这里直接用0x25000了通过libart.so的程序头可以解析出来,每10秒检测一次

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
    int *so ;
    int size ;//写到全局变量里面循环就不会多次调用了
 
void anti1(){
while (1) {
    pthread_mutex_lock(&mutex);
 
 
    for (int n = 0; n < size; n++) {
//    __android_log_print(6, "r0ysue", "i find frida %p %x %x",(long long ) reinterpret_cast<long>((char *) startr - 0x25000 + so[n]),so[n],n);
 
        long long as = *(long long *) reinterpret_cast<long>((char *) startr - 0x25000 + so[n]);
//        __android_log_print(6, "r0ysue", "i find frida %d %x %p",n,so[n],as);
        if (as == 0xd61f020058000050) {
            __android_log_print(6, "r0ysue", "i find frida %p",
                                (long long *) reinterpret_cast<long>((char *) startr - 0x25000 +
                                                                     so[n]));
        }
    }
    sleep(5);
    pthread_mutex_unlock(&mutex);
}
}
void __init(){
 
 
     so = enumsym("/system/lib64/libart.so", (long) end - (long) startr,0x25000);
     size = getsymsize("/system/lib64/libart.so");
}

看一下结果,效果不错就是检测的慢一点但是也达到了预定的目标

检测libnative-lib是否使用inlinehook

接着我们继续搞libnative-lib,这个由于目录特殊性所以我们很难找到它的目录从节头里面搜索符号地址,所以我们只能用程序头搜索它的导出函数,这个之前的文章也讲过,这里就直接贴代码了,这里注意一点,如何确定符号表的大小,仔细观察就知道符号表后面就是字符串表,那么用符号表偏移减去字符串表偏移就是符号表的大小

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
void initanti4(){//用来初始化符号表数组和大小
    char line[1024];
    int *startr;
    int *end;
 
 
    int n=1;
    FILE *fp=fopen("/proc/self/maps","r");
    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, "libnative-lib.so") ) {
            __android_log_print(6,"r0ysue","");
            if(n==1){
                startr = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
                end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
            }
            else{
                strtok(line, "-");
                end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
            }
            n++;
        }
    }
 
 
 
    int phof = 0;
 
 
    Elf64_Ehdr header;
    memcpy(&header, startr, sizeof(Elf64_Ehdr));
    uint64 rel = 0;
    size_t size = 0;
    long *plt = nullptr;
 
 
    char *strtab_ = nullptr;
    Elf64_Sym *symtab_ = nullptr;
    Elf64_Phdr cc;
 
    memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));
 
    for (int y = 0; y < header.e_phnum; y++) {
        memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
               sizeof(Elf64_Phdr));
        if (cc.p_type == 6) {
 
            phof = cc.p_paddr - cc.p_offset;
 
 
        }
 
    }
 
 
    for (int y = 0; y < header.e_phnum; y++) {
        memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
               sizeof(Elf64_Phdr));
        if (cc.p_type == 2) {
            Elf64_Dyn dd;
            for (y = 0; y == 0 || dd.d_tag != 0; y++) {
                memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,
                       sizeof(Elf64_Dyn));
 
                if (dd.d_tag == 5) {
                    strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);
                }
                if (dd.d_tag == 6) {
                    symtab_ = reinterpret_cast<Elf64_Sym *>((
                            (char *) startr + dd.d_un.d_ptr - phof));
                }
 
 
 
            }
 
 
        }
 
 
    }
libnative= static_cast<long *>(malloc(8*((long) (strtab_) - (long) symtab_) / sizeof(Elf64_Sym)));
 
for (int n=0;n<((long)(strtab_)-(long)symtab_)/sizeof(Elf64_Sym);n=n+ 1){
 
    Elf64_Sym * s = symtab_ + n;
    libnative[n]= (long)((char *) startr + s->st_value);
    __android_log_print(6,"r0ysue","%p",*(long*)libnative[n]);
}
 
    libsz=((long)(strtab_)-(long)symtab_)/sizeof(Elf64_Sym);
 
 
 
}
 
void anti4(){//主要的线程启动的函数用来检测frida用了全局变量
 
 
    while (1) {
        pthread_mutex_lock(&mutex);
 
        __android_log_print(6, "r0ysue", "i find frida 1");
 
        for (int n = 0; n < libsz; n++) {
 
 
            long long as = *(long long *) reinterpret_cast<long>(libnative[n]);
//        __android_log_print(6, "r0ysue", "i find frida %d %x %p",n,so[n],as);
            if (as == 0xd61f020058000050) {
                __android_log_print(6, "r0ysue", "i find frida %p",
                                    (long long *) reinterpret_cast<long>(libnative[n]));
            }
        }
        sleep(5);
        pthread_mutex_unlock(&mutex);
    }
 
}
void __init(){
 
 
initanti4();
    pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(anti4), nullptr);
 
}

使用frida脚本hook Java_com_r0ysue_antifrida_MainActivity_stringFromJNI,看一下效果

1
2
3
4
5
6
7
8
9
10
function main() {
var fromjni=Module.findExportByName("libnative-lib.so","Java_com_r0ysue_antifrida_MainActivity_stringFromJNI");
Interceptor.attach(fromjni,{
 
onEnter:function(arts){
console.log(arts);
}
 
})
}

从java hook 的角度检测frida

上篇文章说了Java hook,的做法是将Java函数转化为Native函数,所以我们可以在关键函数上检测是否是Native的方法来检测frida,当然也可以用解析dex的方式来获得所有的类表和所有的函数名,我这里就先不搞了,有机会下篇文章再搞,我这里只提供一个最简单的方式,就是通过Methid来获得ArtMethod的方法检测是否是Native函数,具体做法可以参考上一篇文章,我这里直接贴代码了,我这里使用反射,但是注意由于线程不同所以env不同,不能将主线程的env传入检测线程使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void anti2(__int64 a1){
    while (1) {
        sleep(1);
 
 
 
        if((~*(_DWORD *)(a1 + 4) & 0x80000) !=0)
        __android_log_print(6,"r0ysue","i find frida %x", (~*(_DWORD *)(a1 + 4) & 0x80000) );
    }
 
}
void __init(){
    jclass ss=env->FindClass("com/r0ysue/antifrida/MainActivity");
    jmethodID sss=env->GetMethodID(ss,"encr", "(I)I");
    pthread_create(&thread1, nullptr, reinterpret_cast<void *(*)(void *)>(anti2), sss);
 
 
}

以attach的方式附加检测效果如下

总结

我这里提供的frida检测方法还是比较简单的,当然可以加上各种混淆和自实现线程创建函数,又或者是fork子进程,今天的内容就到这里,感谢大家观看。


【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞1
打赏
分享
最新回复 (18)
雪    币: 8932
活跃值: 活跃值 (1524)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2021-10-19 17:06
2
0
可能你会误认为是找茬?但是我还是不得不说一下看法、
以偏概全?
frda使用了0xd61f020058000050跳转代码进行hook,但是不代表所有使用0xd61f020058000050的都是frida。
前面也有人发过用hook代码来检测frida,我同样表达了类似观点。
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-19 17:31
3
0
确实 所以说这里只是一个检测角度  并没有说通杀frida  
这种方式的绕过方法也很简单就是 更改 frida-gum/tests/core/arch-arm64/arm64relocator.c 中的汇编代码 将x16寄存器改为x17  
这两种方式是我在写完前两篇文章之后的 一个突发奇想的检测方法 攻防是不断进步的  我这里只是提出一种思路
雪    币: 216
活跃值: 活跃值 (917)
能力值: ( LV3,RANK:27 )
在线值:
发帖
回帖
粉丝
svengong 活跃值 2021-10-19 17:32
4
0
tDasm 可能你会误认为是找茬?但是我还是不得不说一下看法、 以偏概全? frda使用了0xd61f020058000050跳转代码进行hook,但是不代表所有使用0xd61f020058000050的都是 ...
上次你没赢,这次你赢了
雪    币: 179
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_hgrbqfun 活跃值 2021-10-20 01:01
5
0
r0ysue 确实 所以说这里只是一个检测角度 并没有说通杀frida 这种方式的绕过方法也很简单就是 更改 frida-gum/tests/core/arch-arm64/arm64relocator.c ...
我觉得这里主要还是还是那个地址的问题,
LDR X16,=loc_xxx  ,很明显,loc_xxx有可能是frida的函数,而且最主要的是,这个地址并不是写死的,因为so一旦加载顺序不一样或者其他原因,就会导致每个so的函数地址可能不一样。
所以这里hook后,汇编肯定是 LDR X12,=loc_xxx   br x16,但loc_xxx的值很可能不一样。

应该先判断汇编是不是  LDR 某寄存器,=loc_xxx   br 某寄存器,然后判断loc_xxx是不是frida的代码地址。

最后就能确定,感觉还是要针对性去弄,去看frida那个版本怎么hook的。
雪    币: 0
活跃值: 活跃值 (1764)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不吃早饭 活跃值 2021-10-20 04:10
6
0
这和frida有啥关系呢......这个叫检测aarch64下inlinehook还差不多......
雪    币: 8932
活跃值: 活跃值 (1524)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2021-10-20 08:12
7
0
r0ysue 确实 所以说这里只是一个检测角度 并没有说通杀frida 这种方式的绕过方法也很简单就是 更改 frida-gum/tests/core/arch-arm64/arm64relocator.c ...
本论坛早就发过这种思路。你不是第一个好吧,版主!
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 08:56
8
0

1

最后于 2021-10-20 08:56 被r0ysue编辑 ,原因:
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 08:57
9
0
mb_hgrbqfun 我觉得这里主要还是还是那个地址的问题, LDR X16,=loc_xxx ,很明显,loc_xxx有可能是frida的函数,而且最主要的是,这个地址并不是写死的,因为so一旦加载顺序不一样或者其他 ...
在frida-gum/tests/core/arch-arm64/arm64relocator.c
中有代码 因为64位操作不了pc  所以跳转代码用的就是 ldr x16,#8是固定的 
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 08:59
10
0
tDasm 本论坛早就发过这种思路。你不是第一个好吧,版主!
我也没说我是第一个发现的呀  自己实现过一个inlinehook就自然有通过inlinehook检测frida这个想法
再说你但凡 把我俩的文章一字一句的看完都知道我们的思路有区别
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 09:00
11
0
不吃早饭 这和frida有啥关系呢......这个叫检测aarch64下inlinehook还差不多......
因为frida 官方版本中用到了这种方法 所以才想到这么搞  
雪    币: 8932
活跃值: 活跃值 (1524)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2021-10-20 14:05
12
0
r0ysue 我也没说我是第一个发现的呀  自己实现过一个inlinehook就自然有通过inlinehook检测frida这个想法 再说你但凡 把我俩的文章一字一句的看完都知道我们的思路有区别

https://bbs.pediy.com/thread-268586.htm
你自己去看,有什么本质区别?区别就是别人只检测某函数入口字节,而你是检测多个函数。既然叫思路就应该与从不同,如果一样那就不是思路而是引申。

最后于 2021-10-20 14:06 被tDasm编辑 ,原因:
雪    币: 9393
活跃值: 活跃值 (5244)
能力值: ( LV13,RANK:375 )
在线值:
发帖
回帖
粉丝
TkBinary 活跃值 5 2021-10-20 14:15
13
0
谢谢分享. 思路很重要. 
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 15:09
14
0
tDasm r0ysue 我也没说我是第一个发现的呀 &nbsp;自己实现过一个inlinehook就自然有通过inlinehook检测frida这个 ...
咱俩出发点不同 我们的目的就是让每一个小白都能明白能手写这方面的内容能解决遇到的坑 
如果是知识相关的内容我们一定有问必答 像你上面的问题我回答了  
接口都是我自己封装的我也把源码贴出来了你说一样就一样吧 
如果你的出发点就是喷我的话 我也没啥好说的  
雪    币: 98
活跃值: 活跃值 (589)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
大魔头 活跃值 2021-10-20 15:15
15
0
目前我们用的检测也都基于这样的逻辑、但是一般不会去扫内存、而是针对性的扫某些特殊函数头、这就解决了上面这个疑问、特定的函数一般不会出现这样LDR BR模式的。但是事无绝对,万一厂商真的碎片化呢?所以就要分析得到BR的跳转内存地址去、去看内存地址的模块地址空间。
检测并不是100%的看一个点一棍子打死、而是结合更多的特征来综合判断。
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 15:37
16
0
大魔头 目前我们用的检测也都基于这样的逻辑、但是一般不会去扫内存、而是针对性的扫某些特殊函数头、这就解决了上面这个疑问、特定的函数一般不会出现这样LDR BR模式的。但是事无绝对,万一厂商真的碎片化呢?所以就 ...
感谢大佬补充
雪    币: 961
活跃值: 活跃值 (3613)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
Ssssone 活跃值 2 2021-10-20 16:18
17
0
记笔记记笔记
雪    币: 1125
活跃值: 活跃值 (3225)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
r0ysue 活跃值 2021-10-20 17:30
18
0
Ssssone 记笔记记笔记
大佬 反正你的文章我看完都记笔记上了 
雪    币: 2
活跃值: 活跃值 (326)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
晨曦遇晓 活跃值 2021-10-22 14:36
19
0
大佬牛逼
游客
登录 | 注册 方可回帖
返回