首页
社区
课程
招聘
[原创]记录一次BlackObfuscator去混淆流程
发表于: 2024-10-2 11:15 5843

[原创]记录一次BlackObfuscator去混淆流程

2024-10-2 11:15
5843

在一些CTF以及某些检测环境的APP中,在Java层会出现一种很奇怪的混淆,经过熊仔哥(看雪ID:白熊)的指点,这种混淆是一种开源的混淆方案,最早我是使用trace smali的方式解决掉的,不过最近有一场ctf比赛,这个混淆又出现了,并且有大佬给出了新的思路,以此契机我开始学习了Java层混淆对抗的路子,并开发出了几个脚本。感谢与我一起写反混淆脚本的实习生,我们一起完善解决了这个方案与脚本。

Jadx-GUi

CleanShot 2024-10-02 at 09.31.17.png

CleanShot 2024-10-02 at 09.31.34.png

GDA

CleanShot 2024-10-02 at 09.32.26.png

JEB

CleanShot 2024-10-02 at 09.33.58.png

由于每种反编译器的反编译效果不同,表现出的伪代码形式也为不同。

我们选择JEB来进行主力分析工具,因为JEB自带有一些神奇魔法,能帮我们做很多的事情。

https://bbs.kanxue.com/thread-278648.htm

在oacia大佬的帖子里,jeb直接去除掉了控制流平坦化

CleanShot 2024-10-02 at 09.36.12.png

但是又是什么神奇魔法让jeb的这种自动反混淆的能力失效了呢,让我来带你详细分析。

jeb官方支持反控制流平坦化的文档

https://www.pnfsoftware.com/blog/control-flow-unflattening-in-the-wild/

如果jeb帮我们完成了处理,那么在函数的头部会留下一些注释。

image.png

首先,打开自动重命名工具,将奇怪的字符串重新命名,增强阅读体验

CleanShot 2024-10-02 at 09.40.50.png

我认为百分之20即可完美去除垃圾字符串(正因为不好看,所以好识别)

CleanShot 2024-10-02 at 09.41.53.png

点击确定即可重新命名,获得一个比较好的阅读体验。

首先我们先摘出来一段控制流平坦化,分析其为什么不能自动反控制流平坦化

在switch(v1) v1如果全部都是已知数值的情况下,jeb可以直接反控制流平坦化

让我们看看是什么影响了v1的值

CleanShot 2024-10-02 at 09.46.37.png

通过查看发现,有两种方式影响了v1的赋值,让我们点进去看一下

第一种形式:

CleanShot 2024-10-02 at 09.47.41.png

第二种形式:

CleanShot 2024-10-02 at 09.48.13.png

当我们解决这两种混淆方式以后,jeb大哥会直接反混淆成功。

由于静态变量不是定植,在其他的地方可能被引用并修改,jeb理解这个值可能发生变化,所以不进行优化。

熟悉混淆的小伙伴可能脑子里立马迸发出一个想法:

这不就是bcf吗? 通过全局变量的不透明谓词来干扰执行流程

让我们分析这个变量在后续有没有修改?

CleanShot 2024-10-02 at 09.50.58.png

发现只有获取,并没有修改的形式。

那么我们该如何解决呢?其实非常简单。

https://bbs.kanxue.com/thread-257213.htm

参考葫芦娃大佬的帖子,我们可以改个标题了

JEB: 十步杀一人,两步秒BlackObfuscator

参考大佬的思路,我们可以把这个字段的权限从读写,改为只读

我们如何将这个字段变为只读字段呢?

熟悉java的朋友应该知道,final加入后只能读就不能写了

我们来观察一下这个字段获取的语句:

那么我们就可以引出第二种修改方式,将sget语句patch为const语句,也可以让我们的jeb直接意识到,这个是不可变的。

CleanShot 2024-10-02 at 10.03.27.png

关键的语句:

const-string v0, "\u06E7\u06E6\u06E0" 定义了一个固定的字符串 赋值给v0寄存器

invoke-static CLS5547->MTH27577(Object)I, v0 调用静态方法,v0参数传入这个函数

000003F8 move-result v0 将结果放回v0寄存器(这里和上面两句使用的寄存器的一般一致,但是我在后面还是处理了)

调用的函数非常简单,就是取一个字符串的hashcode

众所周知,在字符串不变的情况下,hashcode也是不会变的,下面给出算法

CleanShot 2024-10-02 at 10.07.19.png

第二种方式又是用一种巧妙的方法骗过了我们的jeb老大哥

如何解决?

查找所有静态调用的方法,查看方法体是否调用了hashcode(如果更快我觉得可以收集opcode特征打一个md5,但是没必要),如果是,手动计算hashcode,并patch回原来的调用处。

为了给原来的smali方法体擦干净屁股,我们需要将三条smali指令替换为一条。

也就是把

替换为const v0,计算后的hashcode

这个部分更多的是引出两种去混淆方案的形式,为第二篇AST解混淆做出预告

最早我使用的方案是使用jeb脚本的方式来修改,当然幻想是美好的,实际却是残酷的。

在这里我想引入两种概念,借用看雪另外一位大佬(https://bbs.kanxue.com/homepage-760871.htm)的两个帖子来带大家领略这两个反混淆的概念。

https://bbs.kanxue.com/thread-263011.htm

https://bbs.kanxue.com/thread-263012.htm(选看,本帖没用上)

下面我们开始讲如何用jeb。实现第一种形式的patch**(没有成功实现,大佬选看,只是记录踩坑过程)**

第一种想法,找到sget指令,获取到sget操作的字段,patch回去

尴尬的来了,找了半天jeb文档,没有相关写入方法。(有大佬有写入方法的话带带,我能写出jeb脚本)

insn是属于IDalvikInstruction类下面的

我们打开jeb文档

https://www.pnfsoftware.com/jeb/apidoc/reference/com/pnfsoftware/jeb/core/units/code/android/dex/IDalvikInstruction.html

我们可以看到各种get方法,

CleanShot 2024-10-02 at 10.36.46.png

也许可以拿到offset和offsetend 在针对性patch,但是我没有考虑这种方式。

总的来说,通过DEX字节码层面可以拿到所有我能拿到的信息,但是并没有一个方法来设置。

大体思路就是先拿到DEX模块,进一步拿到所有method,在拿到所有基本块,遍历所有基本块里的指令,如果遇到sget,则找到sget获取的Filed,然后修改sget这个指令。

也可以另外一种思路,收集所有静态字段,然后查找引用的地方,再去以字节码层面patch

同时也可以收集所有静态字段,拿到静态字段的权限,进行过滤,然后修改为只读(JEB可以拿到确切的权限,但是没法修改) 拿到权限那段代码我丢了,我后续会补上

既然jeb不能满足我们的需求,我们可以采用dex2lib这个库来进行反混淆操作。

首先先实现方案1,将所有静态字段增加final属性

show me the code!

CleanShot 2024-10-02 at 10.51.31.png

CleanShot 2024-10-02 at 10.54.08.png

很多读者有疑问,为什么hashcode没有解析也可以识别了,我的答案是jeb牛逼。

当然我也针对了hashcode写了对抗的脚本,大家可以当demo参考。

运行结果:

CleanShot 2024-10-02 at 10.58.32.png

CleanShot 2024-10-02 at 10.58.48.png

第三个思路:

收集所有sget指令,替换为const,这里涉及到一个搜索问题

第一个版本我是先拿到Feild,然后遍历所有类,找到相同的,导致速度很慢

第二个版本我先提前收集好所有Feild,然后建立一个hashmap做匹配,速度提升了不少

第一个版本:

第一个版本的搜索算法(别喷)

第二个版本:

提前收集字段

最后还原后,还有一些函数在外面调用,我准备下一篇文章讲一下具体api,然后手把手带着做一下

CleanShot 2024-10-02 at 11.05.46.png

CleanShot 2024-10-02 at 11.06.12.png

这个留给下一篇文章进行详细讲解然后解决,其实解决起来也非常的容易,大家可以尝试一下

坑点:app是多个dex的,有两种解决方式

第一种合并dex(我采用的)

第二种收集多个dex的静态字段,建立maps映射(朋友实现的)

我现在给出合并dex的思路和脚本(大家需要自行下载一下jar包)

提前预告:JSAST的思路都可以引入进来,做自己的反混淆插件,比如常量折叠,反控制流平坦化。

这一篇章就是基于JEB提供的一系列AST接口来实现,而且我推断官方内置的就是使用这种方式来实现的,总的来看Java层面的混淆比较难做,碍于Java字节码的机制,对于调用树恢复的程度很高。

Java层面的反混淆的对抗成本和开发成本是对等的(混淆和去混淆都用的一个库,基于dex层面做)

脚本还有一些瑕疵需要完善,近期会上传。

000003EE  const-string        v0, "\u06E7\u06E6\u06E0"
000003F2  invoke-static       CLS5547->MTH27577(Object)I, v0
000003F8  move-result         v0
000003EE  const-string        v0, "\u06E7\u06E6\u06E0"
000003F2  invoke-static       CLS5547->MTH27577(Object)I, v0
000003F8  move-result         v0
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
 
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
 
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
000003EE  const-string        v0, "\u06E7\u06E6\u06E0"
000003F2  invoke-static       CLS5547->MTH27577(Object)I, v0
000003F8  move-result         v0
000003EE  const-string        v0, "\u06E7\u06E6\u06E0"
000003F2  invoke-static       CLS5547->MTH27577(Object)I, v0
000003F8  move-result         v0
DEX字节码层面(IDexUnit部件)
(1)访问DEX与Class
(2)遍历Field / Method
(3)访问某个Method
(4)访问指令
(5)访问基本块
(6)访问控制流数据流
DEX字节码层面(IDexUnit部件)
(1)访问DEX与Class
(2)遍历Field / Method
(3)访问某个Method
(4)访问指令
(5)访问基本块
(6)访问控制流数据流
# -*- coding: UTF-8 -*-
from com.pnfsoftware.jeb.client.api import IScript
from com.pnfsoftware.jeb.core.units import UnitUtil
from com.pnfsoftware.jeb.core.units.code.android import IDexUnit
from com.pnfsoftware.jeb.core.units.code.android.dex import IDexClass
from com.pnfsoftware.jeb.core.actions import ActionContext
from com.pnfsoftware.jeb.core.actions import Actions
from com.pnfsoftware.jeb.client.api import IClientContext
from com.pnfsoftware.jeb.core import IRuntimeProject
from com.pnfsoftware.jeb.core.units import IUnit
from com.pnfsoftware.jeb.core.units.code import IFlowInformation
from com.pnfsoftware.jeb.core.units.code.android import IDexUnit
 
class SGetRightOperandTree(IScript):
    def run(self, ctx):
        prj = ctx.getMainProject();
        dexUnit =prj.findUnit(IDexUnit);
 
        # Check if the unit is a DEX unit
        if not isinstance(dexUnit, IDexUnit):
            print('The script must be run on a DEX unit.')
            return
 
        # Specify the class name you're interested in
        method_sign = 'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;'
 
        method = dexUnit.getMethod(method_sign)
 
        dexMethodData = method.getData();
        dexCodeItem= dexMethodData.getCodeItem();
        for idx,insn in enumerate(dexCodeItem.getInstructions()):
            if str(insn)=="sget":
                print insn
                print idx,"(01) getCode                      >>> ",insn.getCode()               # 二进制
                print idx,"(02) getOpcode                    >>> ",insn.getOpcode()             # 操作码
                print idx,"(03) getParameters:"                                                 # 指令操作数
                for a,b in enumerate(insn.getParameters()):
                    print "<",a,">",b.getType(),b.getValue()
                if len(insn.getParameters()) > 1:
                    fieldRef = insn.getParameters()[1]
                print("Field Reference: ", fieldRef)
                if len(insn.getParameters()) > 1:
                    fieldRef = insn.getParameters()[1].getValue()
                    field = dexUnit.getField(fieldRef)
                    print(field.getName())
                    # currentFlags = field.getAddress()
                    # print("currentFlags",currentFlags)
                    fieldData = field.getData()
                    print(field.getStaticInitializer())
                    fieldValue=field.getStaticInitializer()
                    # if field:
                    # 那么就patch回去
 
    
# -*- coding: UTF-8 -*-
from com.pnfsoftware.jeb.client.api import IScript
from com.pnfsoftware.jeb.core.units import UnitUtil
from com.pnfsoftware.jeb.core.units.code.android import IDexUnit
from com.pnfsoftware.jeb.core.units.code.android.dex import IDexClass
from com.pnfsoftware.jeb.core.actions import ActionContext
from com.pnfsoftware.jeb.core.actions import Actions
from com.pnfsoftware.jeb.client.api import IClientContext
from com.pnfsoftware.jeb.core import IRuntimeProject
from com.pnfsoftware.jeb.core.units import IUnit
from com.pnfsoftware.jeb.core.units.code import IFlowInformation
from com.pnfsoftware.jeb.core.units.code.android import IDexUnit
 
class SGetRightOperandTree(IScript):
    def run(self, ctx):
        prj = ctx.getMainProject();
        dexUnit =prj.findUnit(IDexUnit);
 
        # Check if the unit is a DEX unit
        if not isinstance(dexUnit, IDexUnit):
            print('The script must be run on a DEX unit.')
            return
 
        # Specify the class name you're interested in
        method_sign = 'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;'
 
        method = dexUnit.getMethod(method_sign)
 
        dexMethodData = method.getData();
        dexCodeItem= dexMethodData.getCodeItem();
        for idx,insn in enumerate(dexCodeItem.getInstructions()):
            if str(insn)=="sget":
                print insn
                print idx,"(01) getCode                      >>> ",insn.getCode()               # 二进制
                print idx,"(02) getOpcode                    >>> ",insn.getOpcode()             # 操作码
                print idx,"(03) getParameters:"                                                 # 指令操作数
                for a,b in enumerate(insn.getParameters()):
                    print "<",a,">",b.getType(),b.getValue()
                if len(insn.getParameters()) > 1:
                    fieldRef = insn.getParameters()[1]
                print("Field Reference: ", fieldRef)
                if len(insn.getParameters()) > 1:
                    fieldRef = insn.getParameters()[1].getValue()
                    field = dexUnit.getField(fieldRef)
                    print(field.getName())
                    # currentFlags = field.getAddress()
                    # print("currentFlags",currentFlags)
                    fieldData = field.getData()
                    print(field.getStaticInitializer())
                    fieldValue=field.getStaticInitializer()
                    # if field:
                    # 那么就patch回去
 
    
@Override
   public Rewriter<Field> getFieldRewriter(Rewriters rewriters) {
       return new FieldRewriter(rewriters) {
           @Override
           public Field rewrite(Field field) {
               int accessFlags = field.getAccessFlags();
               // 检查是否为 public static
               if ((accessFlags & AccessFlags.PUBLIC.getValue()) != 0 &&
                       (accessFlags & AccessFlags.STATIC.getValue()) != 0 &&
                       (accessFlags & AccessFlags.FINAL.getValue()) == 0) {
                   // 添加 FINAL 修饰符
                   accessFlags |= AccessFlags.FINAL.getValue();
                   System.out.println("Modified field " + field.getName() + " to public static final");
                   // 返回修改后的字段
                   return new ImmutableField(
                           field.getDefiningClass(),
                           field.getName(),
                           field.getType(),
                           accessFlags,
                           field.getInitialValue(),         // initialValue
                           field.getAnnotations(),          // annotations
                           field.getHiddenApiRestrictions() // hiddenApiRestrictions
                   );
               }
               return super.rewrite(field);
           }
       };
   }
@Override
   public Rewriter<Field> getFieldRewriter(Rewriters rewriters) {
       return new FieldRewriter(rewriters) {
           @Override
           public Field rewrite(Field field) {
               int accessFlags = field.getAccessFlags();
               // 检查是否为 public static
               if ((accessFlags & AccessFlags.PUBLIC.getValue()) != 0 &&
                       (accessFlags & AccessFlags.STATIC.getValue()) != 0 &&
                       (accessFlags & AccessFlags.FINAL.getValue()) == 0) {
                   // 添加 FINAL 修饰符
                   accessFlags |= AccessFlags.FINAL.getValue();
                   System.out.println("Modified field " + field.getName() + " to public static final");
                   // 返回修改后的字段
                   return new ImmutableField(
                           field.getDefiningClass(),
                           field.getName(),
                           field.getType(),
                           accessFlags,
                           field.getInitialValue(),         // initialValue
                           field.getAnnotations(),          // annotations
                           field.getHiddenApiRestrictions() // hiddenApiRestrictions
                   );
               }
               return super.rewrite(field);
           }
       };
   }
@Override
  public Rewriter<MethodImplementation> getMethodImplementationRewriter(Rewriters rewriters) {
      return new MethodImplementationRewriter(rewriters) {
          @Override
          public MethodImplementation rewrite(MethodImplementation methodImpl) {
              if (methodImpl == null) {
                  return null;
              }
              List<Instruction> originalInstructions = new ArrayList<>();
              for (Instruction instruction : methodImpl.getInstructions()) {
                  originalInstructions.add(instruction);
              }
              List<Instruction> newInstructions = new ArrayList<>();
 
              for (int i = 0; i < originalInstructions.size(); i++) {
                  Instruction instruction = originalInstructions.get(i);
 
                  if (instruction.getOpcode() == Opcode.INVOKE_STATIC) {
                      if (instruction instanceof ReferenceInstruction) {
                          ReferenceInstruction refInstr = (ReferenceInstruction) instruction;
                          Reference reference = refInstr.getReference();
                          if (reference instanceof MethodReference) {
                              MethodReference methodRef = (MethodReference) reference;
                              //拿到方法签名
                              String methodKey = getMethodKey(methodRef);
                              //拿到要调用的方法的签名
                              Method calledMethod = methodMap.get(methodKey);
                              if (calledMethod != null && calledMethod.getImplementation() != null) {
                                  //这里就是判断是否调用了 hashCode 方法
                                  if (methodContainsHashCodeInvocation(calledMethod)) {
                                      // 获取参数寄存器
                                      List<Integer> parameterRegisters = getInvokeInstructionParameterRegisters(instruction);
                                      if (parameterRegisters.size() > 0) {
                                          int paramRegister = parameterRegisters.get(0);
                                          // 向上寻找赋值给 paramRegister 的指令,追踪源寄存器,直到找到 const-string
                                          String stringValue = findStringAssignedToRegister(paramRegister, originalInstructions, i);
                                          if (stringValue != null) {
                                              // 计算哈希码
                                              int hashCode = stringValue.hashCode();
                                              // 获取结果寄存器
                                              int resultRegister = getResultRegister(originalInstructions, i);
                                              if (resultRegister != -1) {
                                                  // 创建 const 指令替换 invoke-static 和 move-result
                                                  Instruction constInstr = createConstInstruction(resultRegister, hashCode);
                                                  if (constInstr != null) {
                                                      newInstructions.add(constInstr);
                                                      System.out.println("Replaced invoke-static with const for method " + methodRef.getName() + ", hashCode: " + hashCode);
                                                      // 跳过 invoke-static 和后续的 move-result 指令
                                                      if (i + 1 < originalInstructions.size()) {
                                                          Instruction nextInstr = originalInstructions.get(i + 1);
                                                          if (nextInstr.getOpcode() == Opcode.MOVE_RESULT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_OBJECT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_WIDE) {
                                                              i++; // 跳过 move-result 指令
                                                          }
                                                      }
                                                      continue;
                                                  }
                                              }
                                          }
                                      }
                                  }
                              }
                          }
                      }
                  }
                  // 复制原始指令
                  newInstructions.add(instruction);
              }
 
              return new ImmutableMethodImplementation(
                      methodImpl.getRegisterCount(),
                      newInstructions,
                      methodImpl.getTryBlocks(),
                      methodImpl.getDebugItems()
              );
          }
      };
  }
@Override
  public Rewriter<MethodImplementation> getMethodImplementationRewriter(Rewriters rewriters) {
      return new MethodImplementationRewriter(rewriters) {
          @Override
          public MethodImplementation rewrite(MethodImplementation methodImpl) {
              if (methodImpl == null) {
                  return null;
              }
              List<Instruction> originalInstructions = new ArrayList<>();
              for (Instruction instruction : methodImpl.getInstructions()) {
                  originalInstructions.add(instruction);
              }
              List<Instruction> newInstructions = new ArrayList<>();
 
              for (int i = 0; i < originalInstructions.size(); i++) {
                  Instruction instruction = originalInstructions.get(i);
 
                  if (instruction.getOpcode() == Opcode.INVOKE_STATIC) {
                      if (instruction instanceof ReferenceInstruction) {
                          ReferenceInstruction refInstr = (ReferenceInstruction) instruction;
                          Reference reference = refInstr.getReference();
                          if (reference instanceof MethodReference) {
                              MethodReference methodRef = (MethodReference) reference;
                              //拿到方法签名
                              String methodKey = getMethodKey(methodRef);
                              //拿到要调用的方法的签名
                              Method calledMethod = methodMap.get(methodKey);
                              if (calledMethod != null && calledMethod.getImplementation() != null) {
                                  //这里就是判断是否调用了 hashCode 方法
                                  if (methodContainsHashCodeInvocation(calledMethod)) {
                                      // 获取参数寄存器
                                      List<Integer> parameterRegisters = getInvokeInstructionParameterRegisters(instruction);
                                      if (parameterRegisters.size() > 0) {
                                          int paramRegister = parameterRegisters.get(0);
                                          // 向上寻找赋值给 paramRegister 的指令,追踪源寄存器,直到找到 const-string
                                          String stringValue = findStringAssignedToRegister(paramRegister, originalInstructions, i);
                                          if (stringValue != null) {
                                              // 计算哈希码
                                              int hashCode = stringValue.hashCode();
                                              // 获取结果寄存器
                                              int resultRegister = getResultRegister(originalInstructions, i);
                                              if (resultRegister != -1) {
                                                  // 创建 const 指令替换 invoke-static 和 move-result
                                                  Instruction constInstr = createConstInstruction(resultRegister, hashCode);
                                                  if (constInstr != null) {
                                                      newInstructions.add(constInstr);
                                                      System.out.println("Replaced invoke-static with const for method " + methodRef.getName() + ", hashCode: " + hashCode);
                                                      // 跳过 invoke-static 和后续的 move-result 指令
                                                      if (i + 1 < originalInstructions.size()) {
                                                          Instruction nextInstr = originalInstructions.get(i + 1);
                                                          if (nextInstr.getOpcode() == Opcode.MOVE_RESULT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_OBJECT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_WIDE) {
                                                              i++; // 跳过 move-result 指令
                                                          }

[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!

收藏
免费 18
支持
分享
最新回复 (8)
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-10-2 11:18
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2024-10-2 11:46
0
雪    币: 10
活跃值: (86)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
2024-10-2 22:03
0
雪    币: 1925
活跃值: (2322)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2024-10-2 22:20
0
雪    币: 2290
活跃值: (1698)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6
师傅牛逼!
2024-10-3 11:13
0
雪    币: 102
活跃值: (1920)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2024-10-8 11:41
0
雪    币: 1357
活跃值: (2420)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2024-10-9 10:58
0
雪    币: 1720
活跃值: (1190)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
期待下一篇
3天前
0
游客
登录 | 注册 方可回帖
返回
//