자바스크립트 엔진을 대략적으로 공부하고 싶어서 동기를 부여할 겸 조금 취약하다고 할 수 있는 Jerryscript를 타겟으로 겸사겸사 분석을 진행한적이 있다. 여차저차 처음으로 오픈소스 소프트웨어에서 취약점을 발견하게 되었고 공부도 많이 되었었다. 취약점을 찾긴 했지만 웹 브라우저에서 사용되는건 아니어서 크게 영향력이 있을것 같진 않았고 그래서 그냥 Jerryscript Git 페이지에 제보를 해줬다.
github.com/jerryscript-project/jerryscript/issues/2146
영어를 정말 못할 때 쓴 거라 영어 실력이 형편없다. 물론 지금도 잘하는 건 아니다.
전형적인 Type-Confusion 버그였는데 Mark and Sweep 알고리즘을 구현하는 과정에서 Type 핸들링을 제대로 해주지 않아 발생했던 취약점이다.
우선 Jerryscript의 프로퍼티를 다루는 방식에 대한 설명이 필요한데 Jerryscript는 프로퍼티들을 2개씩 pair로 ecma_property_pair_t를 이용해 관리한다. 즉 아래 코드에서 values[0], values[1] 에 각각 별개의 필드값을 가지는 식이다.
- ecma-globals.h
typedef struct
{
ecma_property_header_t header; /**< header of the property */
ecma_property_value_t values[ECMA_PROPERTY_PAIR_ITEM_COUNT]; /**< property value slots */
jmem_cpointer_t names_cp[ECMA_PROPERTY_PAIR_ITEM_COUNT]; /**< property name slots */
} ecma_property_pair_t;
이때 만약 Object의 프로퍼티가 array라면 ecma_property_pair_t가 아니라 ecma_property_hashmap_t 라는 전용 property 관리 오브젝트를 사용한다. ecma_property_pair_t와 ecma_property_hashmap_t는 똑같이 ecma_property_header_t를 사용하는 property 관리 오브젝트이다.
typedef struct
{
ecma_property_header_t header; /**< header of the property */
uint32_t max_property_count; /**< maximum property count (power of 2) */
uint32_t null_count; /**< number of NULLs in the map */
uint32_t unused_count; /**< number of unused entires in the map */
/*
* The hash is followed by max_property_count ecma_cpointer_t
* compressed pointers and (max_property_count + 7) / 8 bytes
* which stores a flag for each compressed pointer.
*
* If the compressed pointer is equal to ECMA_NULL_POINTER
* - flag is cleared if the entry is NULL
* - flag is set if the entry is deleted
*
* If the compressed pointer is not equal to ECMA_NULL_POINTER
* - flag is cleared if the first entry of a property pair is referenced
* - flag is set if the second entry of a property pair is referenced
*/
} ecma_property_hashmap_t;
Jerryscript에서는 array의 구현을 위해 hash map을 사용하는데 hash map 생성과정을 보면 헤더 구조체 ecma_property_header_t를 설정할 때 각 0,1 번째 types을
hashmap_p->header.types[0] = ECMA_PROPERTY_TYPE_HASHMAP;
hashmap_p->header.types[1] = ECMA_PROPERTY_TYPE_DELETED;
으로 설정해 이 오브적트가 ecma_property_pair_t 가 아니라 ecma_property_hashmap_t 오브젝트 임을 표시한다.
반면에 배열의 크기가 0x10000 ( LIT_STRING_HASH_LIMIT ) 보다 크면 types[1]에 shift_counter 값을 저장해 크기에 대한 shift 값을 저장한다.
hashmap_p->header.types[1] = shift_counter;
- ecma-property-hashmap.c
ecma_property_hashmap_create (ecma_object_t *object_p){
...
ecma_property_hashmap_t *hashmap_p = (ecma_property_hashmap_t *) jmem_heap_alloc_block_null_on_error (total_size);
...
hashmap_p->header.types[0] = ECMA_PROPERTY_TYPE_HASHMAP;
hashmap_p->header.types[1] = ECMA_PROPERTY_TYPE_DELETED;
...
uint8_t shift_counter = 0;
if (max_property_count <= LIT_STRING_HASH_LIMIT)
{
hashmap_p->header.types[1] = 0;
}
else
{
while (max_property_count > LIT_STRING_HASH_LIMIT)
{
shift_counter++;
max_property_count >>= 1;
}
}
hashmap_p->header.types[1] = shift_counter;
...
}
문제는 Mark and Sweep 과정에서 이에 대한 핸들링을 제대로 다루지 못해 발생한다. ecma_gc_mark_property 함수는 Mark and Sweep과정에서 Property들이 가비지 컬렉팅을 당하게 하지 않기 위해 Marking을 담당하는 함수이다.
즉 ecma_property_pair_t, ecma_property_hashmap_t 오브젝트를 담당하며 Marking을 한다.
아까 봤듯이 ecma_property_hashmap_t->header.types[1] 에는 shift_counter값이 들어갈 수 있지만 ecma_gc_mark_propety 함수의 코드에서는 이를 전혀 핸들링 하지 못하고 switch { case ECMA_PROPERTY_TYPE_NAMEDDATA: } 블록을 실행하게 되고 Type-Confusion이 발생하게 된다.
static void
ecma_gc_mark_property (ecma_property_pair_t *property_pair_p, /**< property pair */
uint32_t index) /**< property index */
{
uint8_t property = property_pair_p->header.types[index];
switch (ECMA_PROPERTY_GET_TYPE (property))
{
case ECMA_PROPERTY_TYPE_NAMEDDATA:
{
if (ECMA_PROPERTY_GET_NAME_TYPE (property) == ECMA_STRING_CONTAINER_MAGIC_STRING
&& property_pair_p->names_cp[index] >= LIT_NEED_MARK_MAGIC_STRING__COUNT)
{
break;
}
ecma_value_t value = property_pair_p->values[index].value;
if (ecma_is_value_object (value))
{
ecma_object_t *value_obj_p = ecma_get_object_from_value (value);
ecma_gc_set_object_visited (value_obj_p);
}
break;
}
...
}
}
자바스크립트 엔진에서 Type-Confusion 같은 경우는 굉장히 exploit하기 쉬운 편이어서 무난하게 exploit 할 수 있었다.
https://github.com/zbvs/opensource-research/tree/master/jerryscript/bug_1
I wanted to study Javascript Engine so I choose Jerryscript as my target because I wanted to motivate myself and it looks like vulnerable and looks like I'm able to find something. As a result I could find vulnerability for the first time in the opensource project and it was really beneficial study for me. I just reported it to Jerryscript Git page because I thought Jerryscript is not used by Web Browser so doesn't have any big effect.
github.com/jerryscript-project/jerryscript/issues/2146
It was a typical Type Confusion bug. They didn't handle the type of object appropriately in the Mark and Sweep code for Jerryscript.
To explain the bug, First I need to explain how Jerryscript handles Javascript property. Jerryscript makes pair for every 2 properties with ecma_property_pair_t object. For example, as we can see below, values[0] and values[1] would be each property field value.
- ecma-globals.h
typedef struct
{
ecma_property_header_t header; /**< header of the property */
ecma_property_value_t values[ECMA_PROPERTY_PAIR_ITEM_COUNT]; /**< property value slots */
jmem_cpointer_t names_cp[ECMA_PROPERTY_PAIR_ITEM_COUNT]; /**< property name slots */
} ecma_property_pair_t;
But if the property of a Object is an array then Object doesn't use ecma_property_pair_t for the array but use ecma_property_hashmap_t. ecma_property_pair_t and ecma_property_hashmap_t both are property managing object with header field of ecma_property_header_t type.
typedef struct
{
ecma_property_header_t header; /**< header of the property */
uint32_t max_property_count; /**< maximum property count (power of 2) */
uint32_t null_count; /**< number of NULLs in the map */
uint32_t unused_count; /**< number of unused entires in the map */
/*
* The hash is followed by max_property_count ecma_cpointer_t
* compressed pointers and (max_property_count + 7) / 8 bytes
* which stores a flag for each compressed pointer.
*
* If the compressed pointer is equal to ECMA_NULL_POINTER
* - flag is cleared if the entry is NULL
* - flag is set if the entry is deleted
*
* If the compressed pointer is not equal to ECMA_NULL_POINTER
* - flag is cleared if the first entry of a property pair is referenced
* - flag is set if the second entry of a property pair is referenced
*/
} ecma_property_hashmap_t;
Jerryscript use hash map to implement an array. When it creats hash map, it sets 0, 1 index types of header structure ecma_property_header_t as
hashmap_p->header.types[0] = ECMA_PROPERTY_TYPE_HASHMAP;
hashmap_p->header.types[1] = ECMA_PROPERTY_TYPE_DELETED;
to describe it's not ecma_property_pair_t but it is ecma_property_hashmap_t object.
On the other hand, if the size of array is bigger than 0x10000 ( LIT_STRING_HASH_LIMIT ) it sets shift_counter value at types[1] as a shift value for size.
hashmap_p->header.types[1] = shift_counter;
- ecma-property-hashmap.c
ecma_property_hashmap_create (ecma_object_t *object_p){
...
ecma_property_hashmap_t *hashmap_p = (ecma_property_hashmap_t *) jmem_heap_alloc_block_null_on_error (total_size);
...
hashmap_p->header.types[0] = ECMA_PROPERTY_TYPE_HASHMAP;
hashmap_p->header.types[1] = ECMA_PROPERTY_TYPE_DELETED;
...
uint8_t shift_counter = 0;
if (max_property_count <= LIT_STRING_HASH_LIMIT)
{
hashmap_p->header.types[1] = 0;
}
else
{
while (max_property_count > LIT_STRING_HASH_LIMIT)
{
shift_counter++;
max_property_count >>= 1;
}
}
hashmap_p->header.types[1] = shift_counter;
...
}
The problem was, Mark and Sweep didn't handle this special case appropriately. ecma_gc_mark_propety is a function that prevents garbage collecting an object of property field by marking. This function inspects ecma_property_pair_t, ecma_property_hashmap_t objects and marks them.
As we saw, there can be shift_counter value in ecma_property_hashmap_t->header.types[1] but ecma_gc_mark_propety function's code doesn't handle it at all and executes switch { case ECMA_PROPERTY_TYPE_NAMEDDATA: } block code which leads to Type-Confusion.
static void
ecma_gc_mark_property (ecma_property_pair_t *property_pair_p, /**< property pair */
uint32_t index) /**< property index */
{
uint8_t property = property_pair_p->header.types[index];
switch (ECMA_PROPERTY_GET_TYPE (property))
{
case ECMA_PROPERTY_TYPE_NAMEDDATA:
{
if (ECMA_PROPERTY_GET_NAME_TYPE (property) == ECMA_STRING_CONTAINER_MAGIC_STRING
&& property_pair_p->names_cp[index] >= LIT_NEED_MARK_MAGIC_STRING__COUNT)
{
break;
}
ecma_value_t value = property_pair_p->values[index].value;
if (ecma_is_value_object (value))
{
ecma_object_t *value_obj_p = ecma_get_object_from_value (value);
ecma_gc_set_object_visited (value_obj_p);
}
break;
}
...
}
}
Usually, Type-Confusion in Javascript Engine is easy to exploit and actually it was too.
https://github.com/zbvs/opensource-research/tree/master/jerryscript/bug_1
'hacking & security > system' 카테고리의 다른 글
Game and Messenger Hooking (0) | 2021.01.07 |
---|
댓글