MoeCTF 2024 Reverse
逆向工程入门指北
已经给出源代码,加密逻辑为异或22,那再异或回来就拿到flag了。
1 | enflag = [123, 121, 115, 117, 98, 112, 109, 100, 37, 96, 37, 100, 101, 37, 73, 39, 101, 73, 119, 73, 122, 121, 120, 113, 73, 122, 121, 120, 113, 73, 97, 119, 111, 73, 98, 121, 73, 115, 110, 102, 122, 121, 100, 115, 107, 22] |
xor
拖到IDA里F5一下先。
程序比较输入逐字节异或0x24后的结果与0x1400022B8处开始的44字节是否相等。把这些字节异或0x24即可得到flag。
双击跳转到对应内存区域,右键Convert可以把它导出成各种数据类型。
1 | enflag = [0x49, 0x4B, 0x41, 0x47, 0x50, 0x42, 0x5F, 0x41, 0x1C, 0x16, 0x46, 0x10, 0x13, 0x1C, 0x40, 0x09, 0x42, 0x16, 0x46, 0x1C, 0x09, 0x10, 0x10, 0x42, 0x1D, 0x09, 0x46, 0x15, 0x14, 0x14, 0x09, 0x17, 0x16, 0x14, 0x41, 0x40, 0x40, 0x16, 0x14, 0x47, 0x12, 0x40, 0x14, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] |
upx
先拖到DIE里看看。正如标题所言,程序使用了upx压缩。
咱们先upx -d脱一下壳,然后拖到IDA里。
嘿!直接把flag告诉咱了。
moectf{ec5390dd-f8cf-4b02-bc29-3bb0c5604c29}
dynamic
标题提示需要动调,不过还是先拖到IDA看看。看到一堆回调,这里就不放了。
那我们先Shift+F12看看字符串。
咱跳过去看看引用。
估计这块就是真正的主函数了,再跳到sub_140011DC0这儿。
推测字节数组v5就是加密后的flag。根据输出提示这块应该是先用sub_14001129E解密了v5,然后又调用它进行加密。密钥v6还藏了个小彩蛋。
然后x64dbg在第二次加密前下个断点即可。
在“符号”这儿拿到程序的基址,到IDA里rebase一下(编辑->段->重新设置基址)。
然后在0x7FF695761EEB下断点,拿到flag了!
moectf{18d4c944-947c-4808-9536-c7d34d6b3827}
upx-revenge
还是先upx -d,发现报错,不然咋叫revenge呢!
估摸着是修改了特征码或者别的啥,拖到010editor看看。
upx区段名被修改了。直接把vmp0,vmp1改回UPX0,UPX1看看成不成。成了。
拖IDA里又直接告诉flag了。
moectf{554ea35c-a1bb-4d8f-a323-bd697564bf27}
TEA
拖到IDA看看。
程序先让咱输入16进制的x,y,z。估计是保存到v9,v10,v11里了。那么它们分别谁是谁呢?
看一下汇编。可以看到lea rdx, [rsp+48h+...]的片段。这实际上就是lea rdx, [rbp+...]。
Windows x64传参顺序为rcx,rdx,r8,r9,...,这意味着三次调用的第二个参数分别是指向v10,v11,v9的指针,接下来的v4和v5即xxxxxxxx和yyyyzzzz。
(当然完全可以动调或者靠猜啥的也行)
接下来就是一个对[v4,v5]的tea加密了,密钥是base64xorteaxtea。-0x61C88647和+0x9E3779B9效果是一样的。这是一个原封不动的tea。
假如这就是一个全新但可逆的由cfbb本人设计的算法,我们该怎么解密?
我们就从第32次循环开始倒推。这时$v_3=32\cdot delta$,$v_5$被加后的值只依赖于手头已有的$v_3$和$v_4$的末值,那么用$v_5$减去加上的值就得到了这一轮加密之前的$v_5$,类似地可以得到$v_4$。如此循环就能得到$v_4,v_5$的初值。这种加解密过程极为相似的分组密码结构被称为Feistel结构。那么我们可以编写如下解密算法:
1 |
|
xtea
拖到IDA老样子看字符串跳过去。
0x140022000处存着加密后的flag。仿照我们上一题TEA的流程,我把sub_14001119F改成解密函数,把这里整个流程倒过来执行一遍(目的和源也交换)不就解密好了?进去看看。
这其实就是xtea算法,不过这里$delta$换成了0xCCFFBBBB()。就算你完全不知道,解密流程就和TEA一样把加密过程倒过来就好了!
1 |
|
xxtea
拖到IDA,又是熟悉的配方。v9是加密后的flag。
点进去看是xxtea。但是这次解密算法也被包含在程序里了(if那儿)!我们只要把v10的值改成其相反数,再把这里sub_14001105F的第一个参数改成v9就能在v9即[rbp+8]拿到flag。patch一下或者动调都可以。
moectf{j9h8hg75nky6vhkslh5v5awibr4i}
d0tN3t
题目告诉咱是.NET逆向了,拖到dnSpy(或者ILSpy)。
逻辑清晰明了,直接写脚本:
1 | array = [...] |
rc4
老样子看字符串。
输入flag,调用sub_401360用RC4_1s_4w3s0m3对输入进行rc4加密,结果存放在v14所指内存中。sub_404748应该是strlen,sub_401B01估计是malloc,sub_404679应当是memcmp。最后比较v14和v16所指内存存放内容是否一致。
而rc4是一种自逆的对称加密算法,即对于明文$m$,密钥$k$,密文$c$,有$Enc_k(Enc_k(m))=m$,也就是$Enc_k(c)=m$,将参数中的明文地址v17替换成密文地址v16就能直接在v14即[rbp-88h]处所指的内容得到flag。
v16在[rbp-78h],那直接patch一下把上面这句换成mov rsi, [rbp-78h]即可(刚好4字节)。
然后pwndbg在0x401540处下断就可以看到flag了。
moectf{why_Rc4_haS_The_Rev32sabl3_pr0ceSS}
逆向工程进阶指北
加密就这句。
*(p + i) = (*(p + i) * 0xccffbbbb + 0xdeadc0de) ^ 0xdeadbeef + 0xd3906
注意+的优先级高于^。脱掉异或和加法,最后还要脱掉乘法——怎么脱?
注意这都是unsigned int的运算,即已知$b=xa\bmod 2^{32}$。求$x$只需要求$ba^{-1}\bmod 2^{32}$,而$\mathrm{0xccffbbbb}^{-1}\bmod 2^{32}=\mathrm{0x8d61d173}$,那么最后乘0x8d61d173就可以得到原数据。
1 |
|
moedaily
顶级好活之Excel逆向。
先看看各单元格做了什么。显然D/E14-D/E19把D11的内容按4字节分组。
可以猜测F/G14-F/G19是各4字节对应的无符号数,点开一看果真如此(以F14为例),它将每4个字符转换为对应的无符号数(小端)。
=CODE(MID(D14,1,1))+CODE(MID(D14,2,1))*256+CODE(MID(D14,3,1))*256*256+CODE(MID(D14,4,1))*256*256*256
再看一下D12,这里理应有判断D11内容是否正确的逻辑。可以看到它将H/I14-H/I19的内容与指定值比较来判断正确与否,那么H/I14-H/I19应该就是F/G14-F/G19加密后的结果。
=IF(LEN(D11)=48,IF(AND(AND(AND(AND(AND(AND(H14=1397140385,I14=2386659843),AND(H15=962571399,I15=3942687964)),AND(H16=3691974192,I16=863943258)),AND(H17=216887638,I17=3212824238)),AND(H18=3802077983,I18=1839161422)),AND(H19=1288683919,I19=3222915626)),"恭喜你,拿到了真的FLAG","FLAG输入错了,再试试"),"flag长度不对")
如果修改一下输入,会发现H/I14-H/I19的密文中仅有对应的两个单元格会被改变,这意味着加密方式应是某种块间无关的64位分组加密,我们只要分析任意一行两个单元格的加密逻辑即可。看看H14和I14:
=s3cr3t!E54和=s3cr3t!F54
在s3cr3t中定位到E54:
=BITAND(E53+BITXOR(BITLSHIFT(F53,4)+C38,BITXOR(F53+D54,BITRSHIFT(F53,5)+D38)),4294967295)
即E54 = (E53 + ((F53 << 4) + C38) ^ ((F53 + D54) ^ ((F53 << 5) + D38))) & 0xFFFFFFFF
看到上面这个应该就很眼熟了,这不就是TEA加密么?接着看F54:
=BITAND(F53+BITXOR(BITLSHIFT(E54,4)+E38,BITXOR(E54+D54,BITRSHIFT(E54,5)+F38)),4294967295)
即F54 = (F53 + ((E54 << 4) + E38) ^ ((E54 + D54) ^ ((E54 << 5) + F38))) & 0xFFFFFFFF
检查D54可以看到D54 = 114514 + D53。推测key[] = {C38, D38, E38, F38}且delta = 114514。
往上看32轮看到A38和B38,可以看到它们来自E18和F18,而上面是用同样的key和delta对A2和B2进行加密,而它们来自Sheet1的F14和G14。
由此可以推测进行了两次TEA加密,编写如下解密脚本:
1 | enc = [[1397140385, 2386659843], [962571399, 3942687964], [3691974192, 863943258], [216887638, 3212824238], [3802077983, 1839161422], [1288683919, 3222915626]] |
moejvav
java逆向,拖到jadx里康康。一开始把输入字符的ASCII值异或202再加32来加密。
然后是一段仿虚拟机的代码,从vmInsn数组中取指取操作数并执行相应操作。可以看到这个try-catch块实际上和switch insn差不多。需要注意insn取3, 4, 5时(即e4, e5, e6对应的catch块)操作可能是不可逆的,不过实际执行时没有用到这些“指令”。
从上边可以看到insn取0时会从array取出一个值存到store,取6时会比较store与操作数是否相等,从而此时的操作数应为输入第二次加密后的值。
那么我们可以顺序执行这些“指令”,每当遇到操作6时就把操作数存到store,并把上一个操作0至此的所有指令全部倒序执行一遍(当然操作2要替换成减),此后((store - 32) ^ 202) % 256就是flag中一个字符的ASCII值。从而可以编写如下脚本拿到flag:
1 | vmInsn = [...] |
sm4
用key对输入的48字节进行加密(sm4,不过不重要,因为程序包含了解密函数)。
但是需要注意Data_plain的长度只有48字节,这样key的第一个字节会被覆盖为0x00。
下面调用decode_fun又对encode_Result解密并将结果存放在decode_Result,可以直接把encode_Result替换成密文enc即[rbp-60h],这样就能拿到flag。
给它们都patch掉!顺便下面那个printf也改一下让它直接输出flag。
然后运行一下就有flag了:
moectf{Congratulations_you_are_an_SM4_master!!!}
ezMAZE
拖DIE,有upx。
脱掉upx扔IDA,一个迷宫。起点(2, 2),终点(75, 55)。
地图大小80*56,sub_140001190应该是用来判断下一个位置是否有效的。
sub_140001190的逻辑:地图上的的点是byte_140005000数组中的比特位。检查(x, y)对应比特位,为1不能走;为0能走并把该位标记为1之后不能再走。
然后把这个逻辑抄过来,再写个BFS跑出最短路径的走法,输入就能拿到flag(这里下标稍微改了下):
1 | import queue |
moectf{the_18446744024826406994_amazing_maze!!}
Just-Run-It
只要成功运行这四个程序就能拿到flag。可是事情也没有太简单。
0x0.exe能直接在win64运行,这没啥好说的。
在wsl试图运行0x1.elf,依赖有点问题。
不过还好只是加了个upx,脱壳之后可以拿到。
0x2.apk需要在Android 14上运行。
而我没有实体机,Android Studio也因为各种原因无法启动ARM架构的模拟器,反编译工具也无法反编译程序的关键部分——
然后我就找了个在线的模拟器凑合了:
0x3.riscv64.elf是一个RISC-V架构下的64位Linux程序,IDA无法反编译(字符串里还有”zaku~zaku~“的雌小鬼字样)。
那么要想拿到flag,最方便的方式就是在qemu创建一个RISC-V架构的64位虚拟机并运行这个程序(而不是阅读RISC-V汇编)。
这里有一个比较方便的教程。照着做就有如下一长串命令来安装并启动虚拟机:
1 | qemu-system-riscv64 -M virt -m 2048M -smp 4 -bios fw_jump.bin -kernel u-boot.bin -drive file=ubuntu-22.04.4-preinstalled-server-riscv64+unmatched.img,format=raw,if=virtio -drive file=riscv_disk.img,format=raw,if=virtio -netdev user,id=net0,hostfwd=tcp::2222-:22 -device virtio-net-device,netdev=net0 -nographic -device virtio-rng-pci |
上面的命令中hostfwd参数将宿主机2222端口转发到客户机22即ssh端口,我们能通过scp将0x3.riscv64.elf传输给客户机:
1 | scp -P 2222 0x3.riscv64.elf ubuntu@localhost:~/ |
然后就能拿到最后一块flag碎片了。
显然前面三块碎片应该是ASCII字符串的hex,第四块是ASCII值,拼起来拿到原字符串。
bW9lY3RmezU5ZmE2MDJjLTYyNGEtNDBiNy04YTVjLWUzNWU1NzRjZjliOX0=
一眼base64,解码拿到flag。
moectf{59fa602c-624a-40b7-8a5c-e35e574cf9b9}
SecretModule
模块刷入时会执行customize.sh,我们直接阅读这个脚本。
1 |
|
脚本自解压并用eval执行,被执行的代码为:
1 | testk() { |
脚本监听音量上下键按键事件,按上给字符串$concatenated拼接114514,按下拼接1919810,重复7次并检查$concatenated的MD5值是否为$sec,最终flag为moectf{$concatenated},从而可以写如下脚本爆破:
1 | from Crypto.Hash import MD5 |
Cython-Strike: Bomb Defusion
把.pyd放DIE里看看,一个开了Themida保护的DLL库。
拖到IDA,发现.data段没有被保护:
装好模块之后用dir看一下模块提供的类和方法。
尝试使用一下这些方法。
之后使用read_memory方法发现读出来的内容就是之前IDA里看到的那段。
...#de.i.e MAX_... 0xffffff...unsign.d int ma..;...int p.ant_b..b(unsigned int input){if .input <= MAX_...) {ma.. = input;r..urn 0;} e.se.{...ret..n -1;} }...void expl.de_b.m...oid).{...void def..e_..mb(v.id){...}...check_p..d(uns.gned.int in.u.).{if.(input.> MAX_...).{...explode_b..b();...}...if.((in..t ^ ma.. == 114) && (input << (ma.. % 5) + 1 == 60578736)).{defuse_b..b();r..urn..}...explode_b..b();...retu..;...}............................................................................................
这应该差不多是enter_pwd里的逻辑,b.enter_pwd(60578736 >> (b.mask % 5) + 1)就能拿到flag了。至于in..t ^ ma.. == 114这个判断,应该是有另外一个名字差不多的变量蒙混过关的。
moectf{CoUnter_TerR0rists_w1n}
SMCProMax
上来看到这个sub_401000。首先调用VirtualProtect修改相应页面内存保护选项为读写执行,然后给0x40105E到0x401427的所有字节全部异或0x90,还原代码并执行。
那直接用IDA给它patch回去。
1 | from idc import * |
往后看一看可以发现这里才应该是sub_401000结束的地方。
再修改一下函数结束地址和函数类型让IDA正常反编译。
然后就能正常阅读了。虽然不知道为什么,我设置了合适的参数,但它还是buffer给标成retaddr[1]了(让我强迫症看着有点不爽)。
开头先给buffer[23]异或0x12,然后对buffer中40字节每4字节以相同逻辑加密,并与给定密文比较。
这个if-else看起来似乎有些难搞,但稍微仔细一点会发现根据v25 & 1就能判断它前一轮加密进入了哪个分支。
上轮进入else分支时必有v25 < 0即v25 & (1 << 31) != 0,而乘2即左移一位时会抹掉最高位,因此还原时需要恢复;若是上轮进入if分支,则直接右移一位即可。那么我们就能编写如下解密程序:
1 |
|
xor(大嘘)
扔进来一看似乎人畜无害,简单的加密和比较。
看看sub_401100,似乎也很简单,就异或一下嘛。
不过拿到了假flag:
This_1s_a_f4k3_flag_plzTry_ag4in
怎么回事呢,来直接看看汇编。
call $+5其实就是push 0x401159(也不一定,基址可能有变化,总之是下一条指令的地址)并执行下一条指令。
然后add [esp], 6让刚才push的地址刚好跳过add和retn占用的这6字节。
从而最后retn会让eip指向retn的下一条指令。
而IDA遇到retn就会认定这是一个函数结束的位置,反编译就会被这种junk code干扰。直接把它们都nop掉,再修改一下函数结束地址就好了。
可以看到除了开头那轮异或还进行了一次TEA和一轮异或。那么可以编写如下程序解密:
1 |
|
babe-z3
加了upx,脱掉扔IDA,可以看到输入按8字节分组放在s,v11,v12,v13中。
然后判断一系列条件是否成立,注意最后一个if表明第一个条件需要不成立。
然后把前七个约束添加到z3里就能求解了。这里s,v11,v12,v13都是64位位向量。
1 | from z3 import * |
moeprotector
静态调试
脱掉upx,扔到IDA,看起来没什么有用的代码。
但是在汇编里可以看到,这段代码被包裹在一个__try块中(这只是非常粗浅的理解),可以发现__except包裹的语句才是真正需要执行的部分。这段代码在输入0时抛出除0异常进而执行__except部分。
__except里也套了__try...__except。
__try中这一部分检查输入长度是否为0x39,不是就打印nonono, length is wrong...。不论是不是都抛出异常跳到loc_4017FD。
而loc_4017FD中调用了sub_4010E0,这个函数我会在之后分析。
让我们看看loc_401830做了什么。
Win32下FS段寄存器(Win64下是GS)指向TEB (Thread Environment Block) 结构体,它储存关于当前线程的各种信息。
该结构体0x30字节偏移处是线程所在进程的PEB (Process Environment Block) 结构体所在地址,这个结构体储存关于当前进程的各种信息。
而PEB的2字节偏移处是BeingDebugged,调试器附加时该项非0,因此可以测试它来进行基础的反调试。
程序继续检测调试器,这次把ProcessParameters拿出来用了。它是PEB的0x10字节偏移处一个_RTL_USER_PROCESS_PARAMETERS 结构体的地址。这个结构体储存进程创建相关的参数。
然后检测该结构体8字节偏移处的Flags的第14位是否为1。
我找遍互联网也没找到这是干什么用的,但是查到ScyllaHide反反调试时有把该位置1的操作。我猜测该位记录进程是否使用默认的PEB(GPT这样说,不过听起来有一定合理性)。
这次用到了PEB的0x18字节偏移处的ProcessHeap。它是Windows进程(默认的)堆,堆头有一些信息可以用来反调试。
这里测试堆0x0C字节偏移处的Flags是否为HEAP_GROWABLE(2)来反调试,它是正常情况下的值。
然后就遇到了一堆向量化指令,初看一脸懵逼。
但是由于输入长度刚好多1字节,对未被编译器优化的最后1字节的处理给了我们充分的提示。
上面这段汇编基本上在做的是byte_40543C[0x38] = (byte_40543C[0x38] ^ (0x38 + 0x15)) + 0x14。
有理由猜想上面的向量化指令被优化前就是byte_40543C[i] = (byte_40543C[i] ^ (i + 0x15)) + 0x14。粗略阅读一下上面的汇编会发现的确如此(不过要直接理解真有点麻烦的)。
后面还有两段几乎一样的代码,只不过0x15被换成了0x1A和0x19。至于中间没有操作byte_40543C的部分直接忽略就可以了。
最后程序比较byte_403658与byte_40543C处的0x39字节内容是否相同。
拿这个密文解密3次就能拿到flag了,解密程序如下:
1 |
|
sub_4010E0,以及反反调试
它到底做了什么?其实动调比较容易看出来,不过我尝试静调弄懂原理。
fs:30h处是PEB的地址,_PEB + 0Ch是一个_PEB_LDR_DATA结构体的地址Ldr,该结构体主要按不同顺序维护三个双向链表,包含进程所加载的模块的信息。
_PEB_LDR_DATA + 0Ch是按模块加载顺序组织的链表表头InLoadOrderModuleList,链表每一个节点都是一个_LDR_MODULE结构体,储存一个模块的信息并包含指向前后节点的指针。
这个链表前两个表项分别与程序自身和NTDLL.DLL相关,第三个与KERNEL32.DLL相关(这里取的就是第三个)。
而_LDR_MODULE + 18h是模块基址BaseAddress(前0x18字节是3个链表的前后节点的指针),所以这里就是取了KERNEL32.DLL的基址存放到[ebp-20h]。
注意mov esi, ecx,在调用sub_4010E0前有一句mov ecx, offset unk_4053B8。esi在之后被多次使用并用被来填充0x4053B8开始的这段内存。
然后把基址也存到edx和[ebp-8h]。分析这之后的部分就需要对PE文件结构有一些了解了。
PE文件以DOS头即_IMAGE_DOS_HEADER结构体开头,_IMAGE_DOS_HEADER + 3Ch是e_lfanew,它是NT头(一个_IMAGE_NT_HEADERS结构体)的地址(装载后只是偏移)。DOS头和NT头之间有DOS Stub,它是程序在DOS系统上所执行的内容。
_IMAGE_NT_HEADERS + 78h指向可选NT头(一个_IMAGE_OPTIONAL_HEADER结构体)的DataDirectory数组的第一项。
DataDirectory是一个长度为16的_IMAGE_DATA_DIRECTORY结构体数组,这个结构体存放PE文件各种表的偏移和大小。
它的第一项就是导出表(一个_IMAGE_EXPORT_DIRECTORY结构体)的偏移和大小。导出表存放着PE文件导出的函数名称和偏移等信息。
接着把NumberOfFunctions,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals加载到栈上。
然后就是一堆反编译出来比较奇怪的代码,但还是能看出来大概什么意思。(就不放图了)
总之就是在AddressOfNames按名查找GetProcAddress的位置,然后在AddressOfNameOrdinals根据这个位置查到函数地址在AddressOfFunctions中的位置,最后从AddressOfFunctions拿到GetProcAddress的地址并把它存放在0x4053B8 + 0Ch。
之后调用GetProcAddress(kernel32.dll基址, "GetModuleHandleW")并把地址存放在0x4053B8 + 5Ch。
最后反复调用GetModuleHandleW和GetProcAddress,将KERNEL32.DLL,KERNELBASE.DLL,NTDLL.DLL,USER32.DLL中一些函数的地址存放在0x4053B8(这里只是静态)开始的内存区域,供以后调用。(过程这就不放了,F5能看个大概)
如果中间出现问题,sub_4010E0返回0,程序最后打印nonono。
调试器附加时,程序异常会先抛给调试器,Shift+F9(或者直接把异常处理者设置成被调试者)可以把异常扔回给SEH处理。
第一次加密前的反调试措施在静态分析部分已描述过,只要手动修改相应的值(或者ScyllaHide把PEB勾上)就能绕过。
在第一次加密后,程序利用之前获得的地址先后调用了CreateMutexW,SetHandleInformation,NtClose,CloseHandle。
前两个没啥影响,但调用NtClose和CloseHandle时,如果附加了调试器,释放无效句柄(这里分别是0xDEADC0DE和esi中的垃圾值)会抛出EXCEPTION_INVALID_HANDLE异常;如果未附加,它们只是返回FALSE。ScyllaHide勾选NtClose就能给它hook掉,或者IDA静态patch掉也ok。
第二次加密后,程序先后调用了NtQueryInformationProcess,CreateFileW。
程序调用NtQueryInformationProcess(-1, 7, ebp-48h, 4, 0)。-1指代当前进程的句柄,传入ProcessDebugPort(7)使函数在ebp-48h写入进程调试器的端口号,-1说明被调试,第四个参数是写入字节数,第五个是成功写入字节数存放的地址。想要绕过它你可以:
- 修改
[ebp-48h]为0。 - 静态patch。
- ScyllaHide勾选NtQueryInformationProcess。
然后调用CreateFileW("\\\\.\\TitanHide", 0xC0000000, 0, 0, 3, 0, 0)以独占读写方式创建已经存在的设备\\.\TitanHide的一个句柄。
通常程序利用创建调试器未关闭的设备 / 文件句柄时,函数返回-1来反调试。但是这个\\.\TitanHide本来就不存在,因此正常情况下才会返回-1。
第三次加密后调用NtTerminateProcess(0, 0),第一个参数为进程句柄,为0即未指定会结束调用进程以外的所有进程,这对调试好像并没有什么影响。
进入sub_4015C0就是弹窗了,不是很重要就不分析了。
最后推荐一个总结了一些反调试技巧的网站。