如果选用jmp esp作为定位shellcode的跳板,那么在函数返回后要根据缓冲区大小、所需shellcode长短等实际情况灵活地布置缓冲区。送入缓冲区的数据可以分为以下几种。
(1)填充物:可以是任何值,但是一般用NOP指令对应的0x90来填充缓冲区,并把shellcode布置于其后。这样即使不能准确地跳转到shellcode的开始,只要能跳进填充区,处理器最终也能顺序执行到shellcode,就是esp地址的下面是shellcode,而上面的地址是shellcode的短程jmp,在上面就全部是Nop填充。
(2)淹没返回地址的数据:可以是跳转指令的地址、shellcode起始地址,甚至是一个近似的shellcode地址。
(3)shellcode:可执行的机器代码。
在缓冲区中怎样摆放shellcode对exploit的成功至关重要。
shellcode比较少的话,比如shellcode只有几十个字节,我们干脆把它直接放在缓冲区里,所以shellcode位于函数返回地址之前。
我们也可以使用跳转指令jmp esp来定位shellcode,所以在溢出时我们多覆盖了一片内存空间,把shellcode恰好布置在函数返回地址之后。
把shellcode布置在函数返回地址之后的好处(不用担心自身被压栈数据破坏)。但是,超过函数返回地址以后将是前栈帧数据(栈的方向,内存高址),而一个实用的shellcode往往需要几百个字节,这样大范围地破坏前栈帧数据有可能引发一些其他问题。例如,若想在执行完shellcode后通过修复寄存器的值,让函数正常返回继续执行原程序,就不能随意破坏前栈帧的数据。
当缓冲区较大时,把shellcode布置在缓冲区内。这样做有以下好处。
(1)合理利用缓冲区,使攻击串的总长度减小:对于远程攻击,有时所有数据必须包含在个数据包中!
(2)对程序破坏小,比较稳定:溢出基本发生在当前栈帧内,不会大范围破坏前栈帧。
当然,即便是使用跳转指令来定位shellcode,我们也可以把缓冲区布置成类似shellcode起始地址这种组织方式,在返回地址之后再多淹没一点,并在那里布置一个仅仅几个字节的“shellcode header”,引导处理器跳转到位于缓冲区中那一大片真正的shellcode中去。(我的理解是,“shellcode header”就是一个短程jmp,刚好跳到shellcode的地址)
将shelcode布置在缓冲区中虽然有不少好处,但是也会产生问题。函数返回时,当前栈帧被弹出、这时缓冲区位于栈顶esp之上的内存区域。在弹出栈帧时只是改变了esp寄存器中的值,逻辑上,esp以上的内存空间的数据已经作度;物理上,这些数据并没有被销毁。如果shellkeode中没有压栈指令向栈中写入数据还没有太大影响:但如果使用push 指令在栈中暂存数据,压栈数据很可能会破坏到shelleode本身。
当缓冲区相对shellcode 较大时,把shellcode布置在缓冲区的“前端”(内存低址方向)。这时shellcode 离栈顶较远,几次压找可能只会破坏到一些填充值nop:但是,如果缓冲区已经被shellcode 占满,则shellcode离栈顶比较近,这时的情况就比较危险了。
为了使shellcode具有较强的通用性,我们通常会在shellcode一开始就大范围抬高栈顶(先用sub
esp,100h之类的指令把栈抬高)。把shellcode“藏”在栈内,从而达到保护自身安全的目的。
使用其他跳转指令
使用jmp esp做“跳板”的方法是最简单,也是最常用的定位shellcode的方法。在实际的漏洞利用过程中,应当注意观察漏洞函数返回时所有寄存器的值。往往除了esp之外,eax、ebx、esi等寄存器也会指向栈顶附近,故在选择跳转指令地址时也可以灵活一些,除了jmp esp之外,mov eax、esp和jmp eax等指令序列也可以完成进入栈区的功能。
常用跳转指令与机器码的对应关系
机器码 | 指令 | 机器码 | 指令 |
---|---|---|---|
FF E0 | jmp eax | FF D0 | call eax |
FF E1 | jmp ecx | FF D1 | call ecx |
FF E2 | jmp edx | FF D2 | call edx |
FF E3 | jmp ebx | FF D3 | call ebx |
FF E4 | jmp esp | FF D4 | call esp |
FF E5 | jmp ebp | FF D5 | call ebp |
FF E6 | jmp esi | FF D6 | call esi |
FF E7 | jmp edi | FF D7 | call edi |
不使用跳转指令
个别有苛刻的限制条件的漏洞不允许我们使用跳转指令精确定位shellcode,而使用shellcode的静态地址来覆盖又不够准确,这时我们可以做一个折中:如果能够淹没大片的内存区域,可以将shellcode布置在一大段nop之后。这时定位shellcode时,只要能跳进这一大片nop中,shellcode就可以最终得到执行。这种方法好像蒙着眼睛射击,如果靶子无比大,那么枪枪命中也不是没有可能。