pwnable.kr-leg WP

首先看一下源码

#include <stdio.h>
#include <fcntl.h>
int key1(){
	asm("mov r3, pc\n");
}
int key2(){
	asm(
	"push	{r6}\n"
	"add	r6, pc, $1\n"
	"bx	r6\n"
	".code   16\n"
	"mov	r3, pc\n"
	"add	r3, $0x4\n"
	"push	{r3}\n"
	"pop	{pc}\n"
	".code	32\n"
	"pop	{r6}\n"
	);
}
int key3(){
	asm("mov r3, lr\n");
}
int main(){
	int key=0;
	printf("Daddy has very strong arm! : ");
	scanf("%d", &key);
	if( (key1()+key2()+key3()) == key ){
		printf("Congratz!\n");
		int fd = open("flag", O_RDONLY);
		char buf[100];
		int r = read(fd, buf, 100);
		write(0, buf, r);
	}
	else{
		printf("I have strong leg :P\n");
	}
	return 0;
}

这个main函数的逻辑比较清晰明了,输入一个key值,如果key1()+key2()+key3()) == key就能获得flag。但是key1、key2、key3的值都是用汇编代码写的,而且是ARM汇编(通过leg.asm文件发现里面寄存器都是表示成r0什么的) 。X86采用eax作为返回值,ARM使用r0作为返回值。
首先来看key1

(gdb) disass key1
Dump of assembler code for function key1:
   0x00008cd4 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cd8 <+4>:	add	r11, sp, #0
   0x00008cdc <+8>:	mov	r3, pc
   0x00008ce0 <+12>:	mov	r0, r3
   0x00008ce4 <+16>:	sub	sp, r11, #0
   0x00008ce8 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008cec <+24>:	bx	lr

和返回值有关的是下面这两句

   0x00008cdc <+8>:	mov	r3, pc
   0x00008ce0 <+12>:	mov	r0, r3   

由于ARM采用了流水线机制,当正确读取了PC的值时,该值为当前指令地址加8个字节,也就是key1 = 0x00008cdc + 0x8

在ARM中,程序计数器R15又被记作PC
ARM7系列处理器中每条指令分取指(IF)、译码(ID)、执行(EX)三个阶段,分别在不同的功能部件上依次独立完成。取指部件完成从存储器装载一条指令,通过译码部件产生下一周期数据路径需要的控制信号,完成寄存器的解码,再送到执行单元完成寄存器的读取、ALU运算及运算结果的写回,需要访问存储器的指令完成存储器的访问。
流水线上虽然一条指令仍需3个时钟周期来完成,但通过多个部件并行,使得处理器的吞吐率约为每个周期一条指令,提高了流式指令的处理速度

下面通过两个图来说明流水线机制,第一个图为非流水时空图,第二个图为流水时空图

非流水线时空图
流水线时空图

可以看到,在非流水机制中,6个机器时钟周期执行了2条指令。流水机制中6个时间周期执行了4条指令。PC总是指向“正在取指”的指令,从图中可以看到,执行指令1的时候,正在对指令3进行取址,所以,PC值 = 当前程序执行位置 + 8

再来看一下key2

(gdb) disass key2
Dump of assembler code for function key2:
   0x00008cf0 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cf4 <+4>:	add	r11, sp, #0
   0x00008cf8 <+8>:	push	{r6}		; (str r6, [sp, #-4]!)
   0x00008cfc <+12>:	add	r6, pc, #1
   0x00008d00 <+16>:	bx	r6
   0x00008d04 <+20>:	mov	r3, pc
   0x00008d06 <+22>:	adds	r3, #4
   0x00008d08 <+24>:	push	{r3}
   0x00008d0a <+26>:	pop	{pc}
   0x00008d0c <+28>:	pop	{r6}		; (ldr r6, [sp], #4)
   0x00008d10 <+32>:	mov	r0, r3
   0x00008d14 <+36>:	sub	sp, r11, #0
   0x00008d18 <+40>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d1c <+44>:	bx	lr
End of assembler dump.

BX 指令跳转到指令中指定的目标地址。寄存器中为跳转的目标地址,当寄存器的bit[0]0时,目标地址处的指令为ARM指令;当寄存器的bit[0]1时,目标地址处的指令为Thumb指令,每条指令2byte,所以PC值 = 当前程序执行位置 + 4
0x00008cfc <+12>: add r6, pc, #1 这一行执行r6 = 0x00008d04 + 0x1 = 0x00008d05
0x00008d00 <+16>: bx r6 从而bx r6就跳转thumb模式下。
0x00008d04 <+20>: mov r3, pc这一行执行r3 = 0x00008d04 + 0x4
0x00008d06 <+22>: adds r3, #4 这一行执行r3 = r3 + 0x4 = 0x00008d04 + 0x4 + 0x4 = 0x00008d0c
所以返回值r0(r0的值等于r3的值,0x00008d10 <+32>: mov r0, r3)
所以key2=0x00008d04 + 0x4 + 0x4
最后看key3

(gdb) disass key3
Dump of assembler code for function key3:
   0x00008d20 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008d24 <+4>:	add	r11, sp, #0
   0x00008d28 <+8>:	mov	r3, lr
   0x00008d2c <+12>:	mov	r0, r3
   0x00008d30 <+16>:	sub	sp, r11, #0
   0x00008d34 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d38 <+24>:	bx	lr
End of assembler dump.

0x00008d2c <+12>: mov r0, r3这一行将r3的值给r0,再往上找,0x00008d28 <+8>: mov r3, lr这一行将lr的值给r3。
寄存器R14又被称为连接寄存器(Link Register, LR),在ARM体系中具有下面两种特殊作用:
(1)存放当前子程序的返回地址。
(2)当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。
这里应该是子程序的返回地址,去main函数中找调用key3函数的地方

扫描二维码关注公众号,回复: 6087997 查看本文章
   0x00008d7c <+64>:	bl	0x8d20 <key3>
   0x00008d80 <+68>:	mov	r3, r0

在ARM体系中,B和BL指令为跳转指令,二者的不同之处在于,B指令仅仅执行跳转操作;BL指令同时还将下条指令的地址保存到LR寄存器中。所以key3的值为0x00008d80
最后可以得到

key1 = 0x00008cdc + 0x8
key2 = 0x00008d04 + 0x4 + 0x4
key3 = 0x00008d80
key = key1 + key2 + key3
key = 108400

这道题主要是考ARM汇编相关的知识,如果学过这方面的知识,那么这道题非常简单,我对这方面不是很熟,如果有什么不对的地方,还请指出来。

猜你喜欢

转载自blog.csdn.net/Casuall/article/details/89486653
今日推荐