PlaidCTF 2014 :: paris (reverse 300)
И так тема второго занятия «Виртуальная машина» (очередные многабукав) =)
Задание звучало следующим образом «This binary was found on some of our Windows machines. It's got The Plague written all over it. What secrets are contained inside?».
Ну что ж поищем секрет.
В качестве жертвы выступает исполняемый файл Windows размером 9216 байт.
Приложение консольное и работает абсолютно по той же схеме, что и рассмотренное выше:
1) запрашивается ввод пароля
Code:
.text:00401006 push 0FFFFFFF5h ; nStdHandle
.text:00401008 call ds:GetStdHandle
.text:0040100E push 0 ; lpReserved
.text:00401010 push offset NumberOfCharsWritten ; lpNumberOfCharsWritten
.text:00401015 push 0Ah ; nNumberOfCharsToWrite
.text:00401017 push offset rqs ; "Password? "
.text:0040101C push eax ; hConsoleOutput
.text:0040101D call ds:WriteConsoleA
2) получается пользовательский ввод;
Code:
.text:00401023 push 0FFFFFFF6h ; nStdHandle
.text:00401025 call ds:GetStdHandle
.text:0040102B push 0 ; pInputControl
.text:0040102D push offset NumberOfCharsWritten ; lpNumberOfCharsRead
.text:00401032 push 0FFh ; nNumberOfCharsToRead
.text:00401037 push offset DATA_ ; lpBuffer
.text:0040103C push eax ; hConsoleInput
.text:0040103D call ds:ReadConsoleA
3) отдается на обработку
Code:
.text:00401056 call VM
4) проверяется результат обработки и чеканья
Code:
.text:0040106A cmp esi, 0DEADBEEFh
.text:00401070 jnz msg_wrong
Как видно обработка выполняется некой VM (Virtual Machine). Поставим на ручник и заглянем под капот этой машинке.
Code:
.text:00402066 VM proc near
.text:00402066 xor ecx, ecx
.text:00402068 NEXT:
.text:00402068 push offset ENTRY_EXCEPTION
.text:0040206D push large dword ptr fs:0
.text:00402074 mov large fs:0, esp
.text:0040207B mov eax, 0
.text:00402080 mov [eax], eax ; RAISE EXCEPTION
.text:00402082 sub edi, 1111h
.text:00402088 pop large dword ptr fs:0
.text:0040208F add esp, 4
.text:00402092 cmp b_Exit_FL, 1
.text:00402099 jz EXIT
.text:0040209F jmp short NEXT
.text:0040209F VM endp
…………………………
.text:00401C90 EXIT:
.text:00401C90 retn
Видим цикл, в котором ставится обработчик SEH, генерится исключение при попытке записи по нулевому адресу, уменьшение значения EDI, удаляется обработчик. Выход из этого цикла при установке некого флага b_Exit_FL. Значит, вся радость нас ждет внутри обработчика. Отметим первую особенность данной ВМ: основной цикл вынесен из самой ВМ.
С помощью (с) Matt Pietrek вспомним прототип обработчика
Code:
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);
Эта информация нам еще понадобится. Прежде чем мы посмотрим на обработчик, краткая характеристика ВМ:- тип ВМ: регистровая
- разрядность ВМ: 16
- собственный сегмент данных ВМ: есть
- собственный стек ВМ: есть
- число обработчиков ВМ: 20
А вот и он:
Code:
.text:00402376 EXCEPTION_HANDLER:
.text:00402376 movzx eax, INDEX
.text:0040237D add eax, offset HANDLER_SWITCH_TABLE
.text:00402382 mov eax, [eax]
.text:00402384 add INDEX, 4
.text:0040238B cmp INDEX, 80
.text:00402392 jl short GOGOGO
.text:00402394 mov INDEX, 0
.text:0040239B
.text:0040239B GOGOGO:
.text:0040239B jmp eax
Тем кто знаком с виртуальными машинами сразу в глаза бросится отсутствие выборки пикода и селектора обработчиков в привычном виде. Так вот второй особенностью данной реализации ВМ есть постселекция опкода, т.е. о том наш ли это хэндлер опкода мы будем знать только на выходе по результатам исполнения его тела. Поэтому представленный выше код точки входа ВМ методично нас отсылает во все хэндлеры присутствующие в таблице.
Code:
.text:0040220E HANDLER_SWITCH_TABLE dd offset HNDL_00_NOP
.text:00402212 dd offset HNDL_01_MOV_REG_REG
.text:00402216 dd offset HNDL_02_MOV_MEM16_REG
.text:0040221A dd offset HNDL_03_MOV_REG_MEM16
.text:0040221E dd offset HNDL_04_MOV_REG_IMM16
.text:00402222 dd offset HNDL_05_ADD
.text:00402226 dd offset HNDL_06_SUB
.text:0040222A dd offset HNDL_07_XOR
.text:0040222E dd offset HNDL_08_AND
.text:00402232 dd offset HNDL_09_ROR8
.text:00402236 dd offset HNDL_10_NOT
.text:0040223A dd offset HNDL_11_INC
.text:0040223E dd offset HNDL_12_CMP
.text:00402242 dd offset HNDL_13_JMP
.text:00402246 dd offset HNDL_14_CJMP
.text:0040224A dd offset HNDL_15_PUSH
.text:0040224E dd offset HNDL_16_POP
.text:00402252 dd offset HNDL_17_SWAP
.text:00402256 dd offset HNDL_18_XOR_BUF
.text:0040225A dd offset HNDL_19_EXIT
Таким образом, количество исполняемого, а следовательно и трассируемого кода увеличивается пропорционально количеству обработчиков. Как реализована постселекция посмотрим на примере самого короткого хэндлера (для команды NOP).
Code:
.text:0040247C HNDL_00_NOP:
.text:0040247C call GET_TYPE1_1_EBX
.text:00402481 sub eax, 0
.text:00402484 setz al
.text:00402487 jmp END_
Сразу остановлюсь на именовании функций- GET - выбрать (получить) команду
- TYPE1 – тип команды 1
- 1 - длина команды 1
- EBX - результат выборки и распарсивания полей команды в EBX
Кроме этого в ЕАХ возвращается опкод команды при попытке его распарсивания как TYPE1. Следующие две команды элемент постселекции – AL бедет 1 если мы действительно исполняли команду NOP. В остальных хэндлерах между выборкой команды и селекцией выполняются действия связанные с самой операцией. И они понятное дело вносят изменения в контекст ВМ: в регистры, в собственные данные и стек.
Поэтому в самом начале перед выборкой команды
Code:
.text:00401CF0 GET_TYPE1_1_EBX:
.text:00401CF0 call SAVE_VM_CONTEXT
у нас весь контекст ВМ сохраняется
Code:
.text:00401C91 SAVE_VM_CONTEXT:
.text:00401C91 mov esi, [esp+14h]
.text:00401C95 mov edi, offset saveArea_context
.text:00401C9A mov ecx, 34h
.text:00401C9F rep movsd
.text:00401CA1 mov esi, offset DATA_ ; text:00401490...401890
.text:00401CA6 mov edi, offset saveArea_data
.text:00401CAB mov ecx, 100h
.text:00401CB0 rep movsd
.text:00401CB2 mov esi, offset STACK_
.text:00401CB7 mov edi, offset saveArea_stack
.text:00401CBC mov ecx, 80h
.text:00401CC1 rep movsd
.text:00401CC3 mov al, b_ZFLAG
.text:00401CC8 mov save_b_ZFLAG, al
.text:00401CCD mov al, b_Exit_FL
.text:00401CD2 mov save_b_Exit_FL, al
.text:00401CD7 mov ax, w_EIP
.text:00401CDD mov save_w_EIP, ax
.text:00401CE3 mov ax, w_ESP
.text:00401CE9 mov save_w_ESP, ax
.text:00401CEF retn
а в случае если постселекция провалилась (AL не равен 1), то восстанавливается
Code:
.text:00401FCA END_:
.text:00401FCA mov edx, [esp+12]
.text:00401FCE cmp al, 1
.text:00401FD0 jz exit_from_VM
.text:00401FD6 mov esi, offset saveArea_context
.text:00401FDB mov edi, edx
.text:00401FDD mov ecx, 34h
.text:00401FE2 rep movsd
.text:00401FE4 mov esi, offset saveArea_data
.text:00401FE9 mov edi, offset DATA_
.text:00401FEE mov ecx, 100h
.text:00401FF3 rep movsd
.text:00401FF5 mov esi, offset saveArea_stack
.text:00401FFA mov edi, offset STACK_
.text:00401FFF mov ecx, 80h
.text:00402004 rep movsd
.text:00402006 mov al, save_b_ZFLAG
.text:0040200B mov b_ZFLAG, al
.text:00402010 mov al, save_b_Exit_FL
.text:00402015 mov b_Exit_FL, al
.text:0040201A mov ax, save_w_EIP
.text:00402020 mov w_EIP, ax
.text:00402026 mov ax, save_w_ESP
.text:0040202C mov w_ESP, ax
.text:00402032 jmp exit_from_VM
Что тоже увеличивает объем исполняемого кода, да и привязаться к содержимому контеста ВМ совсем непросто, т.к. только одно из 60 (3[сохранение-изменение-восстановление]*20 хэндлеров) действий чтения/записи будет нас интересовать.
Исходя из описанных трудностей с динамическим анализом работы ВМ и учитывая, что хэндлеров всего 20 было решено писать декомпилятор ленты PCODE.
Code:
#! /usr/bin/python
# -*- coding: utf-8 -*-
'''
####################################################################
#
# Plaid CTF 2014
# Task: paris (reverse 300)
#
# VM decompilator
#
'''
_TYPES1 = [0x00,0x09,0x15,0x02,0x07,0x18,0x1B,0x0A,0x14]
_TYPES2 = [0x201,0x202,0x203,0x98,0x99,0x9A,0x9B,0x3F]
_TYPES3 = [0x13,0x1F,0x1D]
# get 1-byte command
def PARAM_T1(eip):
b = ord(PCODE[eip])
type = (b >> 3) & 0xFFFF
param = (b & 7) << 1
return type, param
# get 2-byte command
def PARAM_T2(eip):
w = (ord(PCODE[eip]) << 8) | ord(PCODE[eip+1])
type = (w >> 6) & 0xFFFF
param1 = ((w >> 3) & 7) << 1
param2 = (w & 7) << 1
return type, param1, param2
# get 3-byte command
def PARAM_T3(eip):
d = (ord(PCODE[eip]) << 16) | (ord(PCODE[eip+2]) << 8) | ord(PCODE[eip+1])
type = (d >> 19) & 0xFFFF
param1 = ((d >> 16) & 7) << 1
param2 = (d & 0xFFFF)
return type, param1, param2
f = open('paris.exe', 'rb')
f.seek(0xF8E)
PCODE = f.read(108+2)
f.close()
# PCODE pointer
eip = 0
# mail loop
while 1:
# get info as 1-2-3 bytes command
type1,param1 = PARAM_T1(eip)
type2,param2_1,param2_2 = PARAM_T2(eip)
type3,param3_1,param3_2 = PARAM_T3(eip)
# command selector
if type1 in _TYPES1:
if type1 == 0x00:
print '%04X: %02X nop'%(eip,ord(PCODE[eip]))
elif type1 == 0x09:
print '%04X: %02X ror reg-%X,8'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x15:
print '%04X: %02X not reg-%X'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x02:
print '%04X: %02X inc reg-%X'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x07:
print '%04X: %02X push reg-%X'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x18:
print '%04X: %02X pop reg-%X'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x1B:
print '%04X: %02X swap reg-%X'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x0A:
print '%04X: %02X loop_xor buffer,key[reg-%X]'%(eip,ord(PCODE[eip]),param1)
elif type1 == 0x14:
print '%04X: %02X exit'%(eip,ord(PCODE[eip]))
#break
eip += 1
elif type2 in _TYPES2:
if type2 == 0x201:
print '%04X: %02X %02X mov reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x202:
print '%04X: %02X %02X mov [reg-%X],reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x203:
print '%04X: %02X %02X mov reg-%X,[reg-%X]'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x98:
print '%04X: %02X %02X add reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x99:
print '%04X: %02X %02X sub reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x9A:
print '%04X: %02X %02X xor reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x9B:
print '%04X: %02X %02X and reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
elif type2 == 0x3F:
print '%04X: %02X %02X cmp reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
eip += 2
elif type3 in _TYPES3:
if type3 == 0x13:
print '%04X: %02X %02X %02X mov reg-%X,0x%04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_1,param3_2)
elif type3 == 0x1F:
print '%04X: %02X %02X %02X jmp %04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_2)
elif type3 == 0x1D:
print '%04X: %02X %02X %02X cjmp %04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_2)
eip += 3
# end of PCODE
if eip >= 108:
break
Для возможности селекции команд в декомпиляторе также производится распарсивание команды для всех трех типов и их группировка.
Выхлоп декомпилятора (девиртуализатора) оказался коротким, всего 54 команды
Code:
0000: 00 nop
0001: 00 nop
0002: 00 nop
0003: 9A 33 31 mov reg-4,0x3133
0006: 9B 00 00 mov reg-6,0x0000
0009: 9C 00 FF mov reg-8,0xFF00
000C: 9D FF 00 mov reg-A,0x00FF
000F: 80 D8 mov reg-0,[reg-6]
0011: 80 47 mov reg-E,reg-0
0013: DF swap reg-E
0014: AF not reg-E
0015: 0F D7 cmp reg-E,reg-4
0017: EF 37 00 cjmp 0037
001A: 80 7E mov reg-C,reg-E
001C: 26 E6 and reg-C,reg-8
001E: 26 EF and reg-E,reg-A
0020: 4E ror reg-C,8
0021: 26 B7 xor reg-E,reg-C
0023: 9E 00 02 mov reg-C,0x0200
0026: 26 3F add reg-E,reg-E
0028: 26 3E add reg-C,reg-E
002A: 80 F7 mov reg-E,[reg-C]
002C: DF swap reg-E
002D: C6 pop reg-C
002E: 26 B7 xor reg-E,reg-C
0030: 3E push reg-C
0031: 3F push reg-E
0032: 53 loop_xor buffer,key[reg-6]
0033: 13 inc reg-6
0034: FF 0F 00 jmp 000F
0037: 26 BF xor reg-E,reg-E
0039: 9A 00 01 mov reg-4,0x0100
003C: 9E 21 AF mov reg-C,0xAF21
003F: 80 D5 mov reg-A,[reg-4]
0041: DD swap reg-A
0042: 12 inc reg-4
0043: 12 inc reg-4
0044: C3 pop reg-6
0045: 0F F5 cmp reg-A,reg-C
0047: EF 56 00 cjmp 0056
004A: 0F DD cmp reg-A,reg-6
004C: EF 3F 00 cjmp 003F
004F: 9B 00 00 mov reg-6,0x0000
0052: 9A 00 00 mov reg-4,0x0000
0055: A7 exit
0056: 9D 4D 5A mov reg-A,0x5A4D
0059: 0F EB cmp reg-6,reg-A
005B: EF 65 00 cjmp 0065
005E: 9B 00 00 mov reg-6,0x0000
0061: 9A 00 00 mov reg-4,0x0000
0064: A7 exit
0065: 9B AD DE mov reg-6,0xDEAD
0068: 9A EF BE mov reg-4,0xBEEF
006B: A7 exit
Три команды NOP в начале вероятно для того чтобы сразу почувствовать удовольствие от трассирования 60ти хэндлеров без изменений в контексте ВМ.
При просмотре кода можно увидеть желанную сигнатурку 0xDEADBEEF, которая, как мы помним, проверяется после выполнения ВМ и свидетельствует о том, что мы «Ай, молодца!».
Широко в циклах пользуется стек, причем в цикле обработке данные в стек укладываются, а в цикле чеканья выбираются. Тут нам понадобится информация о том, что стек растет от младших адресов к старшим (PUSH - w_ESP += 2 / POP - w_ESP -= 2). И в стеке на вершине уже находится значение 0x5A4D.
Команды работы с памятью ВМ (mov reg16,[reg16]) имеют доступ только к собственному сегменту данных. И при внимательном рассмотрении девиртуализированого кода можно увидеть три характерных адреса (0000h, 0100h, 0200h):
Code:
0006: 9B 00 00 mov reg-6,0x0000
000F: 80 D8 mov reg-0,[reg-6]
0023: 9E 00 02 mov reg-C,0x0200
002A: 80 F7 mov reg-E,[reg-C]
0039: 9A 00 01 mov reg-4,0x0100
003F: 80 D5 mov reg-A,[reg-4]
При этом команда
Code:
0032: 53 loop_xor buffer,key[reg-6]
тоже работает с собственным сегментом данных ВМ начиная с адреса 0200h. Т.е. на каждом цикле обработки ввода наш сегмент данных начиная с адреса 0200h из которого мы выбираем значения полностью перексоривается.
С началом сегмента данных ВМ мы уже встречались выше, именно туда мы получили пользовательский ввод после запроса пароля.
Таким образом, мы имеем цикл обработки пользовательского ввода, без контроля длины значения находящегося в буфере, с выходом из цикла при получении 0x3133. Обратить также стоит внимание на то, что ВМ у нас 16-разрядная и на обработку извлекается одновременно два символа пользовательского ввода, однако указатель в цикле сдвигается на единицу.
Следующий цикл сравнивает полученные значения с константным массивом. Цикл заканчивается, когда из константного массива будет извлечено значение 0xAF21, что и даст нам необходимую длину пароля. Последним извлеченным значением из стека ВМ должно быть значение 0x5A4D, т.е. стек после проверки должен быть пустым.
Вроде все ясно и можно потирая руки реализовать генерацию входного пароля. Вот на этой мысли вероятно и обломились все те кому решить этот таск не удалось.
«Почему же так?», спросит пытливый читатель. Дело в том, что мы не рассмотрели еще одну составляющую ВМ, а именно регистры, хотя машина у нас регистровая. Вот тут нам и понадобится вспомнить, что находимся мы в обработчике исключения, снова посмотреть на прототип обработчика и код хэндлера работающего с регистрами
Code:
.text:00402323 HNDL_01_MOV_REG_REG:
.text:00402323 call GET_TYPE2_2_EBX_ECX
.text:00402328 add ebx, 9Ch
.text:0040232E add ebx, [esp+0Ch]
.text:00402332 movzx ebx, word ptr [ebx]
.text:00402335 add ecx, 9Ch
.text:0040233B add ecx, [esp+0Ch]
.text:0040233F mov [ecx], bx
.text:00402342 sub eax, 201h
.text:00402347 setz al
.text:0040234A jmp END_
и задуматься о том, что же это за волшебная константа 9Ch.
Оказывается регистры ВМ проецируются на контекст прерванного потока (struct _CONTEXT *ContextRecord) и если посмотреть в эту структуру по смещению 9Ch, то увидим куда именно.
Code:
00000000 CONTEXT struc ; (sizeof=0x2CC, standard type)
00000000 ContextFlags dd ?
00000004 Dr0 dd ?
00000008 Dr1 dd ?
0000000C Dr2 dd ?
00000010 Dr3 dd ?
00000014 Dr6 dd ?
00000018 Dr7 dd ?
0000001C FloatSave FLOATING_SAVE_AREA ?
0000008C SegGs dd ?
00000090 SegFs dd ?
00000094 SegEs dd ?
00000098 SegDs dd ?
0000009C _Edi dd ?
000000A0 _Esi dd ?
000000A4 _Ebx dd ?
000000A8 _Edx dd ?
000000AC _Ecx dd ?
000000B0 _Eax dd ?
000000B4 _Ebp dd ?
000000B8 _Eip dd ?
000000BC SegCs dd ?
000000C0 EFlags dd ?
000000C4 _Esp dd ?
000000C8 SegSs dd ?
000000CC ExtendedRegisters db 512 dup(?)
000002CC CONTEXT ends
После осознания этого уже и команда
Code:
.text:00402082 sub edi, 1111h
не станет лишней.
И станет понятна связь между
Code:
0065: 9B AD DE mov reg-6,0xDEAD
0068: 9A EF BE mov reg-4,0xBEEF
и
Code:
.text:0040106A cmp esi, 0DEADBEEFh
Еще часть решавших, вероятно, отсеялась на том, что добавила в цикл обработки ввода вычитание 0х1111.
Для тех кто не понял почему это не верно, последнее замечание: хотя извлеченное из буфера значение будет у нас в регистре reg-0 (отображаемом на EDI) всего в течении одной команды
Code:
000F: 80 D8 mov reg-0,[reg-6]
0011: 80 47 mov reg-E,reg-0
цикл в начале обработчика прерывания прогонит нас по кругу от HNDL_03_MOV_REG_MEM16 до HNDL_01_MOV_REG_REG и каждый раз будет вычитаться значение 0х1111, т.е. в цикл обработки ввода нужно добавить вычитание значения 18*0х1111.
Вот теперь, пожалуй, точно все и можно писать восстановление пароля.
Code:
#! /usr/bin/python
# -*- coding: utf-8 -*-
'''
####################################################################
#
# Plaid CTF 2014
# Task: paris (reverse 300)
#
# Password generator
#
'''
import struct
def swap(r0):
return ((r0 & 0xFF) << 8) | (r0 & 0xFF00) >> 8
def modify(ii):
global values_
k = ord(xor_keys[ii])
k = (k << 8) | k
for j in range(256):
values_[j] ^= k
f = open('paris.exe', 'rb')
f.seek(0x790)
master = [0]*29
for ii in range(29):
master[28-ii] = struct.unpack('H', f.read(2))[0]
f.seek(0x890)
values_ = []
for ii in range(256):
values_.append(struct.unpack('H', f.read(2))[0])
f.seek(0xFFA)
xor_keys = f.read(32)
f.close()
_stack = [0]*30
_stack[0] = 0x5A4D
_out = 'V'
for i in range(len(master)-1):
_stack[i+1] = master[i]
idx = values_.index(master[i]^_stack[i])
print 'pos %02d: index 0x%02X'%(i,idx)
# find in range ' '..'z'
for c1 in range(0x20,0x7A):
z = (~swap((((ord(_out[-1])<<8)|c1)-18*0x1111)&0xFFFF))&0xFFFF
zl = 0xFF & z
zh = (0xFF00 & z) >> 8
_idx = (zh ^ zl) & 0xFF
if _idx == idx:
print 'index 0x%X with first char \'%c\' -- %c%c'%(_idx,_out[-1],_out[-1],chr(c1))
_out += chr(c1)
break
modify(i)
print '[+] FLAG:', _out
Отбрасывая протокол вывода, результат будет таким V1rTu4L_M4ch1n3s_4r3_Aw3s0m3!
Содержание архива paris.rar:- paris – сэмпл из задания
- paris.idb – база для IDA Pro v5.7
- paris_devm.py – декомпилятор (девиртуализатор)
- paris_gen.py – генерация флага
- VM_code – код под ВМ
- paris_gen.log – протокол генерации флага
А на сьогодні все малятка, любі хлопчики й дівчатка! Дід Панас