はじめに
この記事はATOMCamをhackしているatomcam_toolsの内部処理のうちpropertyコマンドのための解析に関して記述したものです。
atomcam_toolsに関しては下記を参照してください。
kitazakiさんのatomcam_toolsお試し記事も貼っておきます。
目的
最近は自宅のAtomCamをatomcam_toolsとHomeKitで運用していてMobileAppを使うことが殆ど無くなってきています。そこでatomcam_toolsのWebUIからATOMCamのMobileAppのみでできる設定変更も実現できるようにしたいというのがモチベーションです。
実際に取り掛かってみたところ、かなりの難敵だったため忘れないうちに解析内容と最終的な実現方法に辿り着くまでを記述しておこうと思います。
検討と解析
目的の関数を探す
最初の取っ掛かりとしてiCamera_appのlogを見ながらMobileAppから設定を変更してみる。
[protocol.c,3117]session[3], RcvData (Len:63):
[protocol.c,3122]
[protocol.c,3132]=======================================
[protocol.c,3133]session:[3], opcode:[38], version:[3]
[protocol.c,3134]=======================================
[protocol.c,3149]session->protocol_info.encryption:1
1111:[8a11fb0e]1111:[be14b6ca]1111:[7e63165b]1111:[9d9e7ce2]
[protocol.c,2336]Decrypt KeyStr:
[protocol.c,2339][e][protocol.c,2339][fb][protocol.c,2339][11][protocol.c,2339][8a][protocol.c,2339][ca][protocol.c,2339][b6][protocol.c,2339][14][protocol.c,2339][be][protocol.c,2339][5b][protocol.c,2339][16][protocol.c,2339][63][protocol.c,2339][7e][protocol.c,2339][e2][protocol.c,2339][7c][protocol.c,2339][9e][protocol.c,2339][9d][protocol.c,2341]
[protocol.c,2348]Decrypt OutStr in 48 out 41 :
[protocol.c,2368][7b][a][20][20][22][50][72][6f][70][65][72][74][79][4c][69][73][74][22][20][3a][20][7b][a][20][20][20][20][22][33][37][22][20][3a][20][32][a][20][20][7d][a][7d]
[protocol.c,2624]----- p2p recv protocol set property -----
[protocol.c,2625]{"PropertyList":{"37":2}}
[protocol.c,2626]------------------------------------------
send json {"Result":1,"ResultList":{"37":1}}
[protocol.c,3225]session[3], SndData(Len:55, contentLen:40):
[protocol.c,3245][48][4c][49][4f][53][30][41][54][3][27][0][28][0][1][0][1c][38][12][99][b5][b][27][d9][ea][32][6e][45][ac][4d][eb][a6][d3][c9][fd][a7][a9][f9][f5][6f][dc][31][62][8b][80][c6][19][6b][fd][1][bb][a3][1b][36][2c][41]
logの中の実行命令と実行結果のあたりに注目。
固定的な文字列っぽい----- p2p recv protocol set property -----
が出力されてる。
GhydraでiCamera_appを開いて文字列を検索する。
s_[%s,%04d]-----_p2p_recv_protocol_0047e698 XREF[2]: FUN_0043bfdc:0043c060(*),
FUN_0043bfdc:0043c070(*)
0047e698 5b 25 73 ds "[%s,%04d]----- p2p recv protocol set property...
2c 25 30
34 64 5d ...
0047e6cd 00 ?? 00h
この関数を仮に P2P_ReceiveProtocol_Parseと名付ける。
最初の関数の構造を確認
引数の使われ方をアセンブラと合わせて追っていくと、第1引数はすぐに潰されているので未使用。
定義として不自然なのでC++のclassの関数でparam_1はthisか?
第2引数を
iVar1 = FUN_0045c244(param_2);
で使用している。
この関数はエラーメッセージの定義からcJSON_Parseかと思われる。
なので、param_2はJSONの文字列と仮定する。
なので関数としては
int P2P_ReceiveProtocol::P2P_ReceiveProtocol_Parse(char *json_str);
のような定義かと推測する。
この中でJSONで渡されてきた
{"PropertyList":{"37":2}}
を解釈しているらしい。
この関数ではparseだけして実行はしていないので、1つ上の関数を見ていく。
呼び出し元の関数を確認
この関数を呼び出している関数は1つだけのよう。
こちらの関数で何か初期化をしているようなので、読んでいく。
関数の引数は3つ。第1引数はそのまま他の関数呼び出しの第1引数として使われているのでやはりthisか。
第2引数はP2P_ReceiveProtocol_Parseの第2引数なので、char *json_str
と思われる。
第3引数は FUN_0043c25c(param_1,param_3,uVar3);
に使われている。
中を見ていくと、param_3にstrcpyしているので、param_3は文字列でresponseっぽい。
仮にこの関数を下記のように定義する。
void P2P_ReceiveProtocol::ProtocolSetProperty(char *req, char * res);
さらにもう1段上の関数を確認
もう1段、この関数の呼び出し元は無い?途絶えた。
logを出している以上、どこからか呼ばれてるはずなのでprintfに仕込みをいれてstackを確認。
libcallback.soにprintf.cを追加。
printf("[%s,%04d]----- p2p recv protocol set property -----\n", ... );
を実行しているところをtrapして、stackをDumpさせる。
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
extern void Dump(const char *str, void *start, int size);
int printf(const char *fmt, ...) {
unsigned int ra = 0;
asm volatile(
"ori %0, $31, 0\n"
: "=r"(ra)
);
unsigned int sp = 0;
asm volatile(
"ori %0, $29, 0\n"
: "=r"(sp)
);
if(!strcmp(fmt, "[%s,%04d]----- p2p recv protocol set property -----\n")) {
fprintf(stderr, "=== printf ra=%08x sp=%08x\n", ra, sp);
Dump("stack", sp, 0x200);
}
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}
出力結果
=== printf ra=0043c070 sp=61d895f0
Dump 61d895f0 stack
printfのstack(関数の先頭でaddiu sp, sp, -0x40しているので0x40bytes)
61d895f0 : c4 b7 ae 77 f0 95 d8 61 00 02 00 00 f0 95 d8 61 ...w...a.......a
61d89600 : 30 3d b0 77 00 00 00 00 50 af 48 00 70 c0 43 00 0=.w....P.H.p.C.
61d89610 : f0 95 d8 61 00 00 53 00 55 98 53 00 00 00 53 00 ...a..S.U.S...S.
61d89620 : a0 8a cd 00 40 44 53 00 c0 0f d9 61 70 c0 43 00 ....@DS....ap.C.
最後のLittleEndian unsigned intの0x0043c070が呼び出し元でP2P_ReceiveProtocol_Parseの中のprintfの戻りアドレス。
P2P_ReceiveProtocol_Parseのstack(関数の先頭でaddiu sp, sp, -0x40しているので0x40bytes)
61d89630 : 98 e6 47 00 a8 d5 47 00 40 0a 00 00 01 00 00 00 ..G...G.@.......
61d89640 : 00 00 00 00 00 00 00 00 00 00 53 00 55 98 53 00 ..........S.U.S.
61d89650 : 40 44 53 00 d4 6b 4f 00 10 99 53 00 d0 be d8 61 @DS..kO...S....a
61d89660 : 01 c1 53 00 00 00 4f 00 c0 0f d9 61 98 c4 43 00 ..S...O....a..C.
呼び出し元は0x0043c498でProtocolSetPropertyの中のP2P_ReceiveProtocol_Parseの戻りアドレス。
ProtocolSetPropertyのstack()(関数の先頭でaddiu sp, sp, -0x40しているので0x40bytes)
61d89670 : 00 00 00 00 43 00 00 00 00 00 00 00 00 00 00 00 ....C...........
61d89680 : 00 00 00 00 00 00 00 00 55 98 53 00 55 98 53 00 ........U.S.U.S.
61d89690 : 00 00 48 00 00 00 48 00 d4 6b 4f 00 10 99 53 00 ..H...H..kO...S.
61d896a0 : 00 00 48 00 01 c1 53 00 04 00 00 00 2c d5 43 00 ..H...S.....,.C.
呼び出し元は0x0043d52cでprotocol.cのsession RcvData関数っぽい。
t9にアドレスをいれてjalr t9して呼び出している。関数ポインタか。
この関数は先頭でaddiu sp, sp, -0x7880と巨大な領域を確保していて、このstack領域をreq, resのbufferとして10240bytesずつ割り当てている。
61d896b0 : d0 be d8 61 00 00 00 00 00 28 00 00 01 00 00 00 ...a.....(......
61d896c0 : 26 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 &...............
61d896d0 : 7b 0a 20 20 22 50 72 6f 70 65 72 74 79 4c 69 73 {. "PropertyLis
61d896e0 : 74 22 20 3a 20 7b 0a 20 20 20 20 22 34 38 22 20 t" : {. "48"
61d896f0 : 3a 20 32 2c 0a 20 20 20 20 22 31 22 20 3a 20 31 : 2,. "1" : 1
61d89700 : 2c 0a 20 20 20 20 22 32 22 20 3a 20 31 0a 20 20 ,. "2" : 1.
61d89710 : 7d 0a 7d 00 00 00 00 00 00 00 00 00 00 00 00 00 }.}.............
どうも関数ポインタ経由だとGhydraのクロスリファレンスでは確認できないらしい。
0043d518 08 00 79 8e lw t9,0x8(s3)
0043d51c 21 20 00 02 move a0,s0
0043d520 20 00 a5 27 addiu a1,sp,0x20
0043d524 09 f8 20 03 jalr t9
0043d528 20 28 a6 27 _addiu a2,sp,0x2820
0043d52c 0b 00 41 04 bgez v0,LAB_0043d55c
ここのjalr t9
のところ。
ただ、RcvData関数の中ではencrypt/decryptのメッセージもあるので、暗号化されたデータを復号してProtocolSetProperty()を呼んでいるようなので、ProtocolSetProperty()を直接使うようにするのが良さそう。
目的の関数を試してみる
libcallback.soにpropertyコマンドを追加して、試しにここの関数を呼び出すようにしてみる。
ProtocolSetProperty以下の関数を一通り読んでみて、とりあえずthisを模して0x100 + 0x2800 + 0x2800のサイズの領域をmallocで確保、第1引数はthisなのでoffset=0, 第2引数はreqなのでoffset=0x100に文字列{"PropertyList":{"37":2}}
、第3引数はresとしてoffset=0x2900をつけて呼び出してみるとうまく動作してresに{"Result":1,"ResultList":{"37":1}}
と返ってきた。
叩き方は判った。あとは実装するのみ!
最初の試み
解析結果を元に早速実装してみる。
まず、printf.cに呼び出し元の関数の1つ上の関数の先頭アドレスを求めるための仕込みを追加する。
spを取得して関数2つ分のstack sizeを辿ってreturn addressを求める。
そのアドレスから関数の先頭のaddiu sp, sp, -xxxxのコードを遡ることで、ProtocolSetProperty()のアドレスを求められる。
これをglobal変数に保持しておいて、propertyコマンドから呼び出せばヨシ!
実装して試してみる。1回目ちゃんと動いた!と思ったら、2回目再起動して実行したらException。
1回目はMobileAppからprintfの仕込みの動作を確認して試していたのでちゃんと動作したけど、再起動後に直接実行したら、ProtocolSetProperty()のアドレスがNULLのままだった。
MobileAppを繋がないとここの関数が全く呼ばれないため、MobileAppなしでWebUIから設定を変更したいという当初の目的が達成できず......
再度検討
関数のアドレスを取得する方法があれば、引数の文字列のフォーマットはわかったのでなんとかなるはず。
どこか他でこの関数のアドレスを呼んでるところ無いかと思ったけど、どこにも無い。
elfファイル自体で何か見えないか色々みてみるけど、内部参照なのでelfのsymbol等出ていない。
こうなったら、アプローチ方法を変えてみる。
ちょっとトリッキーだけどメモリ内を検索して文字列から関数を辿って目的の関数アドレスを探してみるように方針を変更。
検索アドレス範囲を取得する
iCamera_appの.textと.rodataのアドレスをlibcallback.soから取得するにはどうすればいいのか、というところで再度悩む。
iCamera_app側のsymbolが出ておらず、libcallback.so側から参照できるsymbolが殆ど無い。
参照できたのは_init, _finiの2つだけ。でも、これで.textを含む範囲は判った。
仕方がないので、/proc/{pid}/mapsから本体のr-xp属性のアドレス情報を取得。_finiとの合わせ技で.rodataを含む範囲が判る。
文字列からprintf呼び出し元の関数を探す
.rodataからprintfの文字列を検索。
そのアドレスをa0レジスタに入れてprintfを読んでいる箇所のアドレスを.textから検索。
ここもちょっとMIPSのニーモニックの特殊なところで、直接a0に32bitの値を入れられないので、下記のようなコードになる。
lui rX, [addr-high16bit]
:
addiu a0, rX, [addr-low16bit]
:
jal printf
このときadd-low16bitのMSBが1の場合はマイナスなのでaddr-high16bitは+1しないといけない。
rXにはどのレジスタが来るかはわからない。しかも、luiとaddiuは連続しているとは限らない。
さらに遅延スロットがあるのでjal printfの次の命令にaddiuが来る可能性もある。
しかも、このprintfはthunk functionなので直接のアドレスではないので、検索キーとしてはjal命令だけの組み合わせでやってみる。
ということで、.text内でlui+addr-high, addiu+addr-low,jalの組み合わせrXの部分とjalのアドレスをmaskして検索する。
見つかったアドレスから、遡ってaddiu sp, spしているところを探す。
そこのアドレスが、1つ目の関数であるP2P_ReceiveProtocol_Parse()になる。
もう1つ上の関数を探す
次にjal P2P_ReceiveProtocol_Parseしているところを.text内で再度検索する。
そこのアドレスから遡ってaddiu sp, spしているところを探す。
そこのアドレスがProtocolSetProperty()である。
これをconstructorで呼び出されるset_property_init()関数で行うことにした。
実装
まず、logを見ながらMobileAppから各種設定を変更して、PropertyListの番号と引数の値の確認をしていく。
これをテーブル化して実装、propertyコマンドのsub commandとする。
1番目がsub command名、2番目が現在の設定値をget_user_configから取得するための名前、3番目がProtocolSetProperty()でjsonで指定するPropertyList番号、最後が処理関数のポインタである。
static struct CommandTableSt PropertyCommandTable[] = {
{ "raw", "", 0, &Raw }, // raw item val
{ "nightVision", "nightVision", 6, &NightVision }, // nightVision on:1/off:2/auto:3
{ "nightCutThr", "night_cut_thr", 62, &NightCutThr }, // nightCutThr dusk:1/dark:2
{ "IrLED", "pir_alaram", 36, &OnOff }, // IrLED on:1/off:2
{ "motionDet", "MASwitch", 9, &OnOff }, // motionDet on:1/off:2
{ "motionLevel", "MMALevel", 10, &Level3 }, // motionLevel low:1/mid:128/high:255
{ "soundDet", "AASwitch", 11, &OnOff }, // soundDet on:1/off:2
{ "soundLevel", "AMALevel", 12, &Level3 }, // soundLevel low:1/mid:128/high:255
{ "cautionDet", "SASwitch", 13, &PairOnOff }, // cautionDet on:1/off:2
{ "drawBoxSwitch", "drawBoxSwitch", 8, &OnOff }, // drawBoxSwitch on:1/off:2
{ "recordType", "recordType", 29, &RecordType }, // recordType cont:1/motion:2
{ "indicator", "indicator", 22, &OnOff }, // indicator on:1/off:2
{ "horSwitch", "horSwitch", 24, &OnOff }, // horSwitch on:1/off:2
{ "verSwitch", "verSwitch", 25, &OnOff }, // verSwitch on:1/off:2
{ "rotate", "horSwitch", 24, &PairOnOff }, // rotate on:1/off:2
{ "audioRec", "AST", 23, &OnOff }, // audioRec on:1/off:2
{ "timestamp", "osdSwitch", 37, &OnOff }, // timestamp on:1/off:2
{ "watermark", "watermark_flag", 7, &OnOff }, // watermark on:1/off:2
{ "motionArea", "MAT", 15, &MotionArea }, // motionArea all:3(MAT:0)/rect:1 <sx:0-99> <sy:0-99> <width:0-99> <height:0-99>
};
引数のフォーマットごとに処理関数を実装して、最終的に下記の関数でProtocolSetProperty()を呼び出している。
static int setItemProp(int item, int val) {
if(!ProtocolSetProperty) {
fprintf(stderr, "setItemProp: not found P2P_ReceiveProtocol_SetProperty function\n");
return -1;
}
const int bufOffset = 0x100;
const int strSize = 0x2800;
const int bufSize = bufOffset + strSize + strSize;
char *buf = (char *)malloc(bufSize);
char *req = buf + bufOffset;
char *res = req + bufOffset;
memset(buf, 0, bufSize);
snprintf(req, strSize, "{\n \"PropertyList\" : {\n \"%d\" : %d\n }\n}", item, val);
ProtocolSetProperty(buf, req, res);
int ret = 0;
if(!strncmp(res, "{\"Result\":", 10)) sscanf(res, "{\"Result\":%d,", &ret);
free(buf);
return !ret;
}
/scripts/cmd property timestamp on
/scripts/cmd property timestamp off
でtimestampのon/off動作確認。
WebUIを作成して追加
他のコマンドと同様にJavaScruptからコマンドをaxiosで叩くように実装。
ただし、カメラ設定は即時実行したいので、設定変更をまとめて設定ボタンで反映するのではなく、ボタンの変化でコマンドを送信するようにした。
まとめと所感
ATOMCamのMobileAppの設定の機能をatomcam_toolsのWebUIからも設定できるようにしました。
これまで、設定値を変更する手段がconfigファイルを書き換えて再起動する方法だったため、使い勝手が悪すぎてWebUIに実装することを諦めていたのですが、今回MobileApp相当の操作性が実現できたのでWebUIに取り込むことができました。
これまでatomcam_toolsの解析はGhydraで見てもアセンブラの命令までだったけど、今回は久しぶりにMIPSのニーモニックコード表を引っ張り出してオペコードへのハンドアセンブルをしました。昔のCPUのBUGが多かった頃の苦労を思い出しました。