l1: call l3 l2: ;some code l3: mov al, 0c3h mov edi, offset l3 or ecx, -1 rep stosb
我知道C3h是RETN
而且我知道stobs
将al
的值作为opcode根据edi
的偏移量写入操作码,并且由于rep
而完成了ecx
次。
我也意识到stobs
和stosw
会运行,如果他们在intel架构上被预取作为它们的原始格式。
如果我们在debugging模式下运行程序,预取是不相关的,并且将运行l2标签(因为它是单步的),否则如果没有debugging器,它会在l1和l3之间进行乒乓。
当程序被调试(即单步)时,预取队列在每个步骤被刷新(当中断发生时)。 但是,正常执行时,不会发生rep stosb
。 即使有内存写入缓存区域,较旧的处理器也不会刷新它,以便支持除rep movs
和rep stosb
之外的已更改的自修改代码。 (IIRC最终被固定在i7处理器上。)
这就是为什么如果有一个调试器(单步)代码将正确执行,并且当rep stosb
被ret
取代l2
将被执行。 当没有调试器时, rep stosb
将继续运行,因为ecx
是最大可能的,它最终会写入某个地方而不会写入,并且会发生异常。
本文描述了这种反调试技术。
调试器在这里做的唯一事情就是添加时间延迟。 这可能是如何工作的关键。 英特尔(和我承担的AMD)手册明确指出,自修改代码不被测量“工作”,除非程序发信号通知CPU包含修改后的指令的缓存行已经改变。 这是为了使预取逻辑便宜; 芯片设计人员不希望有硬件不断测试指令缓存行的每个字节是否仍然有效。
所以我假设调试器会发生什么情况是l1调用l3,它会在rep stosb之后存储返回值,并且执行返回操作,因为调试器在单步执行中会产生很长的延迟,强制cachecline containsng l3在更改后被重新读取。
没有调试器,我猜测stosb执行后的指令(未显示)。 如果跳转到“无调试器”,那么跳转的成功将证明没有使用单步调试器。
如果我在应用程序中发现这个代码,我会拒绝运行它。