Write up (Wargame)/Pwnable

[pwnable.kr] leg 풀이

그믐​ 2023. 9. 13. 03:10
반응형

 

analysis


문세 설명을 보니 ARM 문제이다.

 

asm 파일이랑 C언어 파일을 다운로드 하고 분석한다.

 

#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;
}

 

이건 C 언어 코드인데 어차피 asm 파일을 봐야할 것 같다.

여기서는 플래그 조건만을 확인하고자 한다. key1, key2, key3 함수가 있는데, 해당 함수들의 반환값을 모두 합한 게 우리가 입력하는 key면 플래그를 출력해준다.

 

어셈블리 파일은 gdb에서 disass 한 걸 그대로 붙여넣었다.

 

(gdb) disass main
Dump of assembler code for function main:
   0x00008d3c <+0>:	push	{r4, r11, lr}
   0x00008d40 <+4>:	add	r11, sp, #8
   0x00008d44 <+8>:	sub	sp, sp, #12
   0x00008d48 <+12>:	mov	r3, #0
   0x00008d4c <+16>:	str	r3, [r11, #-16]
   0x00008d50 <+20>:	ldr	r0, [pc, #104]	; 0x8dc0 <main+132>
   0x00008d54 <+24>:	bl	0xfb6c <printf>
   0x00008d58 <+28>:	sub	r3, r11, #16
   0x00008d5c <+32>:	ldr	r0, [pc, #96]	; 0x8dc4 <main+136>
   0x00008d60 <+36>:	mov	r1, r3
   0x00008d64 <+40>:	bl	0xfbd8 <__isoc99_scanf>
   0x00008d68 <+44>:	bl	0x8cd4 <key1>
   0x00008d6c <+48>:	mov	r4, r0
   0x00008d70 <+52>:	bl	0x8cf0 <key2>
   0x00008d74 <+56>:	mov	r3, r0
   0x00008d78 <+60>:	add	r4, r4, r3
   0x00008d7c <+64>:	bl	0x8d20 <key3>
   0x00008d80 <+68>:	mov	r3, r0
   0x00008d84 <+72>:	add	r2, r4, r3
   0x00008d88 <+76>:	ldr	r3, [r11, #-16]
   0x00008d8c <+80>:	cmp	r2, r3
   0x00008d90 <+84>:	bne	0x8da8 <main+108>
   0x00008d94 <+88>:	ldr	r0, [pc, #44]	; 0x8dc8 <main+140>
   0x00008d98 <+92>:	bl	0x1050c <puts>
   0x00008d9c <+96>:	ldr	r0, [pc, #40]	; 0x8dcc <main+144>
   0x00008da0 <+100>:	bl	0xf89c <system>
   0x00008da4 <+104>:	b	0x8db0 <main+116>
   0x00008da8 <+108>:	ldr	r0, [pc, #32]	; 0x8dd0 <main+148>
   0x00008dac <+112>:	bl	0x1050c <puts>
   0x00008db0 <+116>:	mov	r3, #0
   0x00008db4 <+120>:	mov	r0, r3
   0x00008db8 <+124>:	sub	sp, r11, #8
   0x00008dbc <+128>:	pop	{r4, r11, pc}
   0x00008dc0 <+132>:	andeq	r10, r6, r12, lsl #9
   0x00008dc4 <+136>:	andeq	r10, r6, r12, lsr #9
   0x00008dc8 <+140>:			; <UNDEFINED> instruction: 0x0006a4b0
   0x00008dcc <+144>:			; <UNDEFINED> instruction: 0x0006a4bc
   0x00008dd0 <+148>:	andeq	r10, r6, r4, asr #9
End of assembler dump.

main 함수이다. main 함수는 사실 C 언어에서 분석했기 때문에 그닥 볼게 없다. 이후 함수들을 위주로 분석하자.

 

그 전에 짚어갈 점이 있다.

ARM 아키텍처에서는 레지스터를 r 이후 숫자들로 사용한다. 

용도를 조금 정리해보자면,

 

 

r0 ~ r3 함수 인자, r0는 return value 
r4 ~ r12 변수용 레지스터, r11은 이전 arm 프레임 포인터
r13 stack pointer
r14 LR (link register), RET 역할
r15 pc(program counter), fetch될 명령어 주소

 

ARM에 공부해보다가 pc값이 바로 다음 명령어를 가리키는 amd 아키텍처와 다르게, arm에서는 다음다음 명령어를 가리킨다.

그 이유로는 명령어를 수행할 때 fetch(읽고) -> decode(해석하고) -> execute(실행하고) 과정을 거치는데, 명령어를 두 번 실행하면

fetch -> decode -> execute 다음 명령어를 fetch -> decode -> execute 해서 총 6번의 과정을 거쳐야 하는데

arm에서는 이를 합쳐서 4번의 과정을 수행하기 때문이다.

 

실행하는 명령어가 A B C D 순서라고 할 때,

A(fetch)

B(fetch) -> A(decode)

C(fetch) -> B(decode) -> A(execute)

D(fetch) -> C(decode) -> B(execute)

로 들어간다.

 

따라서 A가 실행되고, B가 실행되는데에 4번으로 줄어든다. 여기서 성능저하를 해결하기 위해 하드웨어적 방법을 사용했다고는 하는데 아직 그건 잘 모르겠고, 중요하게 봐야할 부분으로 A가 execute될 때, fetch할 명령어는 C이다.

 

따라서 pc가 다다음 명령어를 가리킨다.

 

Solution


다시 돌아가서 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
End of assembler dump.

arm에 익숙하지는 않지만 어느정도 해석은 가능하다. 어차피 r0가 어떤 값이 세팅되는 지를 볼 것이므로 mov r0 , r3부분을 보고 잘 따라가면 된다.

 

r0에 r3을 넣는데, r3에는 이전에 pc를 넣는다.

pc는 다다음 주소니까 0x08cdc + 0x8 = 0x08ce4가 r3에 들어가고 결국 r0도 0x08ce4이다.

key1 == 0x08ce4

key2는 스택을 사용하는 것 같지만 여기서도 r0만 보면 된다.

r0는 r3값이 들어간다.

r3에는 4가 더해지고 그 이전에 pc를 대입한다.

 

r3에 pc가 들어가면 다다음 명령어인 0x08d08이 들어가고 그 다음 + 4로 0x08d0c가 들어갈 것이다.

따라서 r0는 0x08d0c 이다.

 

key2 == 0x08d0c

 

 

key3에서는 r0에 r3가 들어가는데 r3에는 lr이 들어간다. lr은 위 레지스터 설명과 같이 RET역할을 한다.

어셈블리에서 branch 이전에 돌아갈 주소를 넣기 때문에 key3를 호출한 main 함수에서 봐야한다.

 

main 함수에서 branch  하게 되면 ret로 0x08d80을 넣고 갈 것이므로 key3는 0x08d80이 된다. 

 

 

따라서 전체 key1 + key2 + key3 = 0x08ce4 + 0x08d0c + 0x08d80인 0x1a770이다. 이걸 이제 10진수로 바꾸어서 넣어주자. 108400

 

풀었당.

반응형