← Blog

Do APK ao primeiro frame: por dentro do protocolo Mibo

Como seguimos o Mibo Smart do login à mídia, separamos evidência de hipótese e reproduzimos uma conexão independente até o primeiro frame.

Caminho ilustrado da pesquisa, da análise do APK ao primeiro frame de vídeo.

Uma tela de vídeo parece uma operação só: tocar numa câmera e esperar a imagem. Por trás daquele toque há pelo menos quatro conversas diferentes. A conta conversa com um serviço HTTPS; o cliente descobre um dispositivo e negocia um caminho até ele; um transporte com sequência e ACKs opera sobre UDP; por fim, um fluxo de mídia encapsulado é verificado, decifrado e entregue aos decodificadores.

A motivação veio de casa. A namorada do nosso fundador, Jojo, nos pediu um aplicativo nativo para acompanhar, no computador, os gatinhos de que cuidamos. Já são mais de 20. Queremos abrir suas câmeras numa experiência pensada para Linux, macOS e Windows; desse pedido nasceu o Noctua.

Antes de desmontar o protocolo, levantamos as opções oficiais: acesso local por protocolos documentados, um cliente desktop e uma API empresarial de streaming. Cada uma tem condições diferentes. Adiante comparamos essas alternativas com a rota remota que efetivamente testamos, sem confundir respostas históricas do suporte com a oferta atual.

Esta é a primeira parte de uma série sobre a pesquisa que sustenta o Noctua, nosso futuro cliente de câmeras. A pergunta inicial era concreta: conseguimos ir de uma identidade própria a um primeiro frame sem executar o Mibo Smart nem seu SDK proprietário? Para a câmera iM3B-C autorizada no laboratório, a resposta observada foi sim. Isso não significa compatibilidade geral, um player de produção ou uma falha de segurança do fornecedor.

O caminho até essa resposta foi menos linear que o diagrama final sugere. Uma lista de símbolos pode parecer uma especificação; um socket aberto pode parecer um fluxo de vídeo; um PLAY 200 pode parecer uma imagem pronta. Nenhuma dessas equivalências é segura. Nosso trabalho foi encontrar a fronteira seguinte em cada caso e exigir que os bytes atravessassem essa fronteira de verdade.

Como classificamos uma descoberta#

Fixamos a análise no APK instalado do Mibo Smart 3.1.1, no Android 16 do AVD e em uma câmera própria autorizada. Registramos hashes de APK, DEX e bibliotecas ARM64 antes de atribuir funções a offsets nativos. A pesquisa ficou restrita ao ambiente e à conta permitidos. Os nomes, endereços, credenciais e pacotes privados ficaram fora deste texto.

Usamos quatro degraus de evidência:

  1. Código instalado: JADX, DEX e disassembly mostram um caminho possível, não que ele tenha sido executado.
  2. Paridade sintética: uma entrada inventada produz a mesma saída numa rotina extraída e numa implementação padrão. Isso fixa bytes e regras, mas não aceitação pelo servidor.
  3. Execução oficial: hooks de metadados, estados do player e capturas de rede ligam a operação à sessão real do app.
  4. Execução independente: um processo sem Mibo/SDK monta requisições e sockets novos, recebe a resposta do peer e decodifica mídia com primitivas padrão.
Quatro degraus da evidência, do APK ao cliente independente
01 / Escada de evidênciasAbrir o diagrama da evidência em tamanho real ↗

Chamamos cada degrau pelo que ele prova. Por exemplo, o nome ProxySession no binário não prova um proxy remoto; onPlayBegan não prova que um pixel chegou à tela; um 200 HTTP não prova que os próximos pacotes são vídeo. As referências de evidência que acompanham as conclusões apontam para entradas do relatório de pesquisa.1

Primeiro, encontrando o caminho no aplicativo#

O app reúne serviços de conta, modelos de dispositivo, um cliente HTTPS e bibliotecas nativas de rede e mídia. A sequência de inicialização prepara configuração OEM, roteamento de serviços, identidade do terminal e subsistemas de reprodução. Um grafo de chamadas estático indicou o caminho GetToken → Login → inventário → detalhes da câmera → player. Mas a execução de fases assíncronas impediu tratar a ordem de uma lista de inicializadores como uma linha temporal garantida.

No lado da mídia, seguimos a passagem do objeto de câmera para os parâmetros nativos: produto/dispositivo, conta e senha da câmera, token P2P e parâmetros de reprodução têm papéis distintos. Um resultado importante foi justamente não chamar tudo isso de “token”. O token da conta assina leituras HTTPS; a credencial da câmera participa da sinalização e da chave de mídia; o randsalt recebido no Info é outro valor, gerado para a negociação. Confundir essas fronteiras gerava assinaturas plausíveis na aparência e rejeitadas no serviço.

Houve obstáculos instrumentais. Algumas combinações de Frida e Android 16 derrubavam o processo; o áudio local do app e o decodificador por hardware também interferiam em observações. Isolamos cada falha, usamos workarounds temporários apenas no processo autorizado e repetimos as medições com hooks menores. Um crash do observador nunca foi registrado como rejeição do protocolo. Para verificar bytes, reconstruímos tráfego e usamos implementações independentes de hashes, AES e codecs. A versão instalada e os hashes permaneceram a referência para offsets e call graphs.

O caminho nativo: do writev ao socket de mídia#

A primeira tentativa de observar o envio procurava send, sendto e sendmsg. Não encontrou o datagrama esperado. Isso era uma lacuna do observador: no ELF ARM64 instalado, o caminho ativo chegou a writev. Seguimos os endereços de retorno até FUN_009cca98 (RVA 0x8cca98), que monta o cabeçalho PTCP e seus vetores; FUN_009cb710 entrega esses vetores ao wrapper de socket; FUN_009a40d8 trata o resultado; e FUN_009a5f34 chama writev. O primeiro iovec tem 24 bytes, mas o datagrama pode incluir vetores adicionais. Portanto, registrar só aquele vetor teria produzido um “pacote de 24 bytes” falso.2

No sentido oposto, a cadeia identificada foi recv → FUN_009a43bc → FUN_009cbd10 → FUN_009ccee8 → FUN_009ccf34. O loop observado usa um buffer de 2.048 bytes; o próximo limite rejeita entradas acima de 65.535 bytes; o parser exige pelo menos 24 bytes e localiza as extensões antes de entregar o payload. Esses são limites da implementação examinada, não uma promessa de MTU ou de segurança do protocolo. Hooks em wrappers diferentes podiam contar a mesma leitura duas vezes, então cruzamos chamadas nativas com datagramas e contadores de bytes, em vez de transformar contagem de hooks em contagem de pacotes.3

O mapa abaixo é pseudocódigo explicativo, escrito por nós a partir das fronteiras identificadas. Não é fonte do SDK, nem especifica as transições que ainda não exercitamos:

UDP recebido
  → FUN_009ccf34: cabeçalho PTCP e posição real do payload
  → FUN_009ce778: janela, sequência e liberação contígua
  → FUN_009c2250: tamanho exato e canal da mensagem proxy
  → FUN_009d65bc: bytes após o cabeçalho proxy para o socket local
  → CSock do transporte HTTP: PLAY, SDP e registros intercalados

A análise estática liga essas funções; a sessão oficial liga o canal ao socket de loopback e aos estados do player; a comparação byte a byte entre PTCP remontado e TCP local liga as duas metades. Nenhuma dessas provas, isoladamente, identifica o peer UDP como a câmera física.4

No ELF ARM64 identificado no Apêndice III, os nomes FUN_* do mapa acima são endereços do Ghidra com image base 0x100000; RVA = endereço do Ghidra − 0x100000. A tabela cruza cada função com seu RVA. CSock designa o componente HTTP no fluxo explicativo, não uma função à qual se possa atribuir um único RVA.

Tabela de dados 01
Símbolo / ponto no ELF instaladoOperação relevanteO que resolveu — e o limite
FUN_009cca98 · 0x8cca98Montagem do cabeçalho PTCP no primeiro iovec, de 24 bytesOs demais vetores também compõem o datagrama; o tamanho do primeiro vetor não era o tamanho do pacote.
0x8cb710 → 0x8a40d8 → 0x8a5f34Entrega dos vetores ao wrapper, tratamento do resultado e chamada de writevExplica por que os hooks em send* nada viram; um retorno de envio local ainda não prova aceitação pelo peer.
FUN_009ccf34 · 0x8ccf34Leitura do controle em byte 12 e cálculo do início do corpo após extensõesImpede classificar byte 24 como tag proxy em todo datagrama.
FUN_009ce778 · 0x8ce778Admissão por janela e liberação contígua na recepção PTCPRollover e sobreposição parcial ainda não têm paridade independente completa.
FUN_009c2250 · 0x8c2250Validação e despacho da mensagem proxy por canalA presença do ramo não prova que todo tipo de mensagem foi exercitado.
FUN_009d65bc · 0x8d65bcEntrega parcial dos bytes de dados ao socket localO avanço do offset local não prova aceitação pelo peer remoto.
0x8c59e8Comparação de w7 com 3, enquanto x8 transporta o retorno agregadoCorrige a numeração visual dos argumentos no decompilador; não prova qual ramo a sessão escolheu.

Esta é uma tabela autoral de operações verificadas no binário identificado, não uma transcrição de disassembly proprietário. Cada linha preserva a diferença entre o que o código pode fazer e o que a sessão observada executou.

Um desvio metodológico apareceu na fábrica de transportes FUN_009c59a4: o decompilador mostrava um parâmetro extra no início. A instrução ARM64 em RVA 0x8c59e8 compara w7 com 3; o retorno agregado sai pelo registrador oculto x8, que o Ghidra representou como pseudoparâmetro e deslocou a numeração visual dos demais. Corrigir essa ABI impediu atribuir a escolha TCP/PTCP ao argumento errado. A comparação e os construtores existem no binário, mas a análise estática não prova qual ramo foi escolhido na abertura específica da câmera; uma reabertura “quente” tampouco chamou a fábrica durante a janela observada.5

Conta, terminal e inventário: três passos diferentes#

Antes de pedir vídeo, o cliente precisa de uma identidade de terminal aceita pela conta. O app consulta o estado de verificação em user.account.GetTwoStepVerifyState. Quando o estado exige confiança, há um fluxo próprio de código e GrantingCredit; ele não é uma consequência implícita de conhecer a senha. Na pesquisa, uma identidade de terminal separada recebeu confiança pelo fluxo legítimo de código inserido pelo usuário. Duas tentativas retornaram o código de erro 11001; comparar seus campos e transporte com a solicitação nativa levou à terceira, aceita. Como mudamos um grupo de atributos públicos do cliente de uma vez, não isolamos qual campo individual causava o 11001. O readback mostrou credit=true para essa identidade.6

Depois, GetToken troca a credencial de login por username, token, ID de sessão e rota. Login usa o contexto retornado; uma leitura autenticada de configuração confirmou a sessão. Uma sessão antiga recebeu o código 12002, enquanto novo GetToken/Login restaurou as leituras. Esse contraste comprova o caminho de recuperação observado, mas não estabelece prazo universal de expiração, refresh automático ou revogação.

As chamadas SaaS têm um corpo JSON com data e uma assinatura calculada sobre bytes bem definidos. Nelas, o Content-MD5 é Base64 de MD5 do corpo JSON não comprimido; isso não define a codificação do header homônimo nas mensagens NFPOST de sinalização. O texto canônico termina cada linha com LF e inclui método, URI, digest, tipo de conteúdo e cabeçalhos x-pcs-* em ordem. A chave usada na fase autenticada é o texto hexadecimal minúsculo de MD5 do token retornado, codificado em UTF-8; não é o digest cru de 16 bytes. Há revisões e detalhes de serialização distintos entre operações, portanto o trecho abaixo mostra a canonização e a assinatura de um exemplo sintético, não um cliente pronto:

import base64
import hashlib
import hmac

body = b'{"data":{"example":"synthetic"}}'
digest = base64.b64encode(hashlib.md5(body).digest()).decode("ascii")
headers = [
    ("x-pcs-apiver", "191204"),
    ("x-pcs-client-ua", "synthetic-only"),
    ("x-pcs-date", "2000-01-01T00:00:00Z"),
    ("x-pcs-nonce", "synthetic-nonce-only"),
    ("x-pcs-username", r"account\synthetic@example.invalid"),
]
canonical = "\n".join([
    "POST", "/pcs/v1/example.synthetic", digest,
    "application/json; charset=utf-8",
    *(f"{name}:{value}" for name, value in headers), "",
])
key = hashlib.md5(b"synthetic-token").hexdigest().encode("ascii")
signature = base64.b64encode(
    hmac.new(key, canonical.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
expected_canonical = (
    "POST\n/pcs/v1/example.synthetic\n"
    "rEtqwrqwnR90eVh0VWvw4g==\n"
    "application/json; charset=utf-8\n"
    "x-pcs-apiver:191204\n"
    "x-pcs-client-ua:synthetic-only\n"
    "x-pcs-date:2000-01-01T00:00:00Z\n"
    "x-pcs-nonce:synthetic-nonce-only\n"
    "x-pcs-username:account\\synthetic@example.invalid\n"
)
assert digest == "rEtqwrqwnR90eVh0VWvw4g=="
assert canonical == expected_canonical
assert signature == "teHxHnfCQYo4tnY0z2d/8B6I58Sxh7S3KcmtITEIjBE="
assert hmac.new(key, canonical.replace("\n", "\r\n").encode(),
                hashlib.sha256).digest() != base64.b64decode(signature)

O token e a rota inventados não permitem uma requisição válida. Este vetor didático fixa bytes e saída esperada; a troca de LF por CRLF é o caso negativo. Seis outros vetores sintéticos foram comparados com a rotina instalada; não atribuímos a este exemplo didático uma paridade que não medimos. A aceitação real veio de chamadas separadas no terminal de pesquisa.

Com a sessão autenticada, UserFamilyGet seleciona o contexto familiar; BasicList pagina os dispositivos e BasicInfoGet devolve os detalhes necessários à câmera autorizada. Uma execução independente percorreu três páginas do inventário (2 + 2 + 1), escolheu a câmera permitida e obteve configuração e credenciais encapsuladas. A consulta de estado e algumas leituras de capacidade também foram aceitas, mas um botão visível no app ou um campo local ability não prova permissão de escrita no servidor. Não executamos mudanças persistentes na câmera para este artigo.

A negociação não começa com RTSP#

O caminho observado não é uma URL RTSP direta nem uma descoberta ONVIF. O cliente envia mensagens P2P em formato NFPOST, consulta a rota de descoberta e recebe um Info cifrado. Depois de decifrar o conteúdo do Info com parâmetros de provisionamento identificados no APK, encontra um randsalt que alimenta uma nova solicitação P2P. Essa segunda solicitação usa campos dinâmicos novos, autenticação WSSE e um papel separado da credencial da câmera. As respostas trazem candidatos de endereço e um valor Identify que relacionamos à troca de conectividade.

O desafio foi não converter uma lista de candidatos em uma regra fixa. Em sessões oficiais, o candidato usado para mídia mudou de posição. Numa captura, duas portas no mesmo endereço público ficaram ativas ao mesmo tempo: uma carregou controles, a outra carregou o canal de mídia. Isso não é evidência de “falhou a primeira, tente a segunda”. Também não identifica se o peer público é a câmera atrás de NAT ou um intermediário do fornecedor. O cliente de pesquisa aceitou o caminho que respondeu à transação de conectividade, não o índice de uma lista.

Sequência observada de conta, sinalização, transporte e primeiro frame
02 / Autenticação até o primeiro frameAbrir a sequência em tamanho real ↗

Em um Mac, sem Mibo, AVD ou bibliotecas proprietárias em execução, repetimos a cadeia com uma sessão própria já confiável: configuração e detalhes atuais por HTTPS, P2P antes do randsalt, descoberta e Info, P2P usando o randsalt, conectividade do candidato, PTCP, abertura do canal proxy, PLAY 200, registro DHAV completo e fechamento. Em outra execução com identidade própria e parâmetros estáticos obtidos offline, o processo também foi da nova autenticação ao frame decodificado com o Mibo parado.7 A evidência cobre essas rotas e esse modelo de câmera, não toda topologia NAT, firmware ou condição de conta.

Recibo higienizado: uma travessia no AVD#

A sequência abaixo pertence à mesma execução independente AUTH-070, em um app_process Android separado, com o Mibo parado e sem classe do SDK carregada. Os tamanhos são das respostas dessa execução; a paginação de três páginas citada antes pertence a outro teste de inventário. Nomes de câmera, endereços, valores de sessão e corpos de mídia não aparecem aqui.

Tabela de dados 02
EtapaMensagens e resultado observadoCondição para avançar
ContaGetToken e Login: HTTP 200/código 10000; respostas de 228 e 1.363 bytesContexto de conta novo aceito; nenhum token anterior reutilizado.
Dados autorizadosGlobalConfigGet, UserFamilyGet, BasicList e BasicInfoGet: HTTP 200/código 10000; respostas de 9.422, 375, 2.760 e 19.120 bytesFamília e câmera autorizada escolhidas das respostas atuais; cinco dispositivos numa resposta BasicList desta execução.
SinalizaçãoNFPOST P2P inicial aceito; uma resposta de descoberta e uma de Info; Info decifrado por primitivas padrão; segundo NFPOST com randsalt novo aceito. Classes: identidade/dispositivo, WSSE (nonce, instante e digest), candidatos, Identify e randsalt; este recibo não preserva comprimentos das mensagens de sinalizaçãoCandidatos e valor Identify vieram dessa negociação; não de um cache do Mibo.
Candidato e transporteTroca de conectividade correspondente, controle PTCP correspondente e abertura do canal proxyO canal recebeu CONN correspondente antes de PLAY.
Mídia e encerramentoPLAY recebeu 200; primeiro DHAV completo de 91.919 bytes passou checksum e trailer; KDF, verificador e AES-OFB padrão produziram um frame H.264 decodificado de 640 × 480; pedido de encerramento recebeu resposta e houve DISC nos dois sentidosUm frame foi decodificado, sem renderizar ou salvar mídia. A troca de fechamento não prova liberação imediata dos recursos remotos.

Os verbos “aceito” e “correspondente” descrevem essa execução delimitada e suas verificações de estado; a tabela não publica material para repetir a autenticação de outra pessoa. A execução posterior no Mac e a apresentação pelo app oficial são experimentos distintos, identificados no recibo final. Na execução separada SIGNAL-114 no Mac, os dois P2P receberam 100 e depois 200; descoberta e Info receberam 200; o PLAY independente tinha 1.327 bytes e recebeu 200, seguido de um DHAV de 96.467 bytes cujo envelope foi validado. Esse registro do Mac não foi decifrado ou decodificado naquela execução; os decodes no host vieram depois.

PTCP: transformando datagramas em uma conversa ordenada#

Os pacotes UDP observados começam com PTCP. Esse nome não fixa um único layout. Dois projetos públicos da família Dahua/Easy4IP, dh-p2p e dahua-p2p, descrevem outro cabeçalho de 24 bytes com PID, LMID e RMID. No ELF e nas capturas desta sessão Mibo, os mesmos offsets participam de controle de extensão, flags, janela e relógios. O PseudoTCP do WebRTC é uma referência estrutural para sequência, ACK e relógios, não prova de interoperabilidade ou origem do código instalado.

Tabela de dados 03
OffsetMibo Smart 3.1.1 nesta pesquisaProjetos Dahua P2P citadosBase da leitura do Mibo
0–3PTCPPTCPCaptura e produtor nativo.
4–7 / 8–11Sequência de bytes / ACK cumulativo, big-endianBytes enviados / recebidosAvanço observado por bytes; ACK e buffer rastreados no nativo.
12 / 13 / 14–15Controle opcional de extensão / flags / janelaPrimeiros quatro bytes do PIDProdutor e parser nativos; 28 pacotes com extensão numa captura delimitada.
16–19 / 20–23Relógio do emissor / eco do peerLMID / RMIDProdutor e correlação de relógios; rollover e RTT completo ainda parciais.
A partir de 24Corpo ou comprimento de extensão seguido do corpoTipo, comprimento de 24 bits, realm e paddingParser nativo e remontagem da captura; não se presume igualdade de wire.

Neste estudo, o payload pode começar no byte 24 ou depois de uma região de extensão. Interpretar o byte 24 sempre como tag proxy confundiria comprimento de extensão com tipo de mensagem. A tabela distingue duas leituras de implementações diferentes; não estabelece que um cliente de uma delas consiga conversar com a outra.

No payload, tags do protocolo de proxy distinguem dados (0x10), abertura (0x11), marcadores como CONN/DISC (0x12) e controle periódico (0x13). SYN e CONN do canal vieram antes de PLAY. O contador do transporte avança em bytes de corpo, e não em número de datagramas. Duplicatas, reordenação, ACK e janela precisam ser tratados como estados, não como uma sequência ingênua de recvfrom.

O extrator abaixo demonstra apenas validação do comprimento e a fronteira de extensão; a pequena remontagem seguinte funciona somente para um fixture offline sem rollover ou lacunas permanentes. Não implementa a confiabilidade completa de PTCP:

def ptcp_body(packet: bytes) -> tuple[int, bytes]:
    if len(packet) < 24 or packet[:4] != b"PTCP":
        raise ValueError("invalid PTCP header")
    offset = 24
    if packet[12]:
        if packet[12] != 1 or len(packet) < 25:
            raise ValueError("unsupported extension control")
        offset = 25 + packet[24]
        if offset > len(packet):
            raise ValueError("truncated extensions")
        position = 25
        while position < offset:
            if position + 2 > offset:
                raise ValueError("truncated extension header")
            tag, size = packet[position:position + 2]
            if tag != 0 or size < 8 or size % 4 or position + 2 + size > offset:
                raise ValueError("invalid range extension")
            position += 2 + size
    return int.from_bytes(packet[4:8], "big"), packet[offset:]

def contiguous(chunks: list[tuple[int, bytes]], start: int) -> bytes:
    parts: dict[int, bytes] = {}
    for sequence, body in chunks:
        if sequence in parts and parts[sequence] != body:
            raise ValueError("conflicting duplicate")
        parts[sequence] = body
    result = bytearray()
    while start in parts:
        body = parts.pop(start)
        result.extend(body)
        start += len(body)
    return bytes(result)

assert contiguous([(3, b"def"), (0, b"abc"), (0, b"abc")], 0) == b"abcdef"

Dois datagramas inventados, uma sequência contínua#

É mais fácil ver a fronteira com bytes concretos. Os dois datagramas abaixo foram montados para este artigo: canal 0x01020304, ACK oposto 0, janela 0x1000, relógio 1 e payloads ASCII abc/def. Nenhum campo veio da câmera, da conta ou de um PCAP. Em ambos, os 24 primeiros bytes são PTCP e os 15 seguintes são uma mensagem proxy 0x10 de 12 bytes mais três bytes de dados:

             PTCP 0–23                              proxy 24–38
A  50 54 43 50  00 00 10 00  00 00 00 00  00 00 10 00
   00 00 00 01  00 00 00 00 |10 00 00 03  01 02 03 04
   00 00 00 00  61 62 63                         abc

B  50 54 43 50  00 00 10 0f  00 00 00 00  00 00 10 00
   00 00 00 01  00 00 00 00 |10 00 00 03  01 02 03 04
   00 00 00 00  64 65 66                         def

50 54 43 50 é PTCP. Os quatro bytes em 4–7 são sequência big-endian: 0x1000 em A e 0x100f em B. A diferença 0x0f é 15 bytes de corpo PTCP, não três bytes do payload final. No corpo, 10 marca dados; 00 03 em body+2 declara três bytes de payload; 01 02 03 04 identifica o mesmo canal em ambas as mensagens; os quatro bytes em body+8 foram zerados neste exemplo, sem atribuir-lhes semântica geral. O validador nativo exige comprimento total exato de 12 + 3 para esse tipo.8 Os bytes 10 00 00 03 também admitiriam a leitura pública “tipo + comprimento de 24 bits igual a 3”. Portanto, A e B ensinam reordenação, não distinguem os dois layouts.

Um teste sintético separado, derivado da regra de extensão do parser instalado, faz essa distinção: partimos de um pacote de dados inventado e definimos byte 12 = 01; em byte 24, 0a declara dez bytes de extensão; seguem 00 08 e oito bytes de intervalo. O corpo 0x10 passa então a começar no offset 35. O verificador sintético check-ptcp-framing.py aceita esse deslocamento e rejeita extensões truncadas. Em uma captura delimitada, 28 pacotes tinham controle de extensão 1 e comprimento 0x0a no byte 24.9 No formato PID/LMID/RMID das implementações públicas citadas, o byte 12 pertence ao PID e o byte 24 inicia o corpo. Os bytes inventados abaixo tornam o deslocamento visível; PTCP… representa os 24 bytes de cabeçalho, com 01 no offset 12:

offset  0–23: PTCP… (byte 12 = 01)
offset     24: 0a                         comprimento da extensão
offset 25–34: 00 08 00 00 00 00 00 00 00 00
offset     35: 10 00 00 03 …            início do corpo proxy

O teste sintético deriva da análise local; não é um pacote real nem prova paridade do cliente público com o Mibo.

Se B chegar antes de A, um receptor que espera 0x1000 pode guardar B, mas ainda não pode liberar def como o início do fluxo. Quando A chega, há 30 bytes PTCP contíguos; só então as duas mensagens de canal podem liberar abc e def na ordem. Uma cópia idêntica de A não acrescenta dados. O ACK cumulativo ilustrativo permaneceria em 0x1000 até A e avançaria para 0x101e depois de A+B. Isso ilustra a admissão e a liberação contínua vistas na rotina nativa; não afirma que esses ACKs inventados foram observados na rede, nem resolve rollover, sobreposição parcial, retransmissão e congestionamento.10

Três chegadas sintéticas: B fora de ordem, A fechando a lacuna e uma duplicata de A descartada; o fluxo proxy resultante contém abcdef
Pacotes / De datagramas fora de ordem a bytes contíguosAbrir o fluxo de datagramas em tamanho real ↗

O teste decisivo não foi “vimos UDP”. Agrupamos o canal de dados, eliminamos duplicatas equivalentes, ordenamos pelos contadores e comparamos o fluxo reconstruído com o TCP de loopback que o SDK entregava ao player. Dois fluxos completos — 2.994.000 e 779.936 bytes — foram idênticos byte a byte às reconstruções locais, inclusive 1.263 e 326 registros DHAV. Isso prende o canal PTCP à mídia efetivamente consumida sem supor a identidade física do peer UDP.11

As camadas do datagrama UDP até o registro de mídia
03 / Camadas de transporte e mídiaAbrir o diagrama das camadas em tamanho real ↗

Há um segundo envelope depois da ordenação PTCP. No produtor nativo FUN_009c4ca4, a abertura de canal 0x11 ocupa 20 bytes: escrita LE32 do valor 0x11 em 0–3, canal BE32 em 4–7, seis bytes zerados, porta BE16 em 14–15 e IPv4 de candidato em 16–19. É um mapa estático da escrita do produtor: 11 00 00 00 também pode ser descrito no wire como tipo 0x11 seguido de três bytes zero. A escrita LE32 isolada não decide a semântica desses quatro bytes, nem prova que qualquer canal foi aceito. A aceitação veio de outra fronteira: para a sessão observada, o canal correspondente recebeu um marcador 0x12 com os quatro bytes literais CONN, mudou de estado e só depois recebeu PLAY 200. O mesmo dispatcher reconhece estaticamente SINV e DISC; os nomes não devem ser transformados em uma enumeração universal de estados.12

Esta transcrição ilustrativa em C mostra só o layout que inferimos do produtor nativo. channel, port e candidate_ipv4 representam valores inventados pelo leitor; as funções put_* expressam endianidade, não são funções copiadas do SDK. O trecho não abre um socket nem produz uma autorização:

uint8_t open[20] = {0};
put_le32(open + 0, 0x11);       // tag de abertura
put_be32(open + 4, channel);    // canal sintético
put_be16(open + 14, port);      // porta do candidato
copy_four(open + 16, candidate_ipv4);

Para dados 0x10, o dispatcher exige 12 + BE16(body+2) bytes e usa o canal BE32 em body+4 para encontrar o stream. FUN_009d65bc envia ao socket local apenas os bytes após esse cabeçalho de 12 bytes; se o socket aceitar só parte, o objeto guarda o offset restante antes de processar outro bloco. É por isso que a comparação PTCP↔loopback pode ser byte a byte sem os cabeçalhos proxy, e por que um cliente próprio precisa manter fronteiras separadas para datagrama, sequência PTCP, mensagem proxy e bytes de mídia.13

O loopback é uma ponte interna do app: um objeto de sessão proxy escreve numa extremidade TCP e o transporte HTTP de mídia lê a outra. Hooks de socket, alias e contadores de bytes confirmaram a passagem. Um cliente novo pode alimentar seu parser diretamente com os bytes ordenados; copiar a arquitetura de loopback não é requisito do protocolo.

PLAY 200 ainda não é um frame#

Dentro do canal proxy, a abertura de mídia usa um pedido PLAY com aparência HTTP. 0, 2 e 3 são valores de um parâmetro da operação examinada, não nomes de métodos RTSP padronizados: na rota observada, 0 abre, 2 funciona como keepalive da aplicação e 3 solicita encerramento. Esse keepalive é distinto de ACKs PTCP e de controle periódico do proxy. O PLAY recebeu HTTP/1.1 200 e SDP; na mesma conversa apareceram registros intercalados com prefixo $, byte de track e comprimento big-endian. Cada registro carrega um corpo DHAV. O prefixo observado nesta rota tem seis bytes: $, um byte de track e comprimento BE32. Isso difere dos quatro bytes ($, canal e BE16) definidos para RTP/RTCP intercalado no RFC 2326 §10.12. Na captura analisada, o BE32 coincidiu com o comprimento DHAV de um registro de 93.140 bytes, maior que o máximo representável por BE16; esse confronto vem do fluxo real, não do fixture inventado.14 Uma resposta da operação 3 pode compartilhar o mesmo pacote proxy com mídia, então o parser precisa ser incremental.

A resposta não termina quando começam os registros#

O fixture totalmente sintético tem 145 bytes. Ele foi construído para forçar a troca de estados do parser após a remontagem PTCP/proxy: uma resposta HTTP com cinco bytes privados, um registro intercalado e outra resposta HTTP. Os offsets são do fluxo ordenado, não dos datagramas UDP:

Tabela de dados 04
OffsetBytesInterpretação no fixture
0–5152HTTP/1.1 200 OK, Cseq: 1, Private-Length: 5, linha vazia e v=0\r\n
52–57624 00 00 00 00 28: $, track 0, comprimento big-endian 40
58–9740DHAV inventado: 44 48 41 56 no início, tipo 0xfc, trailer 64 68 61 76 28 00 00 00
98–14447Nova resposta HTTP/1.1 200 OK, Cseq: 2, Private-Length: 0

O parser precisa esperar os seis bytes do prefixo $ antes de conhecer o tamanho do registro, esperar os 40 bytes declarados antes de validar DHAV e depois voltar a reconhecer HTTP. Em um corte de rede, o próximo byte pode terminar um cabeçalho, um trailer ou iniciar outra resposta. O fixture passou por todos os 146 pontos de divisão em dois blocos, além de cortes uniformes; comprimentos, checksum e trailer malformados foram rejeitados. Isso testa framing incremental com dados inventados, não confiabilidade PTCP nem decodificação de vídeo.15 A captura real mostrou que resposta de método 3 e mídia podem dividir a mesma mensagem proxy, razão prática para essa alternância.16

Um registro DHAV observado tem DHAV no início, comprimento total little-endian no offset 12, extensão iniciada no offset 24, checksum aditivo no byte 23 e trailer dhav seguido da repetição do comprimento. O comprimento da extensão está no byte 22. O exemplo abaixo valida apenas esse envelope num registro sintético de até 1 MiB; não decide codec, criptografia ou autorização:

def validate_dhav(record: bytes) -> bytes:
    if len(record) < 32 or len(record) > 1_048_576:
        raise ValueError("record size")
    total = int.from_bytes(record[12:16], "little")
    payload_start = 24 + record[22]
    if (record[:4] != b"DHAV" or total != len(record)
            or payload_start > len(record) - 8
            or record[23] != sum(record[:23]) % 256
            or record[-8:-4] != b"dhav"
            or int.from_bytes(record[-4:], "little") != total):
        raise ValueError("invalid DHAV envelope")
    return record[payload_start:-8]

Essas verificações não autenticam o remetente. O checksum de um byte detecta corrupção acidental; não é um MAC. Um parser real também precisa de limites por campo, travessia de tags de extensão conhecidas, buffer incremental e tratamento explícito de variantes não reconhecidas. O fixture sintético de HTTP/DHAV percorreu todos os cortes em dois fragmentos e comprimentos de fragmento uniformes, além de rejeitar cabeçalho, trailer e comprimentos inválidos.17

Mapa de offsets do envelope DHAV e da região protegida observada
04 / Registro de mídia DHAVAbrir o mapa DHAV em tamanho real ↗

Do payload protegido ao pixel#

Nos registros examinados, 0xfd trazia keyframes H.264 protegidos, 0xfc carregava vídeo subsequente em claro e 0xf0 carregava AAC protegido. Outros tipos, incluindo 0xf1 em algumas sessões, ainda não têm papel universal demonstrado. O demuxer DHAV do FFmpeg reconhece 0xf1 no probe e salta seu registro durante a leitura; isso não identifica a função dos corpos 0xf1 vistos aqui. Uma extensão 0xb5 de 44 bytes indicava, na variante testada, prefixo em claro, tamanho da região protegida, campos de derivação, verificador e IV. A chave de mídia veio de credenciais da câmera obtidas legitimamente nos detalhes do dispositivo e derivadas com primitivas padrão; não foi copiada do cache nativo.

Aplicamos AES-256-OFB somente na região declarada, preservando o resto do registro. O white paper de segurança Dahua v2 descreve AES-256-OFB para proteção de frames e uma troca de chaves por envelope digital em sua oferta; é contexto de família tecnológica, não prova de que o envelope descrito participe da derivação 0xb5 medida nesta variante. O verificador observado combina SHA-256 e CRC-16 sobre os campos previstos; ele ajudou a confirmar que a chave e o envelope batiam naquele registro, mas não substitui autenticação criptográfica. O frame resultante continha SPS/PPS/IDR Annex B. FFmpeg decodificou uma imagem H.264 Main de 640 × 480. Em uma captura independente delimitada, 63 unidades de vídeo também foram decodificadas; em outra, 168 unidades AAC-LC mono de 16 kHz produziram PCM padrão. Uma execução no host Mac alcançou registro protegido e fechamento e, em uma verificação posterior, decodificou mídia. Nada disso dependeu de exibir ou publicar a imagem privada.

O app oficial forneceu uma comparação importante: no mesmo episódio instrumentado, observamos abertura do canal, PLAY 200, configuração de mídia nativa, metadados do keyframe decifrado, onPlayBegan e o primeiro swap EGL bem-sucedido. O swap foi correlacionado com essa abertura por estados e horários; não identificamos seus pixels nem provamos que o frame exibido é idêntico ao decodificado no Mac. Assim, a evidência de mídia vai além de PLAY 200 ou de um cabeçalho SPS. Também não confundimos decodificar um frame offline com possuir um renderizador desktop estável, sincronização A/V ou recuperação automática.

O que as opções oficiais mudam nessa leitura#

O caminho acima descreve a abertura remota que medimos no Mibo Smart, não todas as formas de acessar uma câmera Intelbras. O manual da Mibo Smart documenta ONVIF Profile S e Intelbras-1 para DVR/NVR, com cadastro prévio pelo app e gravador na mesma rede; ele registra uma exceção para a iM8. Isso torna o acesso local por protocolos padronizados uma alternativa real a investigar, mas não demonstra que esta câmera, em outra rede, ofereça por ONVIF as mesmas sessões, permissões e recursos do caminho P2P estudado.

O contexto oficial mudou com o tempo. Em 2018, o suporte Intelbras disse que o foco da linha Mibo era o monitoramento mobile; em 2024, respondeu que a linha não tinha API nem SDK. Essas respostas históricas não descrevem sozinhas a oferta atual: o manual da GDI hoje documenta uma API HTTP para contas Mibo, inclusive criação de fluxo de vídeo para câmeras iM com visualização em nuvem. Ela requer cadastro empresarial com CNPJ, vínculo da conta e plano de streaming sujeito a cotas. Não testamos essa API, seu formato de mídia, latência ou disponibilidade para esta câmera; ela pode oferecer um caminho de integração distinto, mas não valida nem invalida os bytes PTCP observados. O SIM Play é um cliente oficial para Windows e macOS, com compatibilidade condicionada ao produto ou Intelbras-1; o suporte distingue seu acesso local do acesso Mibo pela nuvem. Não presumimos que ele use a mesma conta ou ofereça as mesmas funções do Mibo Smart.

Instalar outro firmware na câmera estava fora do escopo do Noctua. Exigir que as pessoas modifiquem suas câmeras antes de usar um cliente desktop tornaria a instalação difícil demais e acrescentaria risco a um aparelho do qual já dependem. Por isso, investigamos caminhos que funcionam com o firmware existente.

O objetivo do Noctua é um cliente nativo para Linux, macOS e Windows. Este experimento já atravessou autenticação e mídia num host Mac, mas ainda não prova um aplicativo pronto, equivalência funcional com as ofertas oficiais nem portabilidade nas três plataformas. Comparar GDI, ONVIF local e a rota P2P quanto a acesso remoto, custo, controle da sessão, privacidade e cobertura de modelos é trabalho de produto posterior, não uma conclusão deste primeiro frame.

O que esta primeira travessia realmente entrega#

O resultado mais útil para o Noctua é uma cadeia verificável de entradas e saídas: identidade própria confiável, nova autenticação, inventário paginado, detalhes da câmera, sinalização com o novo randsalt recebido no Info, candidato responsivo, PTCP e proxy, PLAY, DHAV protegido, chave derivada independentemente, frame H.264 decodificado e fechamento solicitado. Os experimentos com o app oficial e com processos independentes se cruzam em fronteiras de bytes, estados e códigos de retorno. Isso é suficiente para começar um SDK sem copiar o aplicativo ou o SDK proprietário em tempo de execução.

Para explicitar a trilha de evidências e seus limites, este recibo separa os ambientes e experimentos que atravessaram as fronteiras mais importantes:

Tabela de dados 05
Execução / marco18O que atravessou a fronteiraO que não demonstra
Mibo no AVD — transportePTCP reconstruído idêntico, byte a byte, ao loopback consumido pelo player em duas capturasIdentidade física do peer UDP; são capturas anteriores à abertura oficial da linha seguinte
Mibo no AVD — apresentaçãoCONN, PLAY 200, metadados do keyframe decifrado, onPlayBegan e primeiro swap EGL correlacionados numa aberturaIdentidade dos pixels exibidos; é outra execução, não o frame independente
Processo próprio no AVD — bootstrapTerminal confiável, novo login, dados atuais, sinalização, frame decodificado e pedido de fechamento com o Mibo paradoCliente desktop ou duração da confiança
Processo próprio no Mac — mídiaSockets novos, PLAY 200, DHAV protegido; numa sessão posterior, 119 unidades de vídeo e 128 AAC decodificadas após o fechamentoRenderização contínua, áudio audível ou sincronização A/V
Fixture offline — parserCortes arbitrários de HTTP/DHAV sintético e rejeição de envelopes inválidosRobustez de todas as variantes ou aceitação por servidor

O arquivo de pesquisa preserva fixtures higienizados e um corpus privado cifrado com SOPS; não publicaremos capturas brutas, binários proprietários nem frames da câmera. Os snippets acima permitem testar ideias de framing em dados inventados. Reproduzir aceitação do servidor requer uma conta e um dispositivo próprios, consentimento e condições de rede equivalentes.

Ainda é uma amostra delimitada. Precisamos de operação longa com relógios e apresentação A/V, mais topologias e variantes de dispositivo, comportamento completo sob perda e reconexão, expiração/renovação de sessão e prova de liberação remota após o fechamento. Leituras de configuração aceitas não tornam comandos de escrita prontos. A próxima etapa da série poderá tratar essas fronteiras e, separadamente, a pesquisa de vulnerabilidades de protocolos proprietários com critérios de confirmação próprios. Aqui não afirmamos ter encontrado uma vulnerabilidade explorável.

Se há uma lição metodológica, é esta: a engenharia reversa avança quando uma hipótese encontra um byte, um estado e um experimento independente capaz de desmenti-la. O diagrama é o fim da história; os testes que sobrevivem às nossas suposições são a história de verdade.

Apêndice I — LLMs no laboratório de protocolos#

Esta investigação teve um segundo instrumento além do disassembler, do proxy e dos hooks: uma LLM trabalhando dentro de um harness de pesquisa. Ela ajudou a atravessar rapidamente código Java, pseudocódigo ARM64, registros de execução e fixtures. O ganho veio de encurtar o intervalo entre pergunta, hipótese, experimento e correção. Não cronometramos uma equipe sem LLM para atribuir um fator numérico de aceleração; “mais rápido” aqui descreve o ciclo de trabalho que conseguimos observar, não um benchmark.

O harness começa por uma âncora imutável: hashes do APK e das bibliotecas, arquitetura e versão do ambiente. Ele fornece à LLM recortes pequenos, com proveniência, e executa verificadores que não aceitam uma explicação apenas porque ela soa plausível. Para as hipóteses que se tornaram achados, registramos uma condição que poderia refutá-las. O resultado volta para um índice de evidências com status separado — código instalado, paridade sintética, execução oficial, aceitação independente ou lacuna. Quando a versão do binário muda, offsets e conclusões dependentes dele precisam ser revalidados.

recorte com hash e origem → hipótese + teste falsificável
                          → fixture sintético / rotina instalada
                          → observação delimitada / cliente independente
                          → evidência, contraexemplo ou desconhecido

Um exemplo concreto foi o silêncio de send, sendto e sendmsg no ponto em que esperávamos um datagrama PTCP. O harness manteve esse resultado como falha de cobertura do observador, abriu o caminho de chamadas nativas e encontrou writev. A leitura de um único iovec sugeriria um pacote de 24 bytes; conferir todos os vetores e compará-los com o datagrama impediu essa conclusão. Em outro ponto, o pseudocódigo da fábrica de transportes deslocava argumentos por causa do retorno agregado oculto em x8. A instrução que usa w7 foi um árbitro melhor que a assinatura decompilada. Esses exemplos mostram onde uma LLM pode acelerar a navegação e propor verificações; os bytes e a ABI decidiram o resultado.

O mesmo padrão funcionou para partes reproduzíveis sem câmera. Para assinatura HTTPS, o harness gerou entradas artificiais e comparou os bytes canônicos com a rotina instalada e com primitivas padrão. Para PTCP e HTTP/DHAV, fixtures inventados cortaram o fluxo em posições arbitrárias e introduziram comprimentos, checksums e trailers inválidos. Isso revelou interpretações frágeis antes de qualquer sessão ao vivo. Paridade sintética fixa uma regra local; só uma requisição aceita, uma transição de estado ou um frame decodificado por processo independente atravessa a fronteira seguinte.

Há também um limite operacional importante. APKs, logs e respostas de ferramentas são dados não confiáveis para a LLM: podem conter instruções hostis, segredos ou valores de outra sessão. O harness restringiu ferramentas e escopo, suprimiu valores sensíveis, usou fixtures sintéticos em checks públicos e guardou o corpus privado cifrado. Nos experimentos descritos aqui, sugestões do modelo não substituíram autorização; os exemplos publicados usam apenas dados sintéticos, e o corpus de mídia permaneceu cifrado. A LLM foi útil justamente porque podia receber um “não” verificável do experimento. Sem esse mecanismo, ela só produziria uma especificação convincente de um protocolo que talvez não exista.

Apêndice II — a bancada de engenharia reversa#

Nenhuma ferramenta entregou “o protocolo” pronto. Montamos uma bancada em que cada instrumento responde a uma pergunta diferente e deixa um artefato que outro instrumento pode contestar. As versões abaixo identificam o ambiente desta pesquisa, não requisitos universais para reproduzir o método.

Tabela de dados 06
InstrumentoO que procuramosEvidência produzida — e seu limite
ADB, AAPT e SHA-256Qual APK estava realmente instalado no AVD?Caminho do pacote, manifesto, versão e hashes do APK/DEX/ELF fixaram a baseline. Um binário identificado ainda não revela qual ramo executou.
JADX 1.5.6 e DEXQuem constrói as requisições e passa dados ao player?Call graphs, DTOs, serialização e campos de origem. Quando o decompilador falhou num switch de retry, voltamos ao DEX e ao modo fallback; pseudocódigo não virou verdade por aparência.
Ghidra 12.1.4 e LLVM do NDK 29Como o ARM64 monta buffers e chama as rotinas nativas?Xrefs, layouts, registradores e disassembly conferiram offsets e ABI. Mapeamos image base para RVA do ELF; a existência de uma função não prova que a sessão a usou.
Frida 17.23.3 no processo MiboO caminho previsto ocorreu na abertura autorizada?Hooks curtos e guardados pelo binário registraram estados, tamanhos, tempos e códigos, sem exportar valores secretos. Um hook ausente ou um crash do observador não é falha do protocolo.
tcpdump no AVD e parsers PythonQuais bytes atravessaram UDP, PTCP, proxy e TCP local?PCAP privado e remontagem com duplicatas removidas permitiram comparar dois fluxos externos com o loopback, byte a byte. Igualdade de fluxo não identifica criptograficamente o peer UDP.
Python/Java 21, primitivas padrão e FFmpegNossa interpretação reproduz bytes e decodifica mídia?Vetores sintéticos compararam assinaturas e derivação com rotinas instaladas; parsers rejeitaram envelopes inválidos; FFmpeg decodificou H.264/AAC. Um decode isolado não prova apresentação contínua nem sincronização A/V.
Fixtures higienizados e SOPSComo repetir checks sem publicar a câmera?Casos artificiais, contagens e hashes ficaram revisáveis; corpos reais de mídia autorizados ficaram cifrados. Um fixture sintético testa a hipótese local, não aceitação por conta, servidor ou firmware arbitrário.

A troca de instrumentos corrigiu erros que seriam difíceis de notar dentro de uma tela só. A primeira sondagem de envio não via datagramas porque observava send/sendto/sendmsg; o disassembly levou a writev e a captura confirmou o tamanho completo dos vetores. Em outro caso, o JADX gerou uma exceção para parte do predicado de retry; o modo fallback recuperou o controle de fluxo e 108 entradas artificiais foram comparadas com o predicado instalado, sem acionar uma reconexão de câmera. Esses checks respondem perguntas locais. A aceitação da conexão e o primeiro frame exigiram a sessão independente descrita no corpo do artigo.19

O trabalho sensível ficou fora do material publicável: APK extraído, decompilação, PCAP e logs brutos permaneceram no laboratório privado; os hooks emitiram apenas metadados permitidos. O corpus de mídia aprovado foi guardado com SOPS, e os exemplos desta página usam bytes inventados. Assim, é possível auditar a forma do teste sem transformar a imagem, a credencial ou a topologia de uma pessoa em demonstração pública.

Apêndice III — hashes da baseline analisada#

Os SHA-256 abaixo identificam a instalação Mibo Smart 3.1.1 (100315), arm64-v8a usada como APK-001. Os APKs fixam a origem do ELF; os três .so listados receberam análise identificada no relatório. libCommonSDK.so sustenta o mapa nativo de transporte e mídia deste artigo. libLCSign.so e libEasy4IpComponent.so foram inventariadas e importadas no Ghidra; sua presença ou análise estática não demonstra participação na abertura observada. Os hashes vêm dos manifestos de extração, não de uma nova captura nesta revisão. Não redistribuímos os binários.

Tabela de dados 07
ArtefatoBytesSHA-256
base.apk160.791.92767b4ca547b0c6f6bf64f4e24cd100563e7c13e0c6fc31e40c8127677fd17509c
split_config.arm64_v8a.apk127.786.0281ab512856731c243e2e0a5349082587eed2e23c67072502c54509552507eb384
lib/arm64-v8a/libCommonSDK.so17.376.3926a01a2a6aa23acd6c616f671948ac4c11e50f4976968d3250c7172feecd9a5f4
lib/arm64-v8a/libLCSign.so354.10446591595b8c0a9ef9b34565dd87b60afdf50ef10842ccefa8d206345d536ad67
lib/arm64-v8a/libEasy4IpComponent.so6.655.1525760d473b944a259e2b7191aa544a75eaec3806bdc0c63c4d74f60db5f1bb61e

Para conferir uma cópia obtida legitimamente da mesma versão e arquitetura, calcule SHA-256 sobre o APK completo ou sobre os bytes da entrada lib/arm64-v8a/<nome> extraída do split, sem reempacotar nem modificar o arquivo. Uma divergência de hash exige revalidar RVAs, call graphs e conclusões dependentes do binário; ela não prova, sozinha, mudança de comportamento.


  1. Os códigos AUTH, TRANSPORT, SIGNAL e MEDIA identificam entradas internas do relatório de pesquisa; não são URLs públicas. Mantemos as referências para distinguir o que foi mapeado estaticamente, observado no app ou aceito pelo cliente independente. ↩︎

  2. Referência interna: TRANSPORT-006 — caminho writev e primeiro iovec. ↩︎

  3. Referência interna: TRANSPORT-008/009 — cadeia de recepção e limites dos hooks. ↩︎

  4. Referência interna: TRANSPORT-001/015–017 — ligação entre PTCP, canal proxy e loopback. ↩︎

  5. Referência interna: TRANSPORT-018–020 — ABI da fábrica de transportes e limite da observação. ↩︎

  6. Referência interna: AUTH-068 — confiança da identidade própria. ↩︎

  7. Referência interna: AUTH-070 — login e frame decodificado sem Mibo em execução. ↩︎

  8. Referência interna: TRANSPORT-016 — validação do tamanho da mensagem proxy. ↩︎

  9. Referências internas: TRANSPORT-009 — extensão observada e parser; fixture sintético check-ptcp-framing.py. O segundo artefato não é paridade executada contra o SDK. ↩︎

  10. Referência interna: TRANSPORT-010/011 — admissão e liberação contígua de PTCP. ↩︎

  11. Referência interna: TRANSPORT-001 — paridade byte a byte do fluxo remontado. ↩︎

  12. Referência interna: TRANSPORT-013/017, SIGNAL-114 — abertura e confirmação do canal proxy. ↩︎

  13. Referência interna: TRANSPORT-016/017 — validação e entrega parcial dos dados proxy. ↩︎

  14. Referência interna: SIGNAL-070 — prefixo de seis bytes e comprimento DHAV conferidos na captura. ↩︎

  15. Referência interna: MEDIA-039 — fixture sintético de HTTP/DHAV. ↩︎

  16. Referência interna: SIGNAL-073/080 — resposta de encerramento intercalada com mídia. ↩︎

  17. Referência interna: MEDIA-039 — rejeição de envelopes DHAV inválidos. ↩︎

  18. IDs internos, na ordem das cinco linhas: TRANSPORT-001; MEDIA-065; AUTH-068/070; SIGNAL-114, MEDIA-069; MEDIA-039. São execuções distintas, não um único PCAP ou um único frame. ↩︎

  19. Referências internas: APK-001 (baseline); NATIVE-002/003/004 (Ghidra, LLVM e offsets); TRANSPORT-001/006 (remontagem e writev); RECOVERY-004/005 (fallback e paridade do predicado); MEDIA-064 (corpus cifrado). ↩︎