[pwnable.kr] leg(2 pts) :: Write-Up

두비니

·

2020. 7. 6. 23:46

 

 

 

Daddy told me I should study arm.
But I prefer to study my leg!

Download : http://pwnable.kr/bin/leg.c
Download : http://pwnable.kr/bin/leg.asm

ssh leg@pwnable.kr -p2222 (pw:guest)

 

 

 

 

우선 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;
}

 

asm함수를 처음봤을 수도 있는데, 간단히 c코드 안에 어셈블리어로 코딩할 수 있게 해주는 함수입니다.(정확히는 '인라인 어셈블러'라고 합니다.)

더 궁금하시다면 다음 링크로: https://araikuma.tistory.com/600

 

[C언어] 고급 기능 - 어셈블리 언어 __asm

C 언어 코드에 어셈블리 언어를 통합하는 방법을 제공한다. 어셈블리 언어는 컴파일러와 시스템에 의존하기 때문에 이 자리에서는 Microoft Visual C ++ 및 Intel x86 호환 프로세서를 전제로 코드 내에 어셈블리..

araikuma.tistory.com

 

 

 

우선 이 문제를 풀기 위해서는 key1, 2, 3을 더한 값을 알아야하네요. 즉 어셈분석을 하면 되겠네요!

인텔어셈으로해도 고통스러운데 이건 arm어셈이네요.

 

arm어셈에 대한 글: https://blog.naver.com/onlyou_4ever/40006966899

 

[ARM강좌] 김효준님

Embedded 관련 공부를 시작하면서 일반적으로 가장먼저 접하게되는 core가 ARM 이죠. 그만큼 중요하구...

blog.naver.com

 

(gdb) disas 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.

 

어셈블리어를 보아하니 모두 함수의 리턴값이 r0에 저장되고, 마지막에는 r2에 key의 합이 들어가있는 듯 합니다.

 

그래서 key함수 각각의 어셈블리어/c언어를 살펴보면,

(gdb) disas 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.
int key1(){
	asm("mov r3, pc\n");
}

key1의 경우,

key1 = pc

pc값이 뭔지 파악해야합니다. 

여기서 pc는 program counter의 줄임말로, 인텔 어셈에서 eip와 비슷한 역할을 합니다.

다만 둘 사이의 차이점은 eip는 현재 실행중인 명령어의 주소를 담지만, pc는 자신이 실행중인 명령어의 다음 명령어의 주소를 받는다는 차이점이 있습니다.

그럼 key1 = 0x8ce0

 

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.
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"
	);
}

key2의 경우에는 <key2+32>에서 r0의 값에 r3을 옮기므로 r3의 값을 봐주면 될 것 같다. r3에 pc의 값을 옮기고, 4를 더한다. 즉

r3 = 0x8d06 + 0x4 = 0x8d0a, key2 = 0x8d0a 이다.

 

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.
int key3(){
	asm("mov r3, lr\n");
}

이번에는 r0가 아닌 lr값을 봐줘야 한다.

lr은 보통 bx명령어로 다른 주소로 점프했을 때, 그 주소에서 돌아왔을때 그다음으로 실행할 주소라고 한다.

따라서 현재 상황이 main함수에서 key3으로 넘어온 상황이므로 lr은 main문에서 확인할 수 있다.

(gdb) disas main
Dump of assembler code for function main:
	...
   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
	...
End of assembler dump.

main함수에서 0x08d7c에서 key3을 불러왔으니 lr에는 0x08d80이 저장되어있겠네요

key3 = 0x08d80

 

그럼 3개를 더한 값은 0x8ce0 + 0x8d0a + 0x08d80 = 0x1a76a = (dec)108394

/$ ./leg
Daddy has very strong arm! : 108394
I have strong leg :P
/$

응 아니야~

 

 

여기서 정말 한참을 헤맨 것 같다. 아래 글 보고 겨우 이해했다. 정말 설명 너무 잘돼있으니까 여기 보도록

http://recipes.egloos.com/4982170

 

일반적인 CPU의 동작 예 (Core)와 Pipe Line

일반적인 CPU의 동작 예를 들기 위하여, 조금 더 Simplified된 CPU모델을 소개하고자 합니다. CPU는 최소한의 동작을 위하여 그림과 같이 , CU, ALU 이외에도 Program Counter, IR, Address Register, Data Register, ACC �

recipes.egloos.com

위 글을 요약하면, cpu는 명령어 처리를 할 때,

fetch > decode > execute 순으로 진행이 된다고 한다.

 

출처: http://recipes.egloos.com/4982170

 

다음과 같이 진행이 되는데, 이 방법으로 진행이 될때는 하나의 과정이 진행될 때, 나머지 두 과정이 그냥 놀게 된다. 이걸 그냥 보고만 있을 수 없는 알뜰한 인간들은 pipeline이라는 것을 도입합니다.

pipeline이란 최대한 "놀고"있는 과정을 줄이기 위한 것인데, 1~5번까지의 instruction을 진행해야하는 프로그램에서 1번 instruction에 대해 decoding과정에 들어간 후에, 2번 fetch과정을 바로 시작하는 식으로 진행이 됩니다.

근데 역설적으로 너무 많이 돌리면 성능이 떨어진다고 하네요. 아무튼 오늘은 과정 자체만 이해하면 되는 것이니, 넘어가도록 합시다.

 

아무튼 이 과정이 중요한 이유는, 어떤 명령어가 실행(execute)가 되고 있다면, pc는 이미 다음 주소를 가르키고 있다는 것이다. 따라서 key1과 key2의 pc값을 다음 주소로 바꿔주면, 결국

 

key1 = 0x8ce4

key2 = 0x8d0c

key3 = 0x08d80

 

key = 0x8ce4 + 0x8d0c + 0x8d80 = 0x1a770 = (dec)108400

이 된다. (key3은 lr을 물어보는 값이였으니 상관없음)

 

 

어셈 싫어...