HN 日本語サマリー

← 一覧へ戻る
プログラミング

安価なIPカメラに再挑戦、パート2

Taking another shot at the cheap IP camera, part 2 (ericswpark.com)

39 pointsby ericswpark16 コメント

要約

この記事は、Tuyaプラットフォームベースの安価なIPカメラのローカルストリーム取得に再挑戦する過程を追っています。前回の試みでは失敗しましたが、今回はファームウェアの抽出とリバースエンジニアリングに焦点を当て、ONVIFサーバーとRTSPストリームを有効にする方法を探ります。JFFS2ファイルシステムの特性や、ファームウェアの再パッキング、そして最終的にはLLM(大規模言語モデル)の助けを借りて、カメラの起動プロセスやネットワーク接続の制限を克服し、ONVIFストリームを機能させることに成功した経緯が詳細に記されています。

全文翻訳

安価なIPカメラに再挑戦、パート2

2026年11月9日

約1年前、私はTuyaという独自の中国製IoTプラットフォームに基づいた、この特定の安価なIPカメラについて不満を述べました。サーバーを経由せずにローカルビデオストリームを取得しようとして失敗した後、ストリーミング機能を実際に公開しているカメラを使用する方が時間の無駄がないと結論付け、それを脇に置きました。

さて、最近ニュージャージー州での新しい仕事に引っ越す際にそのカメラを掘り起こし、もう一度試してみる価値があるかもしれないと思いました。十分な空き時間があり、もし自分で解決できなければ、LLMは最近、物事をリバースエンジニアリングするのに非常に優れていると評判です。しかし、私の最初の目標は、まず自分で試してみて、失敗したらLLMに任せて、それが解明したことから何かを学ぶことでした。

では、正確な目標は何でしょうか?前回からわかっているように、カメラには何らかのONVIFサーバーが組み込まれています。それは基本的に、Open Network Vision Interface Forum(またはONVIF!)によって定義されたIPカメラの標準化されたインターフェースです。そして、ほとんどのONVIFカメラはRTSP、つまりリアルタイムストリーミングプロトコルを使用してビデオストリームを公開します。YouTubeやTwitchのようなライブストリーミングプラットフォームにストリーミングしたことがあるなら、このプロトコルに出くわしたことがあるかもしれません。

問題は、これらのサーバーがファームウェア側で無効化されており、ポートが閉じられていることです。前回は、設定オプションを使用してそれらを有効にしようとしましたが、うまくいきませんでした。今回は、なぜそれが失敗したのか、そしてそれらのストリームを本当に有効にする方法があるのかを突き止める時です。

それを念頭に置いて、まず binwalk -Me を使用してファームウェアイメージを抽出しましょう。

10進数16進数説明
1139920x1BD48CRC32多項式テーブル、リトルエンディアン
4587520x70000uImageファームウェアイメージ、ヘッダーサイズ:64バイト、データサイズ:2803328バイト、圧縮:なし、CPU:ARM、OS:Linux、イメージタイプ:OS、カーネルイメージ、ロードアドレス:0x81C08000、エントリポイント:0x81C08040、作成日時:2021-06-21 03:03:42、イメージ名:「Linux-3.4.35」
36044800x370000CramFSファイルシステム、リトルエンディアン、3ファイル、合計サイズ:3248128バイト
77365240x760CCCJFFS2ファイルシステム、リトルエンディアン、ノード:256、合計サイズ:648000バイト

OK、Linux 3.4.35 が動作しているようです。それはいつリリースされたのかもう一度確認しましょうか? 2013年3月。13年前のカーネル!(コンパイル当時は8年前だったのでしょう。)

前回、ONVIF設定を含むTuya設定ファイル ./extractions/cctv.bin.extracted/760CCC/jffs2-root/tuya_config.json を見つけました。おそらくうまくいかなかった理由は、ファームウェアイメージ自体を16進エディタで編集し、その過程でチェックサムを破損させたからだと思います。

しかし、イメージの抽出はできたので、設定オプションがどこで使用されているかをトレースしてみませんか?もしかしたら、バイナリはそのオプションをまったく使用していないかもしれません。

では、どのバイナリが起動されるのでしょうか?カメラの初期化スクリプト .\extractions\cctv.bin.extracted\370000\initrun.sh を確認する必要があります。そのスクリプトは app tarball を展開し、次に home/init.d/initS のスクリプトを実行し、それが home/init.d 内の他の初期化スクリプトを実行します。

ほとんどのスクリプトは、メモリ、ネットワーク、DHCPなどに関するカーネルパラメータの設定のようなものですので、それらはスキップします。重要な部分は最後のスクリプト、S60ppsapp で、これは /home/app の ppsapp を実行します。

find コマンドを1回実行すると、extractions\cctv.bin.extracted\370000\app.tar.gz.extracted\0\decompressed.bin.extracted\0\home\app\ppsapp が見つかります。そして strings は、ONVIF関連のものが含まれていることを示しています。

$ strings ppsapp | grep -C 2 "onvif_enable"
motion_area_switch
motion_area
onvif_enable
onvif_pwd

OK、しかしバイナリ自体にONVIFストリーミングへの参照はありますか?

$ strings ppsapp | grep "Starting onvif"
Starting onvif server on port %s
$ strings ppsapp | grep "rtsp://"
rtsp://
rtsp://%s:%d/Streaming/Channels/101
rtsp://%s:%d/Streaming/Channels/102

OK、それは良いことです!ONVIFストリーミングURLの形式はわかりましたが、ポート番号はまだ知る必要があります。

そこで、バイナリ自体を Ghidra に投入してアナライザーを実行してみましょう。文字列を見ると、ハードコードされたパスの多くは /var/lib/jenkins/workspace/ を参照しており、誰かがこのイメージを Jenkins パイプラインでビルドしたようです。私たちがやっていることにとって本当に重要ですか?いいえ、しかし興味深いと思いました。彼らはおそらく、出荷する各カメラSKUのために Jenkins ワークフローを持っているでしょうが、サポートは後回しなので、おそらく一度しか使用しないでしょう。

バイナリ自体は2.4MBで、ARMアセンブリの読み取りにはあまり慣れていませんが、まだそこまでする必要はありません!設定文字列がそこにあることを知っているので、ONVIFの部分を探すだけで済みます。

Ghidra で文字列を見つけるには、「Search -> For Strings…」に移動します。オプションを選択すると、文字列検索ウィンドウが表示されます。幸いなことに、onvif_enable でフィルタリングすると、2つの場所しか見つかりません。

場所1(警告:長い)

undefined4 FUN_0000f3c0(void)
{
  bool bVar1;
  FILE *__stream;
  byte *__s;
  size_t sVar2;
  undefined4 *puVar3;
  undefined4 *puVar4;
  undefined4 uVar5;
  void *__src;
  
  __stream = fopen64("/home/cfg/tuya_config.json","r+");
  if (__stream == (FILE *)0x0) {
    uVar5 = 0xffffffff;
  }
  else {
    __s = malloc(0x800);
    if (__s == (byte *)0x0) {
      fclose(__stream);
      uVar5 = 0xffffffff;
    }
    else {
      memset(__s,0,0x800);
      sVar2 = fread(__s,1,0x800,__stream);
      if (sVar2 == 0) {
        fclose(__stream);
        free(__s);
        uVar5 = 0xffffffff;
      }
      else {
        fclose(__stream);
        puVar3 = FUN_001f5cfc(__s);
        if (puVar3 == (undefined4 *)0x0) {
          uVar5 = 0xffffffff;
        }
        else {
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"version");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d58 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"sleep_mode");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d5c = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"light_onoff");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d60 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"flip_onoff");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d64 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"watermark_onoff");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d68 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"night_mode");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d6c = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"alarm_fun_onoff");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d70 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"alarm_fun_sensitivity");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d74 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"alarm_fun_mode_switch");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d78 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"alarm_fun_time_start");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d7c = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"alarm_fun_time_end");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d80 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"loudspeaker_vol_pct");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279efc = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"sound_detect_onoff");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d84 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"sound_detect_sensitivity");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d88 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"event_record_time");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d90 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"enable_event_record");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d8c = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"record_enable");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279d94 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"motion_trace");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279f44 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"motion_tracking");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279ec0 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"cry_detection_switch");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279ec4 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"humanoid_filter");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279ec8 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          bVar1 = FUN_00192cf8();
          if (bVar1) {
            puVar4 = FUN_001f7978((int)puVar3,(byte *)"jingle_mode");
            if (puVar4 != (undefined4 *)0x0) {
              DAT_00279da0 = FUN_00225a88(puVar4[6],puVar4[7]);
            }
            puVar4 = FUN_001f7978((int)puVar3,(byte *)"jingle_sound");
            if (puVar4 != (undefined4 *)0x0) {
              DAT_00279d9c = FUN_00225a88(puVar4[6],puVar4[7]);
            }
            puVar4 = FUN_001f7978((int)puVar3,(byte *)"jingle_volume");
            if (puVar4 != (undefined4 *)0x0) {
              DAT_00279d98 = FUN_00225a88(puVar4[6],puVar4[7]);
            }
            puVar4 = FUN_001f7978((int)puVar3,(byte *)"jingle_exist");
            if (puVar4 != (undefined4 *)0x0) {
              DAT_00279da4 = FUN_00225a88(puVar4[6],puVar4[7]);
            }
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"motion_area_switch");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279da8 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"motion_area");
          if (puVar4 != (undefined4 *)0x0) {
            memset(&DAT_00279dac,0,0x100);
            __src = (void *)puVar4[4];
            sVar2 = strlen((char *)puVar4[4]);
            memcpy(&DAT_00279dac,__src,sVar2);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"onvif_enable");
          if (puVar4 != (undefined4 *)0x0) {
            DAT_00279f00 = FUN_00225a88(puVar4[6],puVar4[7]);
          }
          puVar4 = FUN_001f7978((int)puVar3,(byte *)"onvif_pwd");
          if (puVar4 != (undefined4 *)0x0) {
            memset(&DAT_00279f04,0,0x40);
            strcpy((char *)&DAT_00279f04,(char *)puVar4[4]);
            sVar2 = strlen((char *)&DAT_00279f04);
            FUN_00041ba0(&DAT_00279f04,sVar2);
          }
          if (puVar3 != (undefined4 *)0x0) {
            FUN_001f4480(puVar3);
          }
          free(__s);
          uVar5 = 0;
        }
      }
    }
  }
  return uVar5;
}

そして

場所2(警告:これもやや長い)

undefined4 FUN_0000fd5c(void)
{
  bool bVar1;
  undefined4 *puVar2;
  void *pvVar3;
  char *__s;
  FILE *__stream;
  undefined4 uVar4;
  ulonglong uVar5;
  
  puVar2 = FUN_001f8384();
  if (puVar2 == (undefined4 *)0x0) {
    uVar4 = 0xffffffff;
  }
  else {
    uVar5 = FUN_00225410(DAT_00279d58);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"version",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d5c);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"sleep_mode",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d70);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"alarm_fun_onoff",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d74);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"alarm_fun_sensitivity",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d78);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"alarm_fun_mode_switch",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d7c);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"alarm_fun_time_start",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d80);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"alarm_fun_time_end",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d64);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"flip_onoff",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d60);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"light_onoff",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d6c);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"night_mode",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d84);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"sound_detect_onoff",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d88);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"sound_detect_sensitivity",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d68);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"watermark_onoff",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d90);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"event_record_time",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d8c);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"enable_event_record",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279d94);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"record_enable",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279f44);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"motion_trace",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279da8);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"motion_area_switch",(int)pvVar3);
    pvVar3 = FUN_001f82f4(&DAT_00279dac);
    FUN_001f7b54((int)puVar2,"motion_area",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279ec0);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"motion_tracking",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279ec4);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"cry_detection_switch",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279ec8);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"humanoid_filter",(int)pvVar3);
    uVar5 = FUN_00225410(DAT_00279efc);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"loudspeaker_vol_pct",(int)pvVar3);
    bVar1 = FUN_00192cf8();
    if (bVar1) {
      uVar5 = FUN_00225410(DAT_00279da0);
      pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
      FUN_001f7b54((int)puVar2,"jingle_mode",(int)pvVar3);
      uVar5 = FUN_00225410(DAT_00279d9c);
      pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
      FUN_001f7b54((int)puVar2,"jingle_sound",(int)pvVar3);
      uVar5 = FUN_00225410(DAT_00279d98);
      pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
      FUN_001f7b54((int)puVar2,"jingle_volume",(int)pvVar3);
      uVar5 = FUN_00225410(DAT_00279da4);
      pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
      FUN_001f7b54((int)puVar2,"jingle_exist",(int)pvVar3);
    }
    uVar5 = FUN_00225410(DAT_00279f00);
    pvVar3 = FUN_001f8294((uint)uVar5,(uint)(uVar5 >> 0x20));
    FUN_001f7b54((int)puVar2,"onvif_enable",(int)pvVar3);
    pvVar3 = FUN_001f82f4((char *)&DAT_00279f04);
    FUN_001f7b54((int)puVar2,"onvif_pwd",(int)pvVar3);
    __s = FUN_001f5d2c((int)puVar2);
    if (__s != (char *)0x0) {
      __stream = fopen64("/home/cfg/tuya_config.json","w+");
      if (__stream == (FILE *)0x0) {
        free(__s);
        return 0xffffffff;
      }
      fputs(__s,__stream);
      fclose(__stream);
      free(__s);
    }
    FUN_001f4480(puVar2);
    uVar4 = 0;
  }
  return uVar4;
}

…まあ、読むのはあまり楽しくありません。しかし、推論するだけでいくつかのことを理解できます。

まず、設定ファイル tuya_config.json を開くと、最初の関数の呼び出しが fopen() で、2番目のパラメータが r+ であることに注意してください。一方、2番目の関数では、同じ fopen() 呼び出しが関数の最後に w+ で行われています。これは、最初の関数が設定リーダーであり、2番目の関数が設定ライターであることを示しています!

OK、そして FUN_001f7978 という関数への呼び出しがたくさんあります。それは何でしょうか?

undefined4 * FUN_001f7978(int param_1,byte *param_2)
{
  uint uVar1;
  undefined4 *local_c;
  
  if (param_1 == 0) {
    local_c = (undefined4 *)0x0;
  }
  else {
    local_c = *(undefined4 **)(param_1 + 8);
    while ((local_c != (undefined4 *)0x0 &&
           (uVar1 = FUN_001f4230((byte *)local_c[8],param_2), uVar1 != 0))) {
      local_c = (undefined4 *)*local_c;
    }
  }
  return local_c;
}

では、FUN_001f4230 は何でしょうか?


uint FUN_001f4230(byte *param_1,byte *param_2)
{
  int iVar1;
  int iVar2;
  uint uVar3;
  byte *local_18;
  byte *local_14;
  
  if (param_1 == (byte *)0x0) {
    uVar3 = (uint)(param_2 != (byte *)0x0);
  }
  else {
    local_18 = param_2;
    local_14 = param_1;
    if (param_2 == (byte *)0x0) {
      uVar3 = 1;
    }
    else {
      while( true ) {
        iVar1 = tolower((uint)*local_14);
        iVar2 = tolower((uint)*local_18);
        if (iVar1 != iVar2) break;
        if (*local_14 == 0) {
          return 0;
        }
        local_14 = local_14 + 1;
        local_18 = local_18 + 1;
      }
      iVar1 = tolower((uint)*local_14);
      iVar2 = tolower((uint)*local_18);
      uVar3 = iVar1 - iVar2;
    }
  }
  return uVar3;
}

ふむ、少し名前を付け替えると:

uint FUN_001f4230(byte *s1,byte *s2)
{
  int c1;
  int c2;
  uint retVal;
  byte *local_18;
  byte *local_14;
  
  if (s1 == (byte *)NULL) {
    retVal = (uint)(s2 != (byte *)NULL);
  }
  else {
    local_18 = s2;
    local_14 = s1;
    if (s2 == (byte *)NULL) {
      retVal = 1;
    }
    else {
      while( true ) {
        c1 = tolower((uint)*local_14);
        c2 = tolower((uint)*local_18);
        if (c1 != c2) break;
        if (*local_14 == 0) {
          return 0;
        }
        local_14 = local_14 + 1;
        local_18 = local_18 + 1;
      }
      c1 = tolower((uint)*local_14);
      c2 = tolower((uint)*local_18);
      retVal = c1 - c2;
    }
  }
  return retVal;
}

これは単なる strcasecmp です! glibc のものとは異なり、null チェックを行うので strcasecmp_nc と呼びます。1つ上のレイヤーに戻り、少し名前を付け替えると、次のようになります。

undefined4 * FUN_001f7978(int config_data,byte *config_key)
{
  uint uVar1;
  undefined4 *retVal;
  
  if (config_data == 0) {
    retVal = (undefined4 *)NULL;
  }
  else {
    retVal = *(undefined4 **)(config_data + 8);
    while ((retVal != (undefined4 *)NULL &&
           (uVar1 = strcasecmp_nc((byte *)retVal[8],config_key), uVar1 != 0))) {
      retVal = (undefined4 *)*retVal;
    }
  }
  return retVal;
}

OK、つまりこの関数は、与えられた設定データから特定のキーと値のペアを探すだけで、配列/リンクリスト構造を反復処理してこれを行います。この関数の仕組みの全体像を把握したので、find_config と名前を付け、1つ上のレイヤーに進みます。

FUN_00225a88 は何でしょうか?

uint FUN_00225a88(uint param_1,uint param_2)
{
  uint retVal;
  int iVar1;
  uint uVar2;
  
  iVar1 = param_2 * 2 + 0x200000;
  if (param_2 * 2 < 0xffe00000) {
    if (-1 < iVar1) {
      return 0;
    }
    retVal = iVar1 >> 0x15;
    uVar2 = -retVal - 0x3e1;
    if (retVal < 0xfffffc20 && uVar2 != 0) {
      retVal = (param_2 << 0xb | 0x80000000 | param_1 >> 0x15) >> (uVar2 & 0xff);
      if ((param_2 & 0x80000000) != 0) {
        retVal = -retVal;
      }
      return retVal;
    }
  }
  else if (param_1 != 0 || (param_2 & 0xfffff) != 0) {
    return 0;
  }
  retVal = param_2 & 0x80000000;
  if (retVal == 0) {
    retVal = 0x7fffffff;
  }
  return retVal;
}

うわー。定数とマジックナンバーが何なのか全くわかりません。しかし、LLMならわかるかもしれません。そして、わかりました!

この関数は、IEEE 754 倍精度浮動小数点数を符号付き32ビット整数に変換します。

ああ、なるほど、それは文字通り f64 から i32 への変換ですが、逆コンパイルされた C で行われています。それは役に立ちました。

では、もう一度呼び出しを見てみましょう。

raw_config_value = find_config((int)config_data,(byte *)"onvif_enable");
if (raw_config_value != (undefined4 *)NULL) {
  DAT_00279f00 = f64_to_i32(raw_config_value[6],raw_config_value[7]);
}
raw_config_value = find_config((int)config_data,(byte *)"onvif_pwd");
if (raw_config_value != (undefined4 *)NULL) {
  memset(&DAT_00279f04,0,0x40);
  strcpy((char *)&DAT_00279f04,(char *)raw_config_value[4]);
  config_file_size = strlen((char *)&DAT_00279f04);
  FUN_00041ba0(&DAT_00279f04,config_file_size);
}

それは見るのがずっと良くなりました!つまり、すべてはONVIFキーと値のペアを設定ファイルから読み取るだけでした。次に DAT_00279f00 がどこで使用されているかを確認する必要がありますが、それを ONVIF_ENABLE とリネームすることにします。

ONVIF_ENABLE への参照を検索すると、10個の場所が見つかります。

[画像: ONVIF_ENABLE が参照されている場所]

しかし、バイナリが ONVIF 機能の有効化をチェックしていると思われる READ コンテキストに焦点を当てることができます。以前の write_configuration() 関数内の参照を除外すると、2つの呼び出しサイトが残ります。

int FUN_00010a98(void)
{
  int iVar1;
  
  FUN_00041304(ONVIF_ENABLE);
  FUN_000788f8(ONVIF_ENABLE);
  iVar1 = FUN_0001118c(0xff,ONVIF_ENABLE);
  return iVar1;
}

int FUN_00013480(int param_1)
{
  int iVar1;
  
  if (param_1 == 0) {
    iVar1 = 0;
  }
  else {
    ONVIF_ENABLE = *(int *)(param_1 + 4);
    FUN_00041304(ONVIF_ENABLE);
    FUN_000788f8(ONVIF_ENABLE);
    write_configuration();
    iVar1 = FUN_0001118c(0xff,ONVIF_ENABLE);
  }
  return iVar1;
}

すぐに、関数 FUN_00013480() の write_configuration() への呼び出しのため、これは ONVIF 機能がカメラ実行中に変更されたときに呼び出される関数であることがわかります。最初の関数 FUN_00010a98() は、もう少し興味深いです。それが何を呼び出しているか見てみましょう。

int FUN_00041304(int onvif_enable)
{
  int iVar1;
  
  pthread_mutex_lock((pthread_mutex_t *)&DAT_0027b238);
  DAT_0027b1f4 = (uint)(onvif_enable != 0);
  iVar1 = pthread_mutex_unlock((pthread_mutex_t *)&DAT_0027b238);
  return iVar1;
}

int FUN_000788f8(int onvif_enable)
{
  DAT_0030c4cc = (uint)(onvif_enable != 0);
  return onvif_enable;
}

奇妙なことに、ONVIFフラグが設定されているデータ場所が2つあります。そのため、次のようにマッピングします。

  • DAT_0030c4cc → ONVIF_ENABLED_1
  • DAT_0027b1f4 → ONVIF_ENABLED_2

残念ながら、ここで壁にぶつかりました。なぜなら、それらの変数の両方が、異なる変数を更新し続ける多くの関数に渡されたからです。

また、「Starting onvif server on port %s」という文字列も調べてみたいと思います。なぜなら、私の(かなり無学な)推測では、ONVIFサーバーが開始される場所だからです。(神よ、私の脳は時々本当に機能します)

void FUN_00040794(void)
{
  pthread_t __th;
  int iVar1;
  undefined4 auStack_28 [6];
  int *local_10;
  char *onvif_server_port_num_s;
  
  __th = pthread_self();
  pthread_detach(__th);
  FUN_001fae18(0x2293c8);
  onvif_server_port_num_s = "8000";
  while( true ) {
    while (iVar1 = FUN_00041358(), iVar1 != 1) {
      sleep(1);
    }
    printf("Starting onvif server on port %s\n",onvif_server_port_num_s);
    FUN_0020f62c(auStack_28,0);
    local_10 = FUN_002106bc(auStack_28,onvif_server_port_num_s,FUN_0007893c);
    if (local_10 == (int *)0x0) break;
    FUN_002157e0((int)local_10);
    do {
      FUN_00212158(auStack_28,500);
      iVar1 = FUN_00041358();
    } while (iVar1 != 0);
    FUN_0020f6e8(auStack_28);
  }
  FUN_0020f6e8(auStack_28);
  return;
}

ああ、素晴らしい、だからポート 8000 で動作するんですね!そして、前述の ONVIF_ENABLE_* 変数を参照することで、ONVIF を有効にする追加のチェックがないと推測できます。これが最終的な逆アセンブリです。

void check_and_enable_onvif_server(void)
{
  pthread_t __th;
  int is_onvif_enabled;
  undefined4 auStack_28 [6];
  int *local_10;
  char *onvif_server_port_num_s;
  
  __th = pthread_self();
  pthread_detach(__th);
  FUN_001fae18(0x2293c8);
  onvif_server_port_num_s = "8000";
  while( true ) {
    while (is_onvif_enabled = get_onvif_enabled_2(), is_onvif_enabled != 1) {
      sleep(1);
    }
    printf("Starting onvif server on port %s\n",onvif_server_port_num_s);
    FUN_0020f62c(auStack_28,0);
    local_10 = start_onvif_server(auStack_28,onvif_server_port_num_s,FUN_0007893c);
    if (local_10 == (int *)NULL) break;
    FUN_002157e0((int)local_10);
    do {
      FUN_00212158(auStack_28,500);
      is_onvif_enabled = get_onvif_enabled_2();
    } while (is_onvif_enabled != 0);
    FUN_0020f6e8(auStack_28);
  }
  FUN_0020f6e8(auStack_28);
  return;
}

OK、クール。では、設定ファイルを実際に編集して、カメラが受け入れるように再パックするにはどうすればよいでしょうか?

設定ファイル tuya_config.json が ./extractions/cctv.bin.extracted/760CCC/jffs2-root/tuya_config.json にあることはわかっているので、前回のように生のファームウェアイメージを16進編集するという愚かなことをする代わりに、ファイルを編集してJFFS2パーティションイメージを再パックできるかもしれません。

しかし、なぜ16進編集がうまくいかなかったのでしょうか?さて、JFFS2は特別なファイルシステムで、ログ構造化されており、フラッシュメモリデバイス用に設計されています。これはF2FSに似ているかもしれませんが、Androidデバイスでの使用とは異なり、JFFS2はハードウェア側にウェアレベリングメカニズムを持たないフラッシュメモリ用に設計されています。

ウェアレベリングとは何ですか? 要するに:フラッシュチップとSSDにはフラッシュセルがあります。フラッシュセルには寿命があり、繰り返し書き込むことを好みません。したがって、ウェアレベリングメカニズムは、寿命を延ばすために書き込みがフラッシュセル全体に均等に分散されるようにしようとします。

フラッシュメモリはブロック単位でしかアドレス指定できないため、JFFS2はウェアレベリングメカニズムをソフトウェアで処理する必要があるため、ブロック内の1バイトを変更するだけで古いブロックを上書きすることはできません(この場合、設定オプション)。JFFS2は、より高いバージョン番号を持つ別のブロックをフラッシュメモリに書き込みます。

そして、すべての優れたファイルシステムと同様に、JFFS2はエラー検出のためにチェックサムを使用します。そして、私が把握したところでは、32ビットCRCを使用しています。これは、データ部分だけを編集すると、そのブロック全体が破損することを意味します。

前回 onvif_enable のすべての出現箇所を変更したかどうかは覚えていませんが、どちらにしても重要ではありません。指定されたブロックが破損している場合、JFFS2はより低いバージョン番号を持つ古いブロックに戻ります。そして、すべてのブロックを変更して破損させた場合、ppsapp バイナリは健全なシステムデフォルトにフォールバックしたと推測されます。それが、起動中にチャイム音が聞こえた理由かもしれません。

イメージ内のブロックの出現箇所を grep で確認できます。

$ grep -b -a -n "onvif_enable" extractions/cctv.bin
22803:7849741:  "onvif_enable": 0,
22857:7971273:  "onvif_enable": 0,
22932:7997857:  "onvif_enable": 0,
23041:8064473:  "onvif_enable": 0,
23169:8197025:  "onvif_enable": 0,
23195:8197893:  "onvif_enable": 0,
23227:8205921:  "onvif_enable": 0,
23315:8219209:  "onvif_enable": 0,

OK、OK、その失敗分析はすべて楽しくて良いですが、このブログ記事の終わりまでに動作するカメラが必要です。独自のJFFS2イメージの作成に戻りましょう。

cp -r extractions/cctv.bin.extracted/760CCC/jffs2-root jffs2-root_modified

次に jffs2-root_modified/tuya_config.json で onvif_enable を 1 に編集します。

最後に、JFFS2イメージを再パックします。mkfs.jffs2 コールを発行するときは、ファイルがリトルエンディアン(Anyka AK3918のため)であり、消去ブロックサイズが4KB(0x1000)、イメージサイズが640KB(0xA0000)であることを念頭に置く必要があります。元のイメージ全体は0x800000で、JFFS2イメージは0x760000から始まります。元のイメージは grep 可能だったので、圧縮もしたくありません。

mkfs.jffs2 -d jffs2-root_modified -m none -e 0x1000 -p 0xA0000 -l --pad=0xA0000 -o new_jffs2.bin

残念ながら、これは次の警告を表示します。

Erase size 0x1000 too small. Increasing to 8KiB minimum

それは私たちにとって問題です。なぜなら、元のイメージは4KBの消去ブロックサイズを使用していたからです。Linuxメーリングリストを調べていると、このスレッドで、誰かがサイズチェックを削除するために mkfs.jffs2 をパッチしようとして、うまくいったという情報を見つけました。

しかし、それは本当に奇妙です。なぜなら、チェックがいつ追加されたのか気になって調べたところ、ユーティリティが最初に作成された時から存在していたことがわかったからです。mtd-utilsのGit履歴とblameを調べると、チェックは最初のコミット(2006年4月に行われた)から存在していたようです。

Tuyaがこの値をわざわざ 0x1000 に変更したというのは奇妙に思えましたが、もう少し調査したところ公式OpenWrtリポジトリにパッチがあるのを見つけました。彼らもサイズチェックを 0x1000 にパッチしています。なので、組み込みシステムでは一般的であるようです!

いずれにしても、自分で同じことができます。

git clone git://git.infradead.org/mtd-utils.git
cd mtd-utils
git checkout v2.3.1

次にこのパッチをダウンロードして適用します。

index da07b69..9a60129 100644
--- a/jffsX-utils/mkfs.jffs2.c
+++ b/jffsX-utils/mkfs.jffs2.c
@@ -1668,11 +1668,11 @@ int main(int argc, char **argv)
                                                  }
                                                  erase_block_size *= units;

-                                                 /* If it's less than 8KiB, they're not allowed */
-                                                 if (erase_block_size < 0x2000) {
-                                                         fprintf(stderr, "Erase size 0x%x too small. Increasing to 8KiB minimum\n",
+                                                 /* If it's less than 4KiB, they're not allowed */
+                                                 if (erase_block_size < 0x1000) {
+                                                         fprintf(stderr, "Erase size 0x%x too small. Increasing to 4KiB minimum\n",
                                                                          erase_block_size);
-                                                         erase_block_size = 0x2000;
+                                                         erase_block_size = 0x1000;
                                                  }
                                                  break;
                                          }

これで mkfs.jffs2 をビルドできます。

./autogen.sh
./configure --without-ubifs
make -j$(nproc) mkfs.jffs2

これで外に出て、新しい mkfs.jffs2 バイナリを使用してイメージを作成できます。

cd ..
./mtd-utils/mkfs.jffs2 -d jffs2-root_modified -m none -e 0x1000 -p 0xA0000 -l --pad=0xA0000 -o new_jffs2.bin

そして元のファームウェアイメージに注入します。

cp extractions/cctv.bin cctv_modified.bin
dd if=new_jffs2.bin of=cctv_modified.bin bs=1 seek=$((0x760000)) conv=notrunc

ヤッター、これで新しいイメージをフラッシュするだけです。CH341A を用意し、flashrom でフラッシュし、Wi-Fi に接続して…

動作しません。

OK、それはイライラします。ONVIFサーバーを有効にする関数が、Ghidra によるとどこにも参照されていないことがわかりました。RTSPストリームを設定する関数も同様です。

おそらく、呼び出されないコードをリバースエンジニアリングしていたのかもしれません。

しかし、まあ、このデバイスには何でもフラッシュできますよね?初期化ステップを編集して telnetd を実行してみましょう。

./extractions/cctv.bin.extracted/370000/initrun.sh:

#!/bin/sh

BASE_DIR=/opt/pps
if [ -e /tmp/PPStrong.runpath ]; then
        BASE_DIR=`cat /tmp/PPStrong.runpath`
fi

if [ ! -e ${BASE_DIR} ]; then
        exit
fi

tar xzf ${BASE_DIR}/app.tar.gz -C /

umount ${BASE_DIR}

# Run telnet
telnetd -l /bin/sh &

home/init.d/initS &

イメージを再パックします。

cd ./extractions/cctv.bin.extracted/370000/
mkdir tmp
cp initrun.sh tmp
cp app.tar.gz tmp
mkfs.cramfs tmp cramfs_new.bin

そしてファームウェアイメージに注入します。

dd if=cramfs_new.bin of=cctv.bin bs=1 seek=$((0x370000)) conv=notrunc

そしてフラッシュします。動作しますか?

> nmap -p 23 192.168.8.11
Starting Nmap 7.98 ( https://nmap.org ) at 2026-09-13 16:24 -0400
Nmap scan report for 192.168.8.11
Host is up (0.038s latency).

PORT     STATE  SERVICE
23/tcp   closed telnet
MAC Address: B4:FB:E3:C2:11:BC (AltoBeam (China))

Nmap done: 1 IP address (1 host up) scanned in 2.26 seconds

なぜ?!

カメラに組み込まれている busybox バイナリには telnetd が含まれていないことが判明しました。

この時点で、このカメラに誰かが遭遇したことがあるかどうか、オンラインで検索することにしました。Tuyaは巨大な製品エコシステムであり、私がやる前に誰かがこれを行ったに違いありません。

そして、あります。

したがって、これを読んでいる方は、@guino さん、ありがとうございます。

ppsapp バイナリに戻り、彼がやったことと全く同じように、onvif_enable に関連するすべての呼び出しを 1 にパッチすることにしました。しかし、楽しみのために、onvif_enable 変数のすべてのチェックをパッチアウトして、常に無条件でONVIFサーバーを起動するようにすることにしました。すべての命令が nop に変わるのを見ると、本当に満足感があります。

これを元のファームウェアに再パッケージ化するのは、すでに大変だったことよりもさらに面倒です。なぜなら、ppsapp は app.tar.gz の中にあり、それはさらに CramFS パーティションの中にあり、それはさらに元の cctv.bin ファームウェアの中にあります。だから、私は3回パッケージ化しなければならないようです。

(変更されたバイナリを以下のディレクトリにコピーしたと仮定します…)

cd ./extractions/new_wifi_mod.bin.extracted/370000/app.tar.gz.extracted/0/decompressed.bin.extracted.modified/0
tar -czf ../../../../new_app.tar.gz bin etc home
cd ../../../../
mkdir tmp
cp new_app.tar.gz tmp/app.tar.gz
cp initrun.sh tmp
mkfs.cramfs tmp new_cramfs.bin
cd ../../../
cp new_wifi_mod.bin third_try.bin
dd if=new_cramfs.bin of=third_try.bin bs=1 seek=$((0x370000)) conv=notrunc

残念ながら、このイメージをフラッシュした後、カメラは調子が悪くなり、Wi-Fiに接続できなくなり、青いLEDが点滅し続けました。これは通常の点灯状態とは異なる変化でした。

数週間(はい、数週間!)壁に頭をぶつけ、あらゆる種類の迂回路を試しました。基板上のUARTパッドをいくつか見つけ、シリアルモニターを接続するのが価値があるかもしれないと思いましたが、コンソールには何も出力されませんでした。

ひらめきでマルチメーターを取り出し、パッドで3.3Vを読み取ったとき、私の心臓は止まりそうになりました。Arduino Uno は5Vで動作していました。しまった。

幸いなことに、ボードを壊してはいませんでした。なぜなら、パッドが本当にUARTパッドかどうかを確認するためにUSBロジックアナライザーを取り出したからです。そして、Pulseview が示すように、それらはUARTパッドでした。

[画像: Pulseview ダンプ画面]

そこで、複数の電圧レベルに対応できる適切なUSB-TTLアダプターを購入しました。ブート出力の何かが初期化が失敗した理由を教えてくれることを期待していました。

[画像: UARTパッドにはんだ付けされたケーブル]

…ええ、これはグランドとしてネジとパッドを使用しているところです。最後のパッドがグランドだったのですが、はんだがうまく乗らなかったからです。幸い、TX/RXパッドではなく、そのパッドだったのが幸いです。

しかし、カメラを起動したとき、私の希望は打ち砕かれました。次の出力の後、それはただ…停止しました。

U-Boot 2013.10.0-AK_V3.0.07 (Jan 22 2021 - 14:16:01) 

DRAM:  64 MiB
8 MiB
ANYKA SDHC/MMC4.0: 0
PPS:Jan 22 2021 14:16:03   anyka_c3:  3     2     1     0 
magic err
magic err
command:setenv bootargs mem=64M console=ttySAK0,115200n8 loglevel=10   mtdparts=spi0.0:256k(bld),64k(env),64k(enc),64k(sysflg),3m(sys),4032k(app),640k(cfg) ppsAppParts=5 ip=192.168.1.99:::255.255.255.0 eth=00:55:7b:b5:7d:f7
## Booting kernel from Legacy Image at 81c08000 ...
   Image Name:   Linux-3.4.35
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    2803328 Bytes = 2.7 MiB
   Load Address: 81c08000
   Entry Point:  81c08040
   Verifying Checksum ... OK
   XIP Kernel Image ... OK

Starting kernel ...

Uncompressing Linux... done, booting the kernel.
Anyka Linux Kernel Version: 1.1.12

カメラの電源を入れ直し、Enter キーを押すと、パスワードプロンプトが表示されました。

ANYKA SDHC/MMC4.0: 0
PPS:Jan 22 2021 14:16:03   anyka_c3:  3     2     0 

please input password::

ふむ。ここで本当に何ができるでしょうか?

いくつかのパスワードを推測しましたが、すべて間違っていました。毎回、ボードはリセットされて最初からやり直されました。

ファームウェア抽出の U-Boot 領域を調べて、パスワードがどのように比較され、メモリのどこに保存されているかを理解しようと考えました。しかし、その部分はあまり興味がないことに気づいたので、LLM に与えてみることにしました。

Claude はすぐに拒否しました。

[画像: Claude が拒否]

AI バブルが弾けるのは、企業がモデルの使用方法について、あまりにも責任感があり、純粋で、高慢である場合、非常に面白いでしょう。

わかりました。米国が助けてくれないなら、中国が助けてくれるでしょう。私は Claude サブスクリプションの返金を申請しました。それはわずか40秒しか持たなかった後、OpenRouter に行き、アカウントを作成し、クレジットをロードしてから API キーを作成しました。次に、それを OpenCode に接続し、Deepseek V4.1 Flash 1017 を選択し、ファームウェアバイナリファイルがある場所と、U-Boot が吐き出した特定のパスワード文字列を伝えました。

約3分間の検索の後、それは期待を裏切りませんでした。

はい — ファームウェアダンプにあります。

U-Boot パスワード:pps_password

試してみると、うまくいきました。U-Boot 環境に入ることができました!

OK、しかしこれはズルい気がします。正直に言うと、そうです。LLM にこのタスクを任せることで、どのような学習機会を逃したのか気になったので、「思考」部分を拡張して、それがその出力を得るために何をしたかを確認しました。

認証ルーチンは 0x80E07000 にあります。これは please input password:: と表示し、入力を 0x80E244A8(ファイルオフセット 0x246A8)にある文字列と比較します。

その文字列はプレーンテキストとしては保存されていません。ビット単位の NOT で難読化されています。生のバイト:8f 8f 8c a0 8f 9e 8c 8c 88 90 8d 9b ff

反転すると:`pps_password

` になります。

OK、これはおそらく私が手に入れたかもしれませんが、そうではないかもしれません? LLM なしでは、もっと多くの頭を壁にぶつけるセッションなしでは、難読化の部分を得られたかどうかはわかりません。しかし個人的には、その部分はあまり興味がありません。興味があるのは、このカメラを私に話させることです。

別のLLMプロンプトの後、Deepseek はカーネルブート引数に ignore_loglevel printk.time=1 ppsdebug=onshow を追加するように指示しました。これにより、ppsapp バイナリがより多くのログ出力を提供するようになります。OK、承知しました!

loadk
printenv bootargs
setenv bootargs `mem=64M console=ttySAK0,115200n8 ignore_loglevel printk.time=1 ppsdebug=onshow`
bootm 0x81C08000

(loadk が console=ttySAK0,115200n8 行の後にさらに多くのパラメータを追加することに注意してください。フラッシュメモリのパーティションレイアウトなどです。それらをコピーして ppsdebug=onshow の後に追加する必要があります。実際、U-Boot がコマンドが長すぎると文句を言ったため、log_level パラメータを完全に削除する必要がありました。)

そして、より多くのログ出力が得られました。Wi-Fi 認証を行っている行を見ました。

wpa_supplicant service ... [stop] ok
wpa_supplicant service ... [start]
[   21.676261] [atbm_log]:br0_netdev_open()-1116: dev_get_by_name(br0) failed2!wpa_supplicant: can't load library 'libnl-3.so.200'
wpa_supplicant service ... [start] ok
osd set init ok--------------->ak_isp_set_user_params_do 1458, set main osd mem attr

[   22.730306] ak_isp_set_user_params_do 1473 chn:0, paddr:80dbd2a0 size:50400 
[01-01 18:12:17:845 TUYA Err][gw_intf.c:598] wf_station_connect error:-1
[01-01 18:12:17:845 TUYA Notice][gw_intf.c:466] stop smt cfg mthd:0
chan:0  set rect ok x:60,y:60,w:[   22.757244] ak_isp_set_user_params_do 1509 paddr:80dca280 size:5600 
840,h:60,_,w:1920,h:1080
[   22.774108] [atbm_log]:ieee80211_netdev_ioctrl:cmd err
[   22.779634] [atbm_log]:ieee80211_netdev_ioctrl:cmd err

ふむ。ライブラリが見つからない?

最後のイメージを再パックしたときに発行したコマンドを覚えていますか?

tar -czf ../../../../new_app.tar.gz bin etc home

ls を実行したときに、どのようなフォルダがあると思いますか?

[ericswpark@stanley 0]$ ls
bin  etc  home  lib

ええ…しまった。それはなかったことにして、もう一度やり直しましょう。

新しいファームウェアイメージをフラッシュした後、ついに進展がありました!カメラはWi-Fiネットワークに再表示され、ネットワークスキャンでポート 8000 が開いていることが示され、ONVIFサーバー用でした。 @guino が文書化したように admin:admin の認証情報を使用しましたが、サーバーから応答がありませんでした。

オンラインで検索したところ、TuyaカメラはSDKサーバーへのアクセスをブロックすると基本的に動作しなくなることがわかりました。そして確かに、それらが外部世界と通信するのをすべてブロックしていました。

Deepseek にもう一度尋ねて、ppsapp バイナリのどこでオンライン接続チェックが行われているか解明できるか確認することにしました。以下のオフセットが見つかりました。

オフセット前命令前後命令後
0x7898000 00 00 0Abeq 0x7898800 00 00 EAb 0x78988
0x40E0C00 48 2D E9stmdb sp!,{r11,lr}00 00 A0 E3mov r0,#0x0
0x40E1004 B0 8D E2add r11,sp,#0x41E FF 2F E1bx lr

Ghidra で逆アセンブリを見て、これらのパッチがなぜ重要なのかを理解しようとしました。最初のパッチは ONVIF サーバー用で、Ghidra の逆コンパイルコードは、それが関数 FUN_0007893c の関数本体であることを示していました。パッチを適用すると、逆コンパイルコードの else ブロックが削除されました。これは、Tuya サーバーに到達できなかった場合に実行される部分だったと思われます。

他の2つのパッチは興味深いものでした。それは基本的に関数 FUN_00040E0C の本体全体を次のように変換しました。

undefined4 FUN_00040E0C(void)
{
    return 0;
}

ARM の x86_64 の ret に相当するものが bx lr であることを学んだと思います。

LLM が接続チェックがどのように機能するかについて何を言ったか興味がある場合は、下のクリックでトランスクリプトを開くことができます。

LLM トランスクリプトを読むにはここをクリック

ONVIF HTTP ハンドラは FUN_0007893c(仮想アドレス 0x7893c)です。リクエストを解析または応答する前に、次のことを行います。

if (*ONVIF_ENABLED_1 != 0) {
   if (FUN_00019f50c() != 0) {
       return;              // <-- リクエストをサイレントにドロップします
   }
   if (event != 0x64) return;
   ... handle HTTP / ONVIF ...
}

FUN_00019f50c はグローバルネットワーク/クラウド状態 0x344230 を読み取り、状態 1、2、および4 に対して非ゼロを返します。

  • 0 → 続行
  • 1 → ブロック済み
  • 2 → ブロック済み
  • 3 → 続行
  • 4 → ブロック済み
  • 5 → 続行

その状態は、Wi-Fiとクラウドステータスコールバックによって駆動されるTuya SDKネットワークモニタータスク(FUN_00019fbcc → FUN_00019fa4c)によって維持されます。インターネットがブロックされていると、カメラはIPを取得して8000でリッスンできますが、「準備完了」状態に遷移しないため、ハンドラは1バイトも送信せずに返します。

再パッケージ化して変更されたファームウェアイメージを再度ビルドした後、フラッシュすると、nmap から有望な結果が得られました。

> nmap -p 8000,8554 192.168.8.11
Starting Nmap 7.993 ( https://nmap.org ) at 2026-11-05 21:24 -0500
Nmap scan report for 101846353.lan (192.168.8.11)
Host is up (0.085s latency).

PORT     STATE  SERVICE
8000/tcp open   http-alt
8554/tcp open   rtsp-alt

Nmap done: 1 IP address (1 host up) scanned in 0.21 seconds

そしてRTSPストリームを確認すると…

[画像: カメラからのRTSPストリーム!]

結論

これを試して、たくさん学びました。しかし同時に、結末はそれほど満足のいくものではないかもしれません。

一方では、ARMアセンブリを読むスキルがまだなく、Ghidra を使って手作業でバイナリをリバースエンジニアリングするプロセスを実際に行うことができないため、LLM を松葉杖として使用したように感じます。

他方では、解凍、検索、はんだ付け、パッケージ化、圧縮、フラッシュを行うプロセスと、それに費やされた大量の研究と時間をかけて、多くのことを学びました。このブログ記事は9月の第2週に書き始めました!同時に仕事で非常に忙しかったにもかかわらず、約2ヶ月の作業は無駄ではなかったようです。

また、LLM がこれをよりアクセスしやすくしているのはクールだと思います。そして、すべてをそれにオフロードして、完璧に動作するバイナリファームウェアイメージを得るためにすべてのパッチ適用と再圧縮を行わせる誘惑(はい、それは間違いなく今でも可能です)がありますが、あなたが自分で知っている中間ステップを踏むことをしないように確実にすることで、本当に役立ちます。

また、興味のある方のために:OpenRouter で Deepseek モデルを実行した総コストは22セントでした。はい、$0.22 です。高価なサブスクリプションを購入したり、Altman や Amodei に魂を売ったりする必要はありません。

これをもう一度試しますか?この特定のクラスのカメラについては、そうしません。前回の投稿で述べたように、これは基本的に親戚が無料でくれた電子廃棄物カメラであり、Tuya のようなゴミは買わないように、私たちは皆知っています。そして再び、このくだらないことをしない良いカメラは安価です!私はただ、できるかどうか知りたかっただけです。