首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
19
17
[原创]LLVM Pass编写及去除 —— 控制流平坦化
发表于: 2026-4-15 21:00
13706
[原创]LLVM Pass编写及去除 —— 控制流平坦化
mb_wckjnnha
1
2026-4-15 21:00
13706
### 编写 ollvm更改了原始代码块的跳转关系,将代码分割为基本块,由分发块统一控制,如下图  基本块为由终结指令结尾的代码块,每个基本块都会跳转到返回块,返回块回到分发块进行下一次跳转 具体代码如下 ```cpp #include <llvm/IR/Function.h> #include <llvm/Pass.h> #include "llvm/IR/LegacyPassManager.h" #include "llvm/Transforms/IPO/PassManagerBuilder.h" #include "llvm/Support/raw_ostream.h" #include "llvm/IR/Instructions.h" #include "llvm/Transforms/Utils.h" #include "llvm/Support/CommandLine.h" #include "llvm/Transforms/Utils/Local.h" #include <SplitBasicBlock.h> #include <Utils.h> #include <vector> #include <cstdlib> #include <ctime> using namespace llvm; using std::vector; namespace{ class Flattening : public FunctionPass{ public: static char ID; Flattening() : FunctionPass(ID){ srand(time(0)); } void flatten(Function &F); bool runOnFunction(Function &F); }; } bool Flattening :: runOnFunction(Function &F) { INIT_CONTEXT(F); // 分割成更小块增加代码混淆程度 FunctionPass *pass = createSplitBasicBlockPass(); pass->runOnFunction(F); flatten(F); return true; } void Flattening :: flatten(Function &F) { if(F.size() <= 1) { return; } // 获取基本块 vector<BasicBlock*> origBB; for(BasicBlock &BB : F) { origBB.push_back(&BB); } // 去掉入口块 origBB.erase(origBB.begin()); // 获取入口块 BasicBlock &entryBB = F.getEntryBlock(); // 检查入口块终结指令是否为分支跳转指令 是的话放回基本块vector中 if(BranchInst *br = dyn_cast<BranchInst>(entryBB.getTerminator())) { if(br->isConditional()) { BasicBlock *newBB = entryBB.splitBasicBlock(br,"newBB"); origBB.insert(origBB.begin(),newBB); } } // 创建分发块 BasicBlock *dispatchBB = BasicBlock :: Create(*CONTEXT,"dispatchBB",&F,&entryBB); // 创建返回块 BasicBlock *returnBB = BasicBlock :: Create(*CONTEXT,"returnBB",&F,&entryBB); // 调整顺序 将入口块调至最前 entryBB.moveBefore(dispatchBB); // 去除入口块的跳转指令 entryBB.getTerminator()->eraseFromParent(); // 创建跳转指令 入口块跳转到分发块 BranchInst *brDispatchBB = BranchInst::Create(dispatchBB,&entryBB); int randNumCase = rand(); // 跳转指令之前新建一个指针 AllocaInst *swVarPtr = new AllocaInst(TYPE_I32,0,"swVar.ptr",brDispatchBB); // 将生成的随机数存到指针中 new StoreInst(CONST_I32(randNumCase),swVarPtr,brDispatchBB); // 获取随机数 LoadInst *swVar = new LoadInst(TYPE_I32,swVarPtr,"swVar",false,dispatchBB); // 创建默认块 BasicBlock *swDefault = BasicBlock :: Create(*CONTEXT,"swDefault",&F,returnBB); // 创建跳转指令 使默认块跳转到返回块 BranchInst::Create(returnBB,swDefault); // 创建跳转指令 使返回块跳转到分发块 BranchInst::Create(dispatchBB, returnBB); // 创建swich指令 SwitchInst *swInst = SwitchInst::Create(swVar,swDefault,0,dispatchBB); // 每一个基本块都加入case for(BasicBlock *BB : origBB) { BB->moveBefore(returnBB); swInst->addCase(CONST_I32(randNumCase),BB); randNumCase = rand(); } for(BasicBlock *BB : origBB) { if(BB->getTerminator()->getNumSuccessors() == 0) { continue; } // 后继块只有一个 else if(BB->getTerminator()->getNumSuccessors() == 1) { // 获取后继块 BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0); // 找到后继块跳转的case 保存到指针中 ConstantInt *numCase = swInst->findCaseDest(sucBB); // 删除尾部跳转 BB->getTerminator()->eraseFromParent(); new StoreInst(numCase,swVarPtr,BB); // 创建跳转指令 跳回返回块 BranchInst::Create(returnBB,BB); } else if(BB->getTerminator()->getNumSuccessors() == 2) { // 获取后继块 ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0)); ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1)); BranchInst *br = cast<BranchInst>(BB->getTerminator()); // 选择条件对应的case SelectInst *sel = SelectInst::Create(br->getCondition(),KeyTrue,keyFalse,"",BB->getTerminator()); // 删除终结指令 BB->getTerminator()->eraseFromParent(); // 存储case值 new StoreInst(sel,swVarPtr,BB); // 创建跳转指令 跳回返回块 BranchInst::Create(returnBB,BB); } } // 修复PHI指令和逃逸变量 fixStack(F); } char Flattening::ID = 0; static RegisterPass<Flattening> X("fla", "Flatten basic blocks"); ``` ### 去除 利用swich case结构控制下一个跳转的基本快,但是这种方式写出来的case值会直接暴露在代码中,很容易找到跳转关系 我们可以尝试手动去除一下这种ollvm  在loc_4011DF这个基本块中,给存放case变量的指针赋值,然后回到返回块,此时0x5CAFE12C就是分发器要找的下一个case值  找到比较0x5CAFE12C的分发器,就可以顺藤摸瓜发现跳转的下一个块为loc_4011F2  也就可以据此恢复跳转关系去除混淆 知道原理之后就可以写出idc脚本批量操作 ```cpp // 基本块入手 #include <idc.idc> static NopCode(Addr, Length) { auto i; for (i = 0; i < Length; i++) { PatchByte(Addr + i, 0x90); } } static findCaseVal(targetByte,cmovAddr) { while(1) { auto currentByte = Byte(cmovAddr); auto caseVal = Dword(cmovAddr + 1); if(currentByte == targetByte) { msg("find caseVal : %X\n",caseVal); return caseVal; } cmovAddr = prev_head(cmovAddr,cmovAddr - 8); } } static findCaseFunc(start_addr,end_addr,caseVal) { msg("find case : %X\n",caseVal); while(start_addr != BADADDR && start_addr < end_addr) { auto op = Byte(start_addr); auto checkCase = Dword(start_addr + 1); if(op == 0x2D && checkCase == caseVal) { auto jz_addr = start_addr + 5; auto offest = Dword(jz_addr + 2); auto caseFunc = offest + jz_addr + 6; return caseFunc; } start_addr = next_head(start_addr, end_addr); } } static main() { auto seg; for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg)) { auto seg_name = get_segm_name(seg); //msg(seg_name + "\n"); if (seg_name == ".text") { auto start_addr = 0x401130; auto current_addr = 0x401130; auto end_addr = 0x40128D; while (current_addr != BADADDR && current_addr < end_addr) { auto setValPtr = Word(current_addr + 1); auto jmpCode = Byte(current_addr + 7); auto jmpCodeBranch = Byte(current_addr + 3); // jmp if(setValPtr == 0xD845 && jmpCode == 0xE9) { auto caseVal; auto caseFunc; auto returnAddr; auto jmpOffset; auto patchByte; //msg("setValPtr : %X",current_addr); // jmp caseVal = Dword(current_addr + 3); msg("caseVal : %X\n",caseVal); caseFunc = findCaseFunc(start_addr,end_addr,caseVal); returnAddr = current_addr + 7; msg("case func : %X\n",caseFunc); msg("return addr : %X\n",returnAddr); //msg("current addr : %X\n",current_addr); jmpOffset = caseFunc - returnAddr; if(jmpOffset >= -126 && jmpOffset <= 129) { patchByte = jmpOffset - 2; PatchByte(returnAddr,0xEB); PatchByte(returnAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); NopCode(returnAddr + 2,3); } else { patchByte = jmpOffset - 5; PatchDword(returnAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } } // branch if(setValPtr == 0xD845 && jmpCodeBranch == 0xE9) { msg("branch jmp addr : %X\n",current_addr); auto cmovAddr = prev_head(current_addr,start_addr); auto cmovOp = print_insn_mnem(cmovAddr); auto op1 = print_operand(cmovAddr,0); auto op2 = print_operand(cmovAddr,1); auto brByte; auto brShortByte; auto regByte; auto caseFunc_op1; auto caseFunc_op2; auto brOffest; auto jmpAddr; if(cmovOp == "cmovb") { brShortByte = 0x72; brByte = 0x820F; } // else comv... if(op1 == "eax") { regByte = 0xB8; caseVal = findCaseVal(regByte,cmovAddr); msg("caseVal : %X\n",caseVal); caseFunc_op1 = findCaseFunc(start_addr,end_addr,caseVal); msg("case func : %X\n",caseFunc_op1); } //else op1... if(op2 == "ecx") { regByte = 0xB9; caseVal = findCaseVal(regByte,cmovAddr); msg("caseVal : %X\n",caseVal); caseFunc_op2 = findCaseFunc(start_addr,end_addr,caseVal); msg("case func : %X\n",caseFunc_op2); } // else op2... returnAddr = current_addr + 3; brOffest = caseFunc_op2 - cmovAddr; msg("br offest : %X\n",brOffest); NopCode(cmovAddr,returnAddr + 5 - cmovAddr); msg("nop addr : %X len : %X\n",cmovAddr,returnAddr + 5 - cmovAddr); // short br if(brOffest > -127 && brOffest < 130) { PatchByte(cmovAddr,brShortByte); patchByte = brOffest - 2; PatchByte(cmovAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } else { PatchWord(cmovAddr,brByte); patchByte = brOffest - 5; PatchByte(cmovAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } jmpAddr = next_head(cmovAddr,end_addr); jmpOffset = caseFunc_op1 - jmpAddr; msg("jmp offest : %X\n",jmpOffset); // short jmp if(jmpOffset >= -126 && jmpOffset <= 129) { patchByte = jmpOffset - 2; PatchByte(jmpAddr,0xEB); PatchByte(jmpAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } else { patchByte = jmpOffset - 5; PatchByte(jmpAddr,0xE9); PatchDword(jmpAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } } current_addr = next_head(current_addr, end_addr); } } } } ``` 脚本从基本块入手,从pass编写脚本可知每个基本块的最后两条指令为给swValPtr赋值和跳转到返回块,且基本块必定只有一个跳转指令,可以根据这些特征轻松定位基本块并进行处理 效果如下 原始伪代码: ```c __int64 __fastcall enc(unsigned __int8 *a1) { __int64 result; // rax int v2; // eax int swValPtr; // [rsp+4h] [rbp-28h] int v4; // [rsp+8h] [rbp-24h] unsigned __int64 v5; // [rsp+14h] [rbp-18h] unsigned __int8 *v6; // [rsp+1Ch] [rbp-10h] unsigned __int8 v7; // [rsp+27h] [rbp-5h] int v8; // [rsp+28h] [rbp-4h] swValPtr = 0x5DE91606; while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( swValPtr == 0x17070602 ) { v4 = v8; swValPtr = 0x5CAFE12C; } if ( swValPtr != 0x26ADAA22 ) break; v2 = 955080127; if ( v5 < 8 ) v2 = 0x5EA2D108; swValPtr = v2; } if ( swValPtr != 0x32A4BB49 ) break; *v6 = v7 ^ 0xA; swValPtr = 1094857714; } result = (unsigned int)(swValPtr - 0x38ED5DBF); if ( swValPtr == 0x38ED5DBF ) break; switch ( swValPtr ) { case 0x414233F2: v8 = v4 + 1; swValPtr = 386336258; break; case 0x5CAFE12C: v5 = v4; swValPtr = 648915490; break; case 0x5DE91606: v4 = 0; swValPtr = 0x5CAFE12C; break; default: v6 = &a1[v4]; v7 = *v6; swValPtr = 0x32A4BB49; break; } } return result; } ``` 使用idc脚本去混淆后 ```c __int64 __fastcall enc(unsigned __int8 *a1) { __int64 result; // rax int i; // [rsp+8h] [rbp-24h] for ( i = 0; ; ++i ) { result = 955080127; if ( (unsigned __int64)i >= 8 ) break; a1[i] ^= 0xAu; } return result; } ``` 可以成功去除 ### 魔改 但是这样的ollvm混淆,case值直接暴露在汇编中,使用d810等工具就可以一把梭,为了增加混淆去除难度,可以对case值的生成进行魔改 ```c for(BasicBlock *BB : origBB) { if(BB->getTerminator()->getNumSuccessors() == 0) { continue; } // 后继块只有一个 else if(BB->getTerminator()->getNumSuccessors() == 1) { // 获取后继块 BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0); // 找到后继块跳转的case 保存到指针中 ConstantInt *numCase = swInst->findCaseDest(sucBB); // 新增魔改case ConstantInt *nowCase = swInst->findCaseDest(BB); // 当前case值 int nowCaseVal = nowCase->getZExtValue(); // 后继块case值 int numCaseVal = numCase->getZExtValue(); // 计算出异或运算需要的值 int xorKey = nowCaseVal ^ numCaseVal; LoadInst *currentSwVar = new LoadInst(TYPE_I32,swVarPtr,"curVar",false,BB->getTerminator()); ConstantInt *keyStone = CONST_I32(xorKey); // 增加异或运算 BinaryOperator *nextSwVar = BinaryOperator::CreateXor(currentSwVar, keyStone, "nextSwVar", BB->getTerminator()); // 删除尾部跳转 BB->getTerminator()->eraseFromParent(); new StoreInst(nextSwVar,swVarPtr,BB); // 创建跳转指令 跳回返回块 BranchInst::Create(returnBB,BB); } else if(BB->getTerminator()->getNumSuccessors() == 2) { // 获取后继块 ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0)); ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1)); BranchInst *br = cast<BranchInst>(BB->getTerminator()); // 新增魔改case ConstantInt *nowCase = swInst->findCaseDest(BB); int nowCaseVal = nowCase->getZExtValue(); // 正确分支case值 int trueCaseVal = numCaseTrue->getZExtValue(); // 错误分支case值 int falseCaseVal = numCaseFalse->getZExtValue(); int keyTrueVal = nowCaseVal ^ trueCaseVal; int keyFalseVal = nowCaseVal ^ falseCaseVal; ConstantInt *KeyTrue = CONST_I32(keyTrueVal); ConstantInt *keyFalse = CONST_I32(keyFalseVal); // 选择条件对应的case SelectInst *sel = SelectInst::Create(br->getCondition(),KeyTrue,keyFalse,"",BB->getTerminator()); LoadInst *currentSwVar = new LoadInst(TYPE_I32,swVarPtr,"curVal",BB->getTerminator()); BinaryOperator *nextSwVar = BinaryOperator::CreateXor(currentSwVar,sel,"newSwVar",BB->getTerminator()); // 删除终结指令 BB->getTerminator()->eraseFromParent(); // 存储case值 new StoreInst(nextSwVar,swVarPtr,BB); // 创建跳转指令 跳回返回块 BranchInst::Create(returnBB,BB); } } ``` 如上,在处理基本块时,对case值的生成新增了异或运算,间接运算case值可以使d810失效  依旧手动去除一下 ### 去除 图中可以看到loc_4011F6的case值为0x772811A7,自身异或的值为0x15FBEF3,运算后得到0x7677AF54  得到后继块为loc_40120E 如果有两个后继块,图中条件为cmovb,相应应该更改为jb + jmp 编写idc脚本去除 ```c // 分发块入手 #include <idc.idc> static NopCode(Addr, Length) { auto i; for (i = 0; i < Length; i++) { PatchByte(Addr + i, 0x90); } } static findCaseVal(targetByte,cmov_addr) { while(1) { auto currentByte = Byte(cmov_addr); auto caseVal = Dword(cmov_addr + 1); if(currentByte == targetByte) { msg("find caseVal : %X\n",caseVal); return caseVal; } cmov_addr = prev_head(cmov_addr,cmov_addr - 8); } } static findCaseFunc(start_addr,end_addr,caseVal) { msg("find case : %X\n",caseVal); while(start_addr != BADADDR && start_addr < end_addr) { auto op = Byte(start_addr); auto checkCase = Dword(start_addr + 1); if(op == 0x2D && checkCase == caseVal) { auto jz_addr = start_addr + 5; auto offest = Dword(jz_addr + 2); auto caseFunc = offest + jz_addr + 6; return caseFunc; } start_addr = next_head(start_addr, end_addr); } } static findJmpAddr(start_addr,end_addr) { auto i; for(i = start_addr; i < end_addr; i = next_head(i,end_addr)) { auto jmp_byte = Byte(i); if(jmp_byte == 0xE9) { return i; } } } static checkType(start_addr,end_addr) { auto i; for(i = start_addr; i < end_addr; i = next_head(i,end_addr)) { auto xor_byte = Byte(i); auto movSwValPtr_byte = Byte(i + 5); auto movSwValPtr_branch = Byte(i + 3); if(xor_byte == 0x35 && movSwValPtr_byte == 0x89) { return 1; } else if(xor_byte == 0x33 && movSwValPtr_branch == 0x89) { return 2; } } return 0; } static findXorData(start_addr) { while(1) { auto xor_op = Byte(start_addr); auto mov_op = Byte(start_addr + 5); if(xor_op == 0x35 && mov_op == 0x89) { auto data = Dword(start_addr + 1); break; } start_addr = next_head(start_addr, start_addr + 8); } return data; } static main() { auto seg; for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg)) { auto seg_name = get_segm_name(seg); //msg(seg_name + "\n"); if (seg_name == ".text") { auto start_addr = 0x401130; auto current_addr = 0x401130; auto end_addr = 0x4012A5; while (current_addr != BADADDR && current_addr < end_addr) { auto sub_code = Byte(current_addr); auto jz_addr = current_addr + 5; auto jz_code = Word(jz_addr); auto jz_offest = Dword(jz_addr + 2); auto check_dword = Dword(current_addr + 1); if(sub_code == 0x2D && jz_code == 0x840F) { auto basic_block_addr = jz_addr + jz_offest + 6; msg("check dword : %X\n",check_dword); msg("basic block addr : %X\n",basic_block_addr); auto type = checkType(basic_block_addr,end_addr); msg("type : %X\n",type); // jmp if(type == 1) { auto xor_data = findXorData(basic_block_addr); msg("xor data : %X\n",xor_data); auto swVar = xor_data ^ check_dword; auto caseFunc = findCaseFunc(start_addr,end_addr,swVar); msg("case func : %X\n",caseFunc); auto jmp_addr = findJmpAddr(basic_block_addr,end_addr); auto offest = caseFunc - jmp_addr; auto patchByte; if(offest >= -126 && offest <= 129) { patchByte = offest - 2; PatchByte(jmp_addr,0xEB); PatchByte(jmp_addr + 1,patchByte); msg("patch byte : %X\n",patchByte); NopCode(jmp_addr + 2,3); } else { patchByte = offest - 5; PatchDword(jmp_addr + 1,patchByte); } NopCode(jmp_addr - 8,8); } // branch if(type == 2) { jmp_addr = findJmpAddr(basic_block_addr,end_addr); auto mov_addr = prev_head(jmp_addr,basic_block_addr); auto xor_addr = prev_head(mov_addr,basic_block_addr); auto cmov_addr = prev_head(xor_addr,basic_block_addr); auto cmov_op = print_insn_mnem(cmov_addr); auto op1 = print_operand(cmov_addr,0); auto op2 = print_operand(cmov_addr,1); auto br_byte; auto br_short; auto caseFunc_op1; auto caseFunc_op2; auto regByte; if(cmov_op == "cmovb") { br_short = 0x72; br_byte = 0x820F; } // other comv... if(op1 == "eax") { regByte = 0xB8; xor_data = findCaseVal(regByte,cmov_addr); msg("xor data : %X\n",xor_data); swVar = check_dword ^ xor_data; caseFunc_op1 = findCaseFunc(start_addr,end_addr,swVar); msg("case func : %X\n",caseFunc_op1); } // other op1... if(op2 == "ecx") { regByte = 0xB9; xor_data = findCaseVal(regByte,cmov_addr); msg("xor data : %X\n",xor_data); swVar = check_dword ^ xor_data; caseFunc_op2 = findCaseFunc(start_addr,end_addr,swVar); msg("case func : %X\n",caseFunc_op2); } // other op2... auto brOffest = caseFunc_op2 - cmov_addr; msg("br offest : %X\n",brOffest); NopCode(cmov_addr,jmp_addr + 5 - cmov_addr); msg("nop addr : %X len : %X\n",cmov_addr,jmp_addr + 5 - cmov_addr); // short br if(brOffest > -127 && brOffest < 130) { PatchByte(cmov_addr,br_short); patchByte = brOffest - 2; PatchByte(cmov_addr + 1,patchByte); msg("patch byte : %X\n",patchByte); } else { PatchWord(cmov_addr,br_byte); patchByte = brOffest - 5; PatchByte(cmov_addr + 1,patchByte); msg("patch byte : %X\n",patchByte); } auto jmpAddr = next_head(cmov_addr,end_addr); auto jmpOffset = caseFunc_op1 - jmpAddr; msg("jmp offest : %X\n",jmpOffset); // short jmp if(jmpOffset >= -126 && jmpOffset <= 129) { patchByte = jmpOffset - 2; PatchByte(jmpAddr,0xEB); PatchByte(jmpAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } else { patchByte = jmpOffset - 5; PatchByte(jmpAddr,0xE9); PatchDword(jmpAddr + 1,patchByte); msg("patch byte : %X\n",patchByte); } } } current_addr = next_head(current_addr, end_addr); } } } } ``` 因为异或运算需要自身case值,所以可以从分发块开始解析 去混淆效果如下 去混淆前 ```c __int64 __fastcall enc(unsigned __int8 *a1) { __int64 result; // rax int v2; // eax int swValPtr; // [rsp+4h] [rbp-28h] int v4; // [rsp+8h] [rbp-24h] unsigned __int64 v5; // [rsp+14h] [rbp-18h] unsigned __int8 *v6; // [rsp+1Ch] [rbp-10h] unsigned __int8 v7; // [rsp+27h] [rbp-5h] int v8; // [rsp+28h] [rbp-4h] swValPtr = 1392335309; while ( 1 ) { while ( 1 ) { while ( swValPtr == 80288048 ) { v6 = &a1[v4]; v7 = *v6; swValPtr = 1219708018; } if ( swValPtr != 188876390 ) break; v8 = v4 + 1; swValPtr = 1299578078; } result = (unsigned int)(swValPtr - 989136729); if ( swValPtr == 989136729 ) break; switch ( swValPtr ) { case 1219708018: *v6 = v7 ^ 0xA; swValPtr = 188876390; break; case 1299578078: v4 = v8; swValPtr = 1999114663; break; case 1392335309: v4 = 0; swValPtr = 1999114663; break; case 1987555156: v2 = 1283631117; if ( v5 < 8 ) v2 = 1925101156; swValPtr ^= v2; break; case 1999114663: v5 = v4; swValPtr = 1987555156; break; } } return result; } ``` 去除混淆后 ```c __int64 __fastcall enc(unsigned __int8 *a1) { __int64 result; // rax int i; // [rsp+8h] [rbp-24h] for ( i = 0; ; ++i ) { result = 1283631117; if ( (unsigned __int64)i >= 8 ) break; a1[i] ^= 0xAu; } return result; } ``` idc脚本去除ollvm混淆的思路是通用的,但是文中代码只是根据我手里被混淆过的文件特征进行去除,如果想在不同的文件中使用,最好把一些常用代码封装成函数,根据不同的ollvm魔改方式进行去除 ### 例 :CrackMe 此题为魔改ollvm,以它为例,讲解如何分析及去除一个陌生的ollvm,并写出idc脚本一键处理  观察cfg,发现和之前学到的有些不同,只有入口块、分发块和基本块,缺少了返回块  每个基本块都是直接返回到主分发块,而且也没有将case值压入指针的操作,那么分发块是如何确定下一个跳转的基本块的呢 再来看主分发块  最后与case值比较的是寄存器eax,计算公式如图,只有ecx是不确定的。而在刚刚的基本块中,每个块都会对ecx赋值并运算,所以这就是为什么没有返回块的原因:每个基本块都要计算出ecx的值,然后传到主分发块去计算case值 现在,我们手动计算一下loc_140001F5C的case值,得到`5DE42860`,接下来找子分发块,看这个case值对应的地址是多少  然而并没有子分发块比较的值等于`5DE42860`。 观察图中的子分发块,除了最下层是`jz / jnz`这种相等比较,其余都是大小比较,也就是说子分发块没有将所有的case值都明写出来,而是通过比大小的方式决定。像刚刚的`5DE42860`,在层层比较下,最终应该跳转到loc_140001DF2。 我们知道ollvm是将原本块与块的关系打乱,统一由分发块管理,那么去除ollvm就是将块与块的关系恢复。上面的例子已经得到了loc_140001F5C -> loc_140001DF2的关系,接下来就是对每个分发块进行同样的操作 idc脚本如下: ```c #include <idc.idc> static NopCode(Addr, Length) { auto i; for (i = 0; i < Length; i++) { PatchByte(Addr + i, 0x90); } } static calCaseVal(key_data) { auto mul_data = 0x3650F7CD; auto xor_data = 0x268916D7; auto caseVal = (((mul_data * key_data) & 0xFFFFFFFF) ^ xor_data) & 0xFFFFFFFF; return caseVal; } static checkJmp(jmp_name) { if(jmp_name == "jg" | jmp_name == "jz" | jmp_name == "jnz" | jmp_name == "jle" | jmp_name == "jmp") { return 1; } return 0; } static getRegData(current_addr,start_addr,reg_name) { auto xor_reg_name; auto add_data = 0; auto sub_data = 0; auto xor_data = 0; while(current_addr != BADADDR && current_addr > start_addr) { auto insn_name = print_insn_mnem(current_addr); auto reg = print_operand(current_addr,0); auto op2 = print_operand(current_addr,1); auto data = get_operand_value(current_addr,1) & 0xFFFFFFFF; if(reg == reg_name) { if(insn_name == "mov") { auto reg_data = data; //msg("mov data : %X\n",data); break; } if(insn_name == "add") { add_data = data; //msg("add data : %X\n",data); } if(insn_name == "sub") { sub_data = data; //msg("sub data : %X\n",data); } if(insn_name == "xor") { xor_reg_name = op2; //msg("xor reg name : " + xor_reg_name + "\n"); } } if(reg == xor_reg_name) { if(insn_name == "mov") { xor_data = data; //msg("xor data : %X\n",xor_data); } } current_addr = prev_head(current_addr,start_addr); } //msg("key data = (%X + %X - %X) ^ %X\n",reg_data,add_data,sub_data,xor_data); auto key_data = ((reg_data + add_data - sub_data) ^ xor_data) & 0xFFFFFFFF; return key_data; } extern ecx_data; extern other_data; static getKeyData(current_addr,start_addr,type) { if(type == 1) { return getRegData(current_addr,start_addr,"ecx"); } if(type == 2) { ecx_data = getRegData(current_addr,start_addr,"ecx"); auto other_reg = getOtherReg(current_addr,start_addr); other_data = getRegData(current_addr,start_addr,other_reg); } } static getOtherReg(current_addr,start_addr) { current_addr = prev_head(current_addr,start_addr); while(current_addr != BADADDR && current_addr > start_addr) { auto cmov_insn = print_insn_mnem(current_addr); auto other_reg = print_operand(current_addr,1); //msg("cmov insn :" + cmov_insn + "\n"); if(strstr(cmov_insn, "cmov", 0) != -1) { return other_reg; } if(checkJmp(cmov_insn)) { break; } current_addr = prev_head(current_addr,start_addr); } return 1; } static getCmovType(current_addr,start_addr) { current_addr = prev_head(current_addr,start_addr); while(current_addr != BADADDR && current_addr > start_addr) { auto cmov_insn = print_insn_mnem(current_addr); //msg("cmov insn :" + cmov_insn + "\n"); if(strstr(cmov_insn, "cmov", 0) != -1) { if(cmov_insn == "cmovb") { return "jb"; } if(cmov_insn == "cmovz") { return "jz"; } if(cmov_insn == "cmovnz") { return "jnz"; } } if(checkJmp(cmov_insn)) { break; } current_addr = prev_head(current_addr,start_addr); } return 1; } static checkType(current_addr,start_addr) { current_addr = prev_head(current_addr,start_addr); while(current_addr != BADADDR && current_addr > start_addr) { auto cmov_insn = print_insn_mnem(current_addr); //msg("cmov insn :" + cmov_insn + "\n"); if(strstr(cmov_insn, "cmov", 0) != -1) { return 2; } if(checkJmp(cmov_insn)) { break; } current_addr = prev_head(current_addr,start_addr); } return 1; } static isBasicBlock(branch_addr) { auto branch_name = print_insn_mnem(branch_addr); auto op1 = print_operand(branch_addr,0); if(branch_name == "cmp" && op1 == "eax") { return 0; } return 1; } static findJmpAddr(jmp_basicblock_addr,caseVal,current_addr,end_addr) { while (current_addr != BADADDR && current_addr < end_addr) { auto op = Byte(current_addr); auto cmpCase = Dword(current_addr + 1); auto jmp_insn_addr = current_addr + 5; auto jmp_name = print_insn_mnem(jmp_insn_addr); auto next_addr = next_head(jmp_insn_addr,end_addr); auto insn_len = next_addr - (jmp_insn_addr); auto offset; if(insn_len < 5) { offset = Byte(current_addr + 6); } else { offset = Dword(current_addr + 7); } if(offset > 0xFF000000) { offset = 0xFFFFFFFF00000000 | offset; } auto jmp_addr = next_addr + offset; auto is_jmp = checkJmp(jmp_name); auto greater_branch; auto other_branch; auto equal_branch; auto branch_addr; if(op == 0x3D && is_jmp) { //msg("swich cmp : %X\n",current_addr); //msg("cmp data : %X\n",cmpCase); //msg("my data : %X\n",caseVal); //msg("jmp addr : %X\n",jmp_addr); if(jmp_name == "jg") { greater_branch = jmp_addr; other_branch = next_addr; if(caseVal > cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,greater_branch); branch_addr = greater_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(jmp_name == "jz") { equal_branch = jmp_addr; other_branch = next_addr; if(caseVal == cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,equal_branch); branch_addr = equal_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(jmp_name == "jnz") { equal_branch = next_addr; other_branch = jmp_addr; if(caseVal == cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,equal_branch); branch_addr = equal_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(jmp_name == "jle") { greater_branch = next_addr; other_branch = jmp_addr; if(caseVal > cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,greater_branch); branch_addr = greater_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(isBasicBlock(branch_addr)) { break; } current_addr = prev_head(branch_addr,branch_addr - 10); //msg("current addr : %X\n",current_addr); } current_addr = next_head(current_addr, end_addr); } msg("%X -> %X\n",jmp_basicblock_addr,branch_addr); return branch_addr; } static findMovAddr(current_addr,start_addr) { current_addr = prev_head(current_addr,start_addr); while(current_addr != BADADDR && current_addr > start_addr) { auto reg_name = print_operand(current_addr,0); auto op = print_insn_mnem(current_addr); if(op == "mov" && reg_name == "ecx") { return current_addr; } if(checkJmp(op)) { break; } current_addr = prev_head(current_addr,start_addr); } } static PatchJmp(current_addr,next_insn_addr,start_addr,branch_addr_ecx,branch_addr_other,type,cmov_type) { auto mov_addr; auto nop_len; auto offset; mov_addr = findMovAddr(current_addr,start_addr); nop_len = next_insn_addr - mov_addr; NopCode(mov_addr,nop_len); if(type == 1) { PatchByte(mov_addr,0xE9); offset = branch_addr_ecx - mov_addr - 5; PatchDword(mov_addr + 1,offset); } if(type == 2) { auto cmov_byte; if(cmov_type == "jz") { cmov_byte = 0x840F; } if(cmov_type == "jnz") { cmov_byte = 0x850F; } auto offest_other = branch_addr_other - mov_addr - 6; PatchWord(mov_addr,cmov_byte); PatchDword(mov_addr + 2,offest_other); auto jmp_addr = mov_addr + 6; PatchByte(jmp_addr,0xE9); auto offest_ecx = branch_addr_ecx - jmp_addr - 5; PatchDword(jmp_addr + 1,offest_ecx); } } static main() { auto current_addr = 0x1400017D8; auto start_addr = 0x1400017D8; auto end_addr = 0x140001FAF; while (current_addr != BADADDR && current_addr < end_addr) { auto jmp_returnBlock = Byte(current_addr); auto next_insn_addr = next_head(current_addr,end_addr); auto jmp_returnBlock_offset = Dword(current_addr + 1); if(jmp_returnBlock_offset > 0xFF000000) { //msg("jmp return offset : %X\n",jmp_returnBlock_offset); jmp_returnBlock_offset = 0xFFFFFFFF00000000 | jmp_returnBlock_offset; } auto swichBlock_addr = 0x1400019E0; // basicblock if(jmp_returnBlock == 0xE9 && next_insn_addr + jmp_returnBlock_offset == swichBlock_addr) { msg("basicblock addr : %X\n",current_addr); auto type = checkType(current_addr,start_addr); auto key_data; auto caseVal; auto branch_addr; // branch == 1 if(type == 1) { key_data = getKeyData(current_addr,start_addr,type); caseVal = calCaseVal(key_data); branch_addr = findJmpAddr(current_addr,caseVal,start_addr,end_addr); PatchJmp(current_addr,next_insn_addr,start_addr,branch_addr,0,type,0); } // branch == 2 if(type == 2) { getKeyData(current_addr,start_addr,type); key_data = ecx_data; auto caseVal_ecx = calCaseVal(key_data); auto caseVal_other = calCaseVal(other_data); auto cmov_type = getCmovType(current_addr,start_addr); msg(cmov_type + " "); auto branch_addr_other = findJmpAddr(current_addr,caseVal_other,start_addr,end_addr); msg("jmp "); auto branch_addr_ecx = findJmpAddr(current_addr,caseVal_ecx,start_addr,end_addr); PatchJmp(current_addr,next_insn_addr,start_addr,branch_addr_ecx,branch_addr_other,type,cmov_type); } } current_addr = next_head(current_addr, end_addr); } } ``` #### 脚本解析 延续之前的思路,从基本块开始解析,因为每个基本块的结尾都是跳转到一个固定的地址,之前是跳转到返回块,现在是跳转到主分发块,相对于其他地方更好定位 ```c // 获取当前指令的第一个字节 验证是否为跳转指令(0xE9) auto jmp_returnBlock = Byte(current_addr); auto next_insn_addr = next_head(current_addr,end_addr); // 获取当前指令 + 1处的四字 如果第一个字节为跳转 则此处为偏移 auto jmp_returnBlock_offset = Dword(current_addr + 1); if(jmp_returnBlock_offset > 0xFF000000) { // 处理偏移为负数的情况 jmp_returnBlock_offset = 0xFFFFFFFF00000000 | jmp_returnBlock_offset; } auto swichBlock_addr = 0x1400019E0; // 校验是否为跳转到主分发块的指令 if(jmp_returnBlock == 0xE9 && next_insn_addr + jmp_returnBlock_offset == swichBlock_addr) { // 对基本块处理 } ``` 找到基本块后,就该计算出相应的ecx值,对基本块的处理应该分后继块的数量讨论 ```c static getRegData(current_addr,start_addr,reg_name) { auto xor_reg_name; auto add_data = 0; auto sub_data = 0; auto xor_data = 0; while(current_addr != BADADDR && current_addr > start_addr) { auto insn_name = print_insn_mnem(current_addr); // 操作数1 auto reg = print_operand(current_addr,0); // 操作数2 auto op2 = print_operand(current_addr,1); // 操作数2的具体数值 auto data = get_operand_value(current_addr,1) & 0xFFFFFFFF; // 如果操作数1为我们想要的计算器 if(reg == reg_name) { if(insn_name == "mov") { // 赋值操作 auto reg_data = data; //msg("mov data : %X\n",data); break; } if(insn_name == "add") { // 加法操作 add_data = data; //msg("add data : %X\n",data); } if(insn_name == "sub") { // 减法操作 sub_data = data; //msg("sub data : %X\n",data); } if(insn_name == "xor") { // 程序中异或操作并不是异或立即数 而是另一个寄存器 xor_reg_name = op2; //msg("xor reg name : " + xor_reg_name + "\n"); } } // 找对异或寄存器的赋值 if(reg == xor_reg_name) { if(insn_name == "mov") { xor_data = data; //msg("xor data : %X\n",xor_data); } } current_addr = prev_head(current_addr,start_addr); } // 计算最终返回值 auto key_data = ((reg_data + add_data - sub_data) ^ xor_data) & 0xFFFFFFFF; return key_data; } ``` 计算出ecx的值之后,就可以根据固定公式得知该基本块的case值。 接下来就是处理分发块找下一个跳转块,子分发块也很好定位,每个块都是比较 + 跳转,跳转关系只有两种:大于 / 不大于、等于 / 不等于,因为跳转指令只有`jg`,`jle`,`jz`,`jnz` ```c static findJmpAddr(jmp_basicblock_addr,caseVal,current_addr,end_addr) { while (current_addr != BADADDR && current_addr < end_addr) { auto op = Byte(current_addr); auto cmpCase = Dword(current_addr + 1); auto jmp_insn_addr = current_addr + 5; auto jmp_name = print_insn_mnem(jmp_insn_addr); auto next_addr = next_head(jmp_insn_addr,end_addr); auto insn_len = next_addr - (jmp_insn_addr); auto offset; // 短跳转的特殊处理 if(insn_len < 5) { offset = Byte(current_addr + 6); } else { offset = Dword(current_addr + 7); } // 负数的处理 if(offset > 0xFF000000) { offset = 0xFFFFFFFF00000000 | offset; } auto jmp_addr = next_addr + offset; auto is_jmp = checkJmp(jmp_name); auto greater_branch; auto other_branch; auto equal_branch; auto branch_addr; if(op == 0x3D && is_jmp) { // 跳转指令为大于时 if(jmp_name == "jg") { // 要跳转的地址储存到大于分支 greater_branch = jmp_addr; // 下一个地址储存到其他分支 other_branch = next_addr; if(caseVal > cmpCase) { // 大于则把大于分支储存到跳转地址里 branch_addr = greater_branch; } else { // 不大于则把其他分支储存到跳转地址里 branch_addr = other_branch; } } // 其他同理 if(jmp_name == "jz") { equal_branch = jmp_addr; other_branch = next_addr; if(caseVal == cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,equal_branch); branch_addr = equal_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(jmp_name == "jnz") { equal_branch = next_addr; other_branch = jmp_addr; if(caseVal == cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,equal_branch); branch_addr = equal_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(jmp_name == "jle") { greater_branch = next_addr; other_branch = jmp_addr; if(caseVal > cmpCase) { //msg("%X -> %X\n",jmp_basicblock_addr,greater_branch); branch_addr = greater_branch; } else { //msg("%X -> %X\n",jmp_basicblock_addr,other_branch); branch_addr = other_branch; } } if(isBasicBlock(branch_addr)) { break; } current_addr = prev_head(branch_addr,branch_addr - 10); //msg("current addr : %X\n",current_addr); } current_addr = next_head(current_addr, end_addr); } // 最后打印出执行跳转指令的地址 -> 要跳转的地址 msg("%X -> %X\n",jmp_basicblock_addr,branch_addr); return branch_addr; } ``` 这样就分析出了最关键的跳转关系,可以跑一下看结果 ``` basicblock addr : 140001A77 140001A77 -> 140001F94 basicblock addr : 140001AEE jnz 140001AEE -> 140001F5C jmp 140001AEE -> 140001DB9 basicblock addr : 140001BF9 jz 140001BF9 -> 140001A2F jmp 140001BF9 -> 140001E7A basicblock addr : 140001C4B jz 140001C4B -> 140001E7A jmp 140001C4B -> 140001AA1 basicblock addr : 140001D16 jnz 140001D16 -> 140001B16 jmp 140001D16 -> 140001E3C basicblock addr : 140001D45 140001D45 -> 140001E5E basicblock addr : 140001DB4 jz 140001DB4 -> 140001990 jmp 140001DB4 -> 140001D26 basicblock addr : 140001DED 140001DED -> 140001DF2 basicblock addr : 140001E37 jnz 140001E37 -> 140001E5E jmp 140001E37 -> 140001C16 basicblock addr : 140001E59 140001E59 -> 140001B16 basicblock addr : 140001E75 140001E75 -> 140001990 basicblock addr : 140001F57 jnz 140001F57 -> 140001DB9 jmp 140001F57 -> 140001D55 basicblock addr : 140001F8F 140001F8F -> 140001DF2 ``` 得到之后只需要patch计算,改为跳转就可以了 最后记得修一下入口块,这套逻辑对基本块有效,入口块需要特殊处理  因为入口块到主分发块为短跳转,只有两个字节,而一般跳转需要5个字节,所以可以折中一下,nop掉主分发块,改为jmp指令 截图对比效果如下 原始程序:  去混淆后:  去除非常干净 ### 总结 在接触到一陌生ollvm时,可以通过观察:1.case的计算方式 ,2.分发块的比较方式获取去除方法。编写脚本时也同理 这篇博客将会持续更新,分享我见过的ollvm实例以及ollvm pass的编写,大家遇到有趣的ollvm程序也可以分享给我!(不过真的会有人看我的博客吗TxT......)
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#软件保护
收藏
・
19
点赞
・
17
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 05:38
zzzhangyu
感谢你分享这么好的资源!
2026-6-8 18:35
mb_zuqeschf
为你点赞!
2026-5-25 19:13
scz
感谢你的积极参与,期待更多精彩内容!
2026-5-12 16:24
qianfeng_mi
期待更多优质内容的分享,论坛有你更精彩!
2026-4-28 15:39
P1umH0
感谢你的贡献,论坛因你而更加精彩!
2026-4-22 19:07
逆天而行
谢谢你的细致分析,受益匪浅!
2026-4-20 19:49
xianyuuuan
期待更多优质内容的分享,论坛有你更精彩!
2026-4-19 20:34
fu11p7r
+10
感谢你分享这么好的资源!
2026-4-19 16:28
mb_wkoeetvs
感谢你分享这么好的资源!
2026-4-19 13:15
孤独的街
非常支持你的观点!
2026-4-17 18:51
wx_funcrever
感谢你的积极参与,期待更多精彩内容!
2026-4-17 16:07
马来
期待更多优质内容的分享,论坛有你更精彩!
2026-4-16 20:02
wx_Q道长
为你点赞!
2026-4-16 15:43
我的小拇指啊
你的分享对大家帮助很大,非常感谢!
2026-4-16 09:30
qqizai
感谢你的积极参与,期待更多精彩内容!
2026-4-16 09:29
Liv_T
+10
为你点赞!
2026-4-15 21:05
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
13
)
Imxz
雪 币:
112
活跃值:
(9405)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
750
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-4-16 09:14
0
微笑:)
雪 币:
984
活跃值:
(5762)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
22
粉丝
2
关注
私信
微笑:)
3
楼
好文
2026-4-17 09:01
0
buluo533
雪 币:
563
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
84
粉丝
1
关注
私信
buluo533
4
楼
求大佬博客地址学习
2026-4-17 09:14
0
MaYil
雪 币:
9394
活跃值:
(7952)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
476
粉丝
2
关注
私信
MaYil
5
楼
感谢分享
2026-4-17 11:11
0
海带
雪 币:
427
活跃值:
(2110)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
83
粉丝
11
关注
私信
海带
6
楼
火钳刘明
2026-4-17 13:09
0
PPTV
雪 币:
11423
活跃值:
(4532)
能力值:
( LV2,RANK:10 )
在线值:
发帖
4
回帖
214
粉丝
0
关注
私信
PPTV
7
楼
你的分享对大家帮助很大,非常感谢!
2026-4-17 17:41
0
mb_wckjnnha
雪 币:
1524
活跃值:
(874)
能力值:
( LV5,RANK:70 )
在线值:
发帖
6
回帖
14
粉丝
30
关注
私信
mb_wckjnnha
1
8
楼
buluo533
求大佬博客地址学习
博客地址:
6a9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6K9h3&6Y4L8r3g2Z5L8%4u0F1i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1k6Q4x3U0k6F1j5Y4y4H3i4K6y4n7i4@1f1$3i4K6R3@1i4K6W2r3i4@1f1^5i4@1t1H3i4@1p5J5i4@1f1#2i4@1p5@1i4@1p5%4i4@1f1#2i4@1q4q4i4@1t1$3i4@1f1#2i4K6V1$3i4K6W2o6i4@1f1$3i4@1q4o6i4@1p5J5i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1@1i4@1u0n7i4K6S2m8i4@1f1#2i4K6V1H3i4K6S2q4i4@1f1@1i4@1u0o6i4K6W2m8i4@1f1#2i4@1p5@1i4K6W2m8i4@1f1#2i4@1p5@1i4K6W2m8i4@1f1#2i4K6R3^5i4K6R3$3i4@1f1@1i4@1u0m8i4@1q4n7
2026-4-18 02:10
0
需要渗透高手
雪 币:
114
活跃值:
(280)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
42
粉丝
1
关注
私信
需要渗透高手
9
楼
给你点赞
2026-4-18 22:18
0
yuanyouran
雪 币:
296
活跃值:
(3035)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
181
粉丝
0
关注
私信
yuanyouran
10
楼
好文
2026-4-18 22:27
0
fu11p7r
雪 币:
3276
活跃值:
(6612)
能力值:
( LV7,RANK:116 )
在线值:
发帖
12
回帖
48
粉丝
119
关注
私信
fu11p7r
11
楼
强强强
2026-4-19 16:29
0
Thehepta
雪 币:
3596
活跃值:
(7395)
能力值:
( LV11,RANK:185 )
在线值:
发帖
23
回帖
235
粉丝
282
关注
私信
Thehepta
3
12
楼
1
2026-4-20 11:55
0
piaoyi01
雪 币:
327
活跃值:
(290)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
26
粉丝
0
关注
私信
piaoyi01
13
楼
厉害
2026-4-22 18:20
0
mb_btwdlppy
雪 币:
44
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
41
粉丝
0
关注
私信
mb_btwdlppy
14
楼
666
23小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_wckjnnha
1
6
发帖
14
回帖
70
RANK
关注
私信
他的文章
[原创] 实战去除安卓应用中的多种混淆
3937
[原创]LLVM Pass编写及去除 —— 间接跳转
1835
[原创]Android风险环境检测 —— 签名校验
58253
[原创]Android一二三代壳实现
51038
[原创]LLVM Pass编写及去除——虚假控制流
10446
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部