首页
社区
课程
招聘
[原创]OLLVM虚假控制流源码分析
发表于: 2023-8-1 00:03 5749

[原创]OLLVM虚假控制流源码分析

2023-8-1 00:03
5749

这里的ObfTimes对应过来的默认值就是1,对函数进行混淆的次数,ObfProbRate就是30,对基本块进行混淆的概率,分别是opt时传入的参数。

枚举这个函数的所有基本块,如果这个函数后面基本块中含有invoke(调用了某个函数),它就不执行了,就退出这个基本块。

判断有没有bcf也就是虚假控制流,有的话就进入。

首先进行判断这个次数和概率,是否符合条件,不符合的话会进行设置默认值。
紧接着就是一个大型的do while循环里面包含着的代码:

把所有的基本块放在basicblock list里面。
获取一个随机值,如果符合的话就进入:

每个基本块都有ObfProbRate的概率被混淆,即基本块调用了addBogusFlow函数。
这个函数的作用就是对指定函数的每个基本块以ObfProbRate的概率去进行调用函数混淆。

执行完后这里的basicBlock指令是br label %originalBB,而originalBB目前代码块如下:

而之前的entry就是目前basicblock:

紧接着:

createAlteredBasicBlock会把这个originalBB进行克隆

这里的代码的话主要解决两个问题,就cloneBasicBlock函数进行的克隆并不是完全的克隆,第一它不会对操作数进行替换,比如:

在clone出来的基本块中,fadd指令的操作数不是%a.clone,而是a%,所以之后要通过VMap对所有操作数进行映射,使其恢复正常:

第二,它不会对PHI Node进行任何处理,PHI Node的前驱块仍是原始基本块的前驱块,但是新克隆出来的基本块没有任何前驱块,所以要对PHI Node的前驱块进行remap:

往基本块里面添加一些没用的赋值指令,或者修改cmp的条件,binaryop大概指的是add,mul,cmp这类运算指令

copy后变量名字进行改动

这里的话是指把entry里面的跳转和拷贝出来的block块最后一个跳转也删掉:

在entry里面生成两个浮点数,并进行两个浮点数的比较跳转指令:

fcmp后面条件为true,它只会一直跳转为前者originalBB。

在originalBBalteredBB生成一个跳转指令,跳转到originalBB:

查找到originBB最后一条指令进行split,然后创建一个originalBBpart2基本块

切割后originBB最后就变成了无条件的跳转:

紧接着把originBB最后一行给删掉,创建一个fcmp的条件跳转:

如下:

这个函数主要就是创建了entry和originalBB代码块的最后两行浮点数比较的跳转

网上很多很多现成资料,我这个也是自己看了再总结的,如果哪里不对的话,还请多多指教。

if (ObfTimes <= 0) {
        errs()<<"BogusControlFlow application number -bcf_loop=x must be x > 0";
        return false;
      }
if ( !((ObfProbRate > 0) && (ObfProbRate <= 100)) ) {
        errs()<<"BogusControlFlow application basic blocks percentage -bcf_prob=x must be 0 < x <= 100";
        return false;
      }
if (ObfTimes <= 0) {
        errs()<<"BogusControlFlow application number -bcf_loop=x must be x > 0";
        return false;
      }
if ( !((ObfProbRate > 0) && (ObfProbRate <= 100)) ) {
        errs()<<"BogusControlFlow application basic blocks percentage -bcf_prob=x must be 0 < x <= 100";
        return false;
      }
const int defaultObfRate = 30, defaultObfTime = 1;
 
static cl::opt<int>
ObfProbRate("bcf_prob", cl::desc("Choose the probability [%] each basic blocks will be obfuscated by the -bcf pass"), cl::value_desc("probability rate"), cl::init(defaultObfRate), cl::Optional);
 
static cl::opt<int>
ObfTimes("bcf_loop", cl::desc("Choose how many time the -bcf pass loop on a function"), cl::value_desc("number of times"), cl::init(defaultObfTime), cl::Optional);
const int defaultObfRate = 30, defaultObfTime = 1;
 
static cl::opt<int>
ObfProbRate("bcf_prob", cl::desc("Choose the probability [%] each basic blocks will be obfuscated by the -bcf pass"), cl::value_desc("probability rate"), cl::init(defaultObfRate), cl::Optional);
 
static cl::opt<int>
ObfTimes("bcf_loop", cl::desc("Choose how many time the -bcf pass loop on a function"), cl::value_desc("number of times"), cl::init(defaultObfTime), cl::Optional);
// check for compatible
  for (BasicBlock &bb : F.getBasicBlockList()) {
    if (isa<InvokeInst>(bb.getTerminator())) {
      return false;
    }
  }
// check for compatible
  for (BasicBlock &bb : F.getBasicBlockList()) {
    if (isa<InvokeInst>(bb.getTerminator())) {
      return false;
    }
  }
if(toObfuscate(flag,&F,"bcf")) {
        bogus(F);
        doF(*F.getParent());
        return true;
      }
if(toObfuscate(flag,&F,"bcf")) {
        bogus(F);
        doF(*F.getParent());
        return true;
      }
if(ObfProbRate < 0 || ObfProbRate > 100){
       DEBUG_WITH_TYPE("opt", errs() << "bcf: Incorrect value,"
           << " probability rate set to default value: "
           << defaultObfRate <<" \n");
       ObfProbRate = defaultObfRate;
     }
if(ObfTimes <= 0){
       DEBUG_WITH_TYPE("opt", errs() << "bcf: Incorrect value,"
           << " must be greater than 1. Set to default: "
           << defaultObfTime <<" \n");
       ObfTimes = defaultObfTime;
     }
if(ObfProbRate < 0 || ObfProbRate > 100){
       DEBUG_WITH_TYPE("opt", errs() << "bcf: Incorrect value,"
           << " probability rate set to default value: "
           << defaultObfRate <<" \n");
       ObfProbRate = defaultObfRate;
     }
if(ObfTimes <= 0){
       DEBUG_WITH_TYPE("opt", errs() << "bcf: Incorrect value,"
           << " must be greater than 1. Set to default: "
           << defaultObfTime <<" \n");
       ObfTimes = defaultObfTime;
     }
std::list<BasicBlock *> basicBlocks;
 for (Function::iterator i=F.begin();i!=F.end();++i) {
   basicBlocks.push_back(&*i);
 }
std::list<BasicBlock *> basicBlocks;
 for (Function::iterator i=F.begin();i!=F.end();++i) {
   basicBlocks.push_back(&*i);
 }
if((int)llvm::cryptoutils->get_range(100) <= ObfProbRate){
             DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
                 << NumBasicBlocks <<" selected. \n");
             hasBeenModified = true;
             ++NumModifiedBasicBlocks;
             NumAddedBasicBlocks += 3;
             FinalNumBasicBlocks += 3;
             // Add bogus flow to the given Basic Block (see description)
             BasicBlock *basicBlock = basicBlocks.front();
             addBogusFlow(basicBlock, F);
           }else{
             DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
                 << NumBasicBlocks <<" not selected.\n");
           }
if((int)llvm::cryptoutils->get_range(100) <= ObfProbRate){
             DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
                 << NumBasicBlocks <<" selected. \n");
             hasBeenModified = true;
             ++NumModifiedBasicBlocks;
             NumAddedBasicBlocks += 3;
             FinalNumBasicBlocks += 3;
             // Add bogus flow to the given Basic Block (see description)
             BasicBlock *basicBlock = basicBlocks.front();
             addBogusFlow(basicBlock, F);
           }else{
             DEBUG_WITH_TYPE("opt", errs() << "bcf: Block "
                 << NumBasicBlocks <<" not selected.\n");
           }
define dso_local i32 @main(i32 %argc, i8** %argv) #0 {
entry:
  %retval = alloca i32, align 4
  %argc.addr = alloca i32, align 4
  %argv.addr = alloca i8**, align 8
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 %argc, i32* %argc.addr, align 4
  store i8** %argv, i8*** %argv.addr, align 8
  %0 = load i8**, i8*** %argv.addr, align 8
  %arrayidx = getelementptr inbounds i8*, i8** %0, i64 1
  %1 = load i8*, i8** %arrayidx, align 8
  %call = call i32 @atoi(i8* %1) #2
  store i32 %call, i32* %a, align 4
  %2 = load i32, i32* %a, align 4
  %cmp = icmp eq i32 %2, 0
  br i1 %cmp, label %if.then, label %if.else
 
if.then:                                          ; preds = %entry
  store i32 1, i32* %retval, align 4
  br label %return
 
if.else:                                          ; preds = %entry
  store i32 10, i32* %retval, align 4
  br label %return
 
return:                                           ; preds = %if.else, %if.then
  %3 = load i32, i32* %retval, align 4
  ret i32 %3
}
define dso_local i32 @main(i32 %argc, i8** %argv) #0 {
entry:
  %retval = alloca i32, align 4
  %argc.addr = alloca i32, align 4
  %argv.addr = alloca i8**, align 8
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 %argc, i32* %argc.addr, align 4
  store i8** %argv, i8*** %argv.addr, align 8
  %0 = load i8**, i8*** %argv.addr, align 8
  %arrayidx = getelementptr inbounds i8*, i8** %0, i64 1
  %1 = load i8*, i8** %arrayidx, align 8
  %call = call i32 @atoi(i8* %1) #2
  store i32 %call, i32* %a, align 4
  %2 = load i32, i32* %a, align 4
  %cmp = icmp eq i32 %2, 0
  br i1 %cmp, label %if.then, label %if.else
 
if.then:                                          ; preds = %entry
  store i32 1, i32* %retval, align 4
  br label %return
 
if.else:                                          ; preds = %entry
  store i32 10, i32* %retval, align 4
  br label %return
 
return:                                           ; preds = %if.else, %if.then
  %3 = load i32, i32* %retval, align 4
  ret i32 %3
}
Instruction *i1 = &*basicBlock->begin();
     if(basicBlock->getFirstNonPHIOrDbgOrLifetime())
       i1 = basicBlock->getFirstNonPHIOrDbgOrLifetime();
     Twine *var;
     var = new Twine("originalBB");
     BasicBlock *originalBB = basicBlock->splitBasicBlock(i1, *var);
Instruction *i1 = &*basicBlock->begin();
     if(basicBlock->getFirstNonPHIOrDbgOrLifetime())
       i1 = basicBlock->getFirstNonPHIOrDbgOrLifetime();
     Twine *var;
     var = new Twine("originalBB");
     BasicBlock *originalBB = basicBlock->splitBasicBlock(i1, *var);
originalBB:
 %retval = alloca i32, align 4
  %argc.addr = alloca i32, align 4
  %argv.addr = alloca i8**, align 8
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 %argc, i32* %argc.addr, align 4
  store i8** %argv, i8*** %argv.addr, align 8
  %0 = load i8**, i8*** %argv.addr, align 8
  %arrayidx = getelementptr inbounds i8*, i8** %0, i64 1
  %1 = load i8*, i8** %arrayidx, align 8
  %call = call i32 @atoi(i8* %1) #2
  store i32 %call, i32* %a, align 4
  %2 = load i32, i32* %a, align 4
  %cmp = icmp eq i32 %2, 0
  br i1 %cmp, label %if.then, label %if.else
originalBB:
 %retval = alloca i32, align 4
  %argc.addr = alloca i32, align 4
  %argv.addr = alloca i8**, align 8
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 %argc, i32* %argc.addr, align 4
  store i8** %argv, i8*** %argv.addr, align 8
  %0 = load i8**, i8*** %argv.addr, align 8
  %arrayidx = getelementptr inbounds i8*, i8** %0, i64 1
  %1 = load i8*, i8** %arrayidx, align 8
  %call = call i32 @atoi(i8* %1) #2
  store i32 %call, i32* %a, align 4
  %2 = load i32, i32* %a, align 4
  %cmp = icmp eq i32 %2, 0
  br i1 %cmp, label %if.then, label %if.else
entry:
br label %originalBB
entry:
br label %originalBB
Twine * var3 = new Twine("alteredBB");
     BasicBlock *alteredBB = createAlteredBasicBlock(originalBB, *var3, &F);
Twine * var3 = new Twine("alteredBB");
     BasicBlock *alteredBB = createAlteredBasicBlock(originalBB, *var3, &F);
virtual BasicBlock* createAlteredBasicBlock(BasicBlock * basicBlock,
    const Twine &  Name = "gen", Function * F = 0){
  // Useful to remap the informations concerning instructions.
  ValueToValueMapTy VMap;
  BasicBlock * alteredBB = llvm::CloneBasicBlock (basicBlock, VMap, Name, F);
 
 
    // Remap attached metadata.
    SmallVector<std::pair<unsigned, MDNode *>, 4> MDs;
    i->getAllMetadata(MDs);
    // important for compiling with DWARF, using option -g.
    i->setDebugLoc(ji->getDebugLoc());
    ji++;
  } // The instructions' informations are now all correct
virtual BasicBlock* createAlteredBasicBlock(BasicBlock * basicBlock,
    const Twine &  Name = "gen", Function * F = 0){
  // Useful to remap the informations concerning instructions.
  ValueToValueMapTy VMap;
  BasicBlock * alteredBB = llvm::CloneBasicBlock (basicBlock, VMap, Name, F);
 
 
    // Remap attached metadata.
    SmallVector<std::pair<unsigned, MDNode *>, 4> MDs;
    i->getAllMetadata(MDs);
    // important for compiling with DWARF, using option -g.
    i->setDebugLoc(ji->getDebugLoc());
    ji++;
  } // The instructions' informations are now all correct
orig:
  %a = ...
  %b = fadd %a, ...
  
clone:
  %a.clone = ...
  %b.clone = fadd %a, ... ; Note that this references the old %a and
not %a.clone!
orig:
  %a = ...
  %b = fadd %a, ...
  
clone:
  %a.clone = ...
  %b.clone = fadd %a, ... ; Note that this references the old %a and
not %a.clone!
BasicBlock::iterator ji = basicBlock->begin();
for (BasicBlock::iterator i = alteredBB->begin(), e = alteredBB->end() ; i != e; ++i){
  // Loop over the operands of the instruction
  for(User::op_iterator opi = i->op_begin (), ope = i->op_end(); opi != ope; ++opi){
    // get the value for the operand
    Value *v = MapValue(*opi, VMap,  RF_NoModuleLevelChanges, 0);
    if (v != 0){
      *opi = v;
    }
  }
BasicBlock::iterator ji = basicBlock->begin();
for (BasicBlock::iterator i = alteredBB->begin(), e = alteredBB->end() ; i != e; ++i){
  // Loop over the operands of the instruction
  for(User::op_iterator opi = i->op_begin (), ope = i->op_end(); opi != ope; ++opi){
    // get the value for the operand
    Value *v = MapValue(*opi, VMap,  RF_NoModuleLevelChanges, 0);
    if (v != 0){
      *opi = v;
    }
  }
// Remap phi nodes' incoming blocks.
        if (PHINode *pn = dyn_cast<PHINode>(i)) {
          for (unsigned j = 0, e = pn->getNumIncomingValues(); j != e; ++j) {
            Value *v = MapValue(pn->getIncomingBlock(j), VMap, RF_None, 0);
            if (v != 0){
              pn->setIncomingBlock(j, cast<BasicBlock>(v));
            }
          }
        }
// Remap phi nodes' incoming blocks.
        if (PHINode *pn = dyn_cast<PHINode>(i)) {
          for (unsigned j = 0, e = pn->getNumIncomingValues(); j != e; ++j) {
            Value *v = MapValue(pn->getIncomingBlock(j), VMap, RF_None, 0);
            if (v != 0){
              pn->setIncomingBlock(j, cast<BasicBlock>(v));
            }
          }
        }
for (BasicBlock::iterator i = alteredBB->begin(), e = alteredBB->end() ; i != e; ++i){
       // in the case we find binary operator, we modify slightly this part by randomly
       // insert some instructions
       if(i->isBinaryOp()){ // binary instructions
         unsigned opcode = i->getOpcode();
         BinaryOperator *op, *op1 = NULL;
         Twine *var = new Twine("_");
         // treat differently float or int
         // Binary int
         if(opcode == Instruction::Add || opcode == Instruction::Sub ||
             opcode == Instruction::Mul || opcode == Instruction::UDiv ||
             opcode == Instruction::SDiv || opcode == Instruction::URem ||
             opcode == Instruction::SRem || opcode == Instruction::Shl ||
             opcode == Instruction::LShr || opcode == Instruction::AShr ||
             opcode == Instruction::And || opcode == Instruction::Or ||
             opcode == Instruction::Xor){
           for(int random = (int)llvm::cryptoutils->get_range(10); random < 10; ++random){
             switch(llvm::cryptoutils->get_range(4)){ // to improve
               case 0: //do nothing
                 break;
               case 1: op = BinaryOperator::CreateNeg(i->getOperand(0),*var,&*i);
                       op1 = BinaryOperator::Create(Instruction::Add,op,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 2: op1 = BinaryOperator::Create(Instruction::Sub,
                           i->getOperand(0),
                           i->getOperand(1),*var,&*i);
                       op = BinaryOperator::Create(Instruction::Mul,op1,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 3: op = BinaryOperator::Create(Instruction::Shl,
                           i->getOperand(0),
                           i->getOperand(1),*var,&*i);
                       break;
             }
           }
         }
         // Binary float
         if(opcode == Instruction::FAdd || opcode == Instruction::FSub ||
             opcode == Instruction::FMul || opcode == Instruction::FDiv ||
             opcode == Instruction::FRem){
           for(int random = (int)llvm::cryptoutils->get_range(10); random < 10; ++random){
             switch(llvm::cryptoutils->get_range(3)){ // can be improved
               case 0: //do nothing
                 break;
               case 1: op = BinaryOperator::CreateFNeg(i->getOperand(0),*var,&*i);
                       op1 = BinaryOperator::Create(Instruction::FAdd,op,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 2: op = BinaryOperator::Create(Instruction::FSub,
                           i->getOperand(0),
                           i->getOperand(1),*var,&*i);
                       op1 = BinaryOperator::Create(Instruction::FMul,op,
                           i->getOperand(1),"gen",&*i);
                       break;
             }
           }
         }
         if(opcode == Instruction::ICmp){ // Condition (with int)
           ICmpInst *currentI = (ICmpInst*)(&i);
           switch(llvm::cryptoutils->get_range(3)){ // must be improved
             case 0: //do nothing
               break;
             case 1: currentI->swapOperands();
                     break;
             case 2: // randomly change the predicate
                     switch(llvm::cryptoutils->get_range(10)){
                       case 0: currentI->setPredicate(ICmpInst::ICMP_EQ);
                               break; // equal
                       case 1: currentI->setPredicate(ICmpInst::ICMP_NE);
                               break; // not equal
                       case 2: currentI->setPredicate(ICmpInst::ICMP_UGT);
                               break; // unsigned greater than
                       case 3: currentI->setPredicate(ICmpInst::ICMP_UGE);
                               break; // unsigned greater or equal
                       case 4: currentI->setPredicate(ICmpInst::ICMP_ULT);
                               break; // unsigned less than
                       case 5: currentI->setPredicate(ICmpInst::ICMP_ULE);
                               break; // unsigned less or equal
                       case 6: currentI->setPredicate(ICmpInst::ICMP_SGT);
                               break; // signed greater than
                       case 7: currentI->setPredicate(ICmpInst::ICMP_SGE);
                               break; // signed greater or equal
                       case 8: currentI->setPredicate(ICmpInst::ICMP_SLT);
                               break; // signed less than
                       case 9: currentI->setPredicate(ICmpInst::ICMP_SLE);
                               break; // signed less or equal
                     }
                     break;
           }
 
         }
         if(opcode == Instruction::FCmp){ // Conditions (with float)
           FCmpInst *currentI = (FCmpInst*)(&i);
           switch(llvm::cryptoutils->get_range(3)){ // must be improved
             case 0: //do nothing
               break;
             case 1: currentI->swapOperands();
                     break;
             case 2: // randomly change the predicate
                     switch(llvm::cryptoutils->get_range(10)){
                       case 0: currentI->setPredicate(FCmpInst::FCMP_OEQ);
                               break; // ordered and equal
                       case 1: currentI->setPredicate(FCmpInst::FCMP_ONE);
                               break; // ordered and operands are unequal
                       case 2: currentI->setPredicate(FCmpInst::FCMP_UGT);
                               break; // unordered or greater than
                       case 3: currentI->setPredicate(FCmpInst::FCMP_UGE);
                               break; // unordered, or greater than, or equal
                       case 4: currentI->setPredicate(FCmpInst::FCMP_ULT);
                               break; // unordered or less than
                       case 5: currentI->setPredicate(FCmpInst::FCMP_ULE);
                               break; // unordered, or less than, or equal
                       case 6: currentI->setPredicate(FCmpInst::FCMP_OGT);
                               break; // ordered and greater than
                       case 7: currentI->setPredicate(FCmpInst::FCMP_OGE);
                               break; // ordered and greater than or equal
                       case 8: currentI->setPredicate(FCmpInst::FCMP_OLT);
                               break; // ordered and less than
                       case 9: currentI->setPredicate(FCmpInst::FCMP_OLE);
                               break; // ordered or less than, or equal
                     }
                     break;
           }
         }
       }
     }
for (BasicBlock::iterator i = alteredBB->begin(), e = alteredBB->end() ; i != e; ++i){
       // in the case we find binary operator, we modify slightly this part by randomly
       // insert some instructions
       if(i->isBinaryOp()){ // binary instructions
         unsigned opcode = i->getOpcode();
         BinaryOperator *op, *op1 = NULL;
         Twine *var = new Twine("_");
         // treat differently float or int
         // Binary int
         if(opcode == Instruction::Add || opcode == Instruction::Sub ||
             opcode == Instruction::Mul || opcode == Instruction::UDiv ||
             opcode == Instruction::SDiv || opcode == Instruction::URem ||
             opcode == Instruction::SRem || opcode == Instruction::Shl ||
             opcode == Instruction::LShr || opcode == Instruction::AShr ||
             opcode == Instruction::And || opcode == Instruction::Or ||
             opcode == Instruction::Xor){
           for(int random = (int)llvm::cryptoutils->get_range(10); random < 10; ++random){
             switch(llvm::cryptoutils->get_range(4)){ // to improve
               case 0: //do nothing
                 break;
               case 1: op = BinaryOperator::CreateNeg(i->getOperand(0),*var,&*i);
                       op1 = BinaryOperator::Create(Instruction::Add,op,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 2: op1 = BinaryOperator::Create(Instruction::Sub,
                           i->getOperand(0),
                           i->getOperand(1),*var,&*i);
                       op = BinaryOperator::Create(Instruction::Mul,op1,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 3: op = BinaryOperator::Create(Instruction::Shl,
                           i->getOperand(0),
                           i->getOperand(1),*var,&*i);
                       break;
             }
           }
         }
         // Binary float
         if(opcode == Instruction::FAdd || opcode == Instruction::FSub ||
             opcode == Instruction::FMul || opcode == Instruction::FDiv ||
             opcode == Instruction::FRem){
           for(int random = (int)llvm::cryptoutils->get_range(10); random < 10; ++random){
             switch(llvm::cryptoutils->get_range(3)){ // can be improved
               case 0: //do nothing
                 break;
               case 1: op = BinaryOperator::CreateFNeg(i->getOperand(0),*var,&*i);
                       op1 = BinaryOperator::Create(Instruction::FAdd,op,
                           i->getOperand(1),"gen",&*i);
                       break;
               case 2: op = BinaryOperator::Create(Instruction::FSub,
                           i->getOperand(0),

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2023-8-1 00:12 被寻梦之璐编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (2)
雪    币: 3525
活跃值: (31011)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2023-8-1 09:46
1
雪    币: 5
活跃值: (203)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
大佬能否分享一些关于ios的 我这里刚好有样本
2023-8-1 12:52
0
游客
登录 | 注册 方可回帖
返回
//