tmxklab

[pwnable.kr] leg 본문

War Game/pwnable.kr

[pwnable.kr] leg

tmxk4221 2020. 11. 20. 17:37

1. 문제 확인

 

문제에는 leg.c와 leg.asm파일이 주어진다.

 

[ leg.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;
}

key변수에 값을 받고 key1,2,3()함수 값을 모두 더한 값이 key와 같으면 flag를 얻을 수 있을 것 같다. 그리고 key1,2,3()에는 Arm Assembly로 작성되어 있다.

 

 


2. 접근 방법

 

이제 leg.asm파일을 열어서 확인해 보자

 

[ main부분 ]

중점적으로 봐야할 부분은 key1,2,3()이 호출하고 반환된 값이 어디에 저장되는지 살펴봐야 한다.

 

위 빨간색 박스의 어셈 코드를 해석해보면

① r0(key1함수의 리턴 값이 저장된 곳)의 값을 r4로 옮김

② r0(key2함수의 리턴 값이 저장된 곳)의 값을 r3로 옮김

③ r4에 다시 r4와 r3값을 더한 값을 저장

④ r0(key3함수의 리턴 값이 저장된 곳)의 값을 r3로 옮김

⑤ r2에 r4(key1()+key2())와 r3(key3())을 더한 값을 저장

⑥ r2와 r3와 비교한다. (여기서 r3는 사용자의 입력 값이 저장되어 있음)

 

결론적으로 key1,2,3()의 리턴 값은 r0에 저장되어 있고 모두 더한 값을 r3에 저장한다. 그리고 입력 값이 저장된 r2와 비교한다.

 


3. 문제 풀이

 

그럼 이제 key1,2,3()에서 r0의 값이 어떻게 저장되는지 확인해보고 리턴 값을 알아내보자

(여기서부터 leg.c를 컴파일한 arm 바이너리를 qemu로 디버깅한 그림이다. 주소 값은 신경쓰지 말자)

이 때 입력 값은 1000을 주었다.

 

1) key1() 디버깅

r3에 pc의 값을 저장한다. 이 때, pc에 저장된 값은 0x105b4<key1+8>이다.

 

하지만 r3에 실제로 저장되는 값은 0x105b4가 아닌 0x105bc<key1+16>이다.

 

최종적으로 r0에 0x105bc<key1+16>가 저장된 것을 볼 수 있다.

(왜 mov r3, pc에서 0x105b4<key1+8>이 아닌 0x105bc<key1+16>가 저장되는 지는 마지막에 설명하겠다.)

 

key1()의 리턴 값 : <key1+16>

 

 

2) key2() 디버깅

r3에 pc의 값을 저장하게 된다.

 

하지만 key1()과 동일하게 실제로 r3에 저장되는 값은 0x105e4<key2+24>이다.

 

그리고 r3에 4를 더한 값을 저장한다.

 

마지막으로 r0에 r3의 값 0x105e8를 저장한다.

 

key2()의 리턴 값 : <key2+24> + 0x4

 

 

3) key3() 디버깅

마지막으로 key3()를 디버깅을 시작한다.

참고로 함수가 종료되고 main함수로 복귀할 주소(0x10670)로 lr에 저장된다.

 

위에서 말했듯이 lr에 0x10670이 저장된 것을 알 수 있다.

 

r3에 lr의 값을 저장한다.

 

마침내 r0에 r3의 값인 0x10670이 저장된 것을 확인할 수 있다.

 

key3의 리턴 값 : <main+68>

 

 

4) 최종

다시 main함수로 돌아와서 r2와 r3값을 비교한다.

 

r2 : <key1+16> + <key2+24> + 0x4 + <main+68> = 0x105bc + 0x105e8 + 0x10670 = 0x31214

r3 : 0x3e8(1000) # 사용자 입력 값

 

위에 결과 값은 로컬에서 돌린 환경이므로 leg.asm에서 대응 시켜보면

<key1+16> : 0x8ce4

<key2+24> + 0x4 : 0x8d0c

<main+68> : 0x8d80

 

0x8ce4 + 0x8d0c + 0x8d80 = 0x1a770(108,400)

 

즉, 입력 값에는 108,400을 넣으면 성공할 것이다.

 

실행결과)

 


4. 몰랐던 개념

 

key1(), key2()에서 봤듯이 <key1+8> : mov r3, pc를 할 때 r3에 저장되는 값이 <key1+8>값이 아니라 <key1+16>인 이유

 

ARM 아키텍쳐는 RISC(Reduced Instruction Set Computer)구조로써 파이프라인을 지원한다.

 

ARM Core는 메모리에 있는 코드를 읽어오고, 해석하고, 실행한다. 이를 Fetch-Decode-Execute로 총 3단계로 표현된다.

  • Fetch : 메모리에 있는 데이터를 ARM Core가 처리하기 위해 메모리에 접근
  • Decode : 메모리에 접근해 읽어온 데이터를 ARM Core가 이해하는 기계어로 번역
  • Execute : 번역된 기계어를 통해 실행

위에서 설명한 3단계가 각 단계별로 CPU Clock을 한 cycle을 소모한다.

그렇다면 3개의 어셈명령어가 수행한다면 총 3 * 3 = 9번의 cycle을 소모하게 된다.

여기서 각 단계별로 사용되는 자원이 다르므로 중첩해서 사용하는 방식을 사용하게 되는데 이것이 바로 파이프라인이다.

 

가장 기본적으로 위와 같이 3단계 파이프라인 동작을 하며 세부적으로 나누면 5단계, 8단계, 18단계로도 나뉜다.

(AMR7 : 3단계, AMR9 : 5단계, ARM11 : 8단계)

 

 

다시 문제에로 돌아오면 PC에 저장된 값이 현재 실행할 명령어의 주소가 아닌 다다음 실행할 명령어의 주소를 가져오게 된다. PC(Program Counter)는 실행할 프로그램을 읽어올 위치를 가르키는 용도로 즉, fetch할 주소를 담고 있다.

 

 

따라서, 위 그림을 참고하여 설명하자면 파이프라인의 제일 첫 번째 어셈명령어를 실행하기 위해 3단계를 거치고

마지막 Execute단계에서 <key1+8> : mov r3, pc 명령어를 실행할 때 다다음 명령어는 fetch단계이다.

결국 PC에는 다다음 명령어의 주소를 담고 있게 된다.

 

 

참고 자료)

'War Game > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] shellshock  (0) 2020.11.20
[pwnable.kr] mistake  (0) 2020.11.20
[pwnable.kr] input  (0) 2020.11.20
[pwnable.kr] random  (0) 2020.11.20
[pwnable.kr] passcode  (0) 2020.11.20
Comments