当我们在gdb中break OFFSET的时候,此时的这个OFFSET是一个address.gdb首先会把这个地址的一字节的value单独自己记下来(为了之后替换回original),然后把这个字节的value设置成INT3(0xcc).假设原来在OFFSET的指令是0x8345fc01(addl $0x1,-0x8(*ebp))), gdb会把这个最后一个字节的0x01记下来,然后替换成0x8345fccc.这并不是一个这种的instruction.
OFFSET 01 #original
OFFSET+1 fc
OFFSET+2 45
OFFSET+3 83
最终替换成
OFFSET cc #breakpoint instered
OFFSET+1 fc
OFFSET+2 45
OFFSET+3 83
于是当我们继续进行我们这个debugged program,当遇到INT3的时候,debugger program就会trap进kernel,随后kernel就会通过信号来告知对应的gdb.gdb随后就会把这个OFFSET上的字节用他之前存储的那个original value来替换,并且把EIP的instruction potiner移动到OFFSET,并且重新开始执行OFFSET所在位置的指令。之所以要移动EIP,是因为当CPU执行了INT3之后,此时的EIP已经自动加了1个字节了。
OFFSET cc # after breakpoint
OFFSET+1 fc #
OFFSET+2 45
OFFSET+3 83
OFFSET 01 #
OFFSET+1 fc
OFFSET+2 45
OFFSET+3 83
这个时候我们可以看到此时的指令已经重新编程了0x8345fc01.
这里提一下,为什么IN3应该是1个字节。首先在x86环境下,1个字节是最小的指令长度。如果INT3比1个字节来的长,我们就会因为上面的操作覆写超过1个以上的指令,这就会产生问题。考虑如下的场景:(在Cortex-M CPUs里面,instructions要么是2个字节要么是4个字节,因此breakpoint instruction在这个CPU体系下最小长度取2个字节
OFFSET OFFSET+1
如果假设我们要在instruction 1的地方设置断点,那么整个OFFSET就会被我们改成
OFFSET
OFFSET+1 ......................>
如果此时一旦有指令想要跳到OFFSET+1的位置,这个位置注意并不是INT 3,而是INT 3的一部分,这就是undefined behavoir,如果我们的INT 3本身就是x86最短的1个字节的话就不会有这个问题。
有同学肯定会问,为什么是替换字节而不是插入字节?我就不能插入这个breakpoint然后把接下来的指令都往后移吗?如果你真要做这个过程是非常复杂的,因为他会扰乱所有指令的offset,并且让所有的j*(jump)指令会跳到错误的位置。
看下面的例子:
比如我们本来有指令为
OFFSET
OFFSET+1
当我们用插入的方式来设breakpoints
OFFSET INT3
OFFSET+1 OFFSET+2
假设我们想在instruction 1的地方加上breakpoints,如果我们沿用上面的假设改用插入,那么所有的指令往后移,那么当其他的code想要跳到OFFSET+1的时候,他本来想跳到instruction 2,但是在这里他就跳到了instruction 1,错误就产生了
PS:
- INT 3实际上会引发SIGTRAP信号
- 硬件级别的断点不需要修改binary本身,他是CPU特有的技能,需要你设置一个address给他,CPU会持续的用当前的PC寄存器跟当前的运行地址做比较
- Software Breakpoints这种改变指令的方式其实会改变整个binary的CRC,所以如果你想把你的软件变成anti-debugging,你一旦注意到你的CRC被修改,可能就表示此时你整在被debug中。同时需要注意,修改对应的opcode(替换成INT 3)只能发生在RAM里(Flash Memory不支持)