Androidアプリケーションのネイティブライブラリの関数呼び出しの傍受

それは何のためですか?



ネイティブコードを使用してAndroidアプリケーションをデバッグする必要があることがよくありました。 時にはバイオニック(libc)への呼び出し、時には.so-shkamiへの呼び出しをインターセプトする必要がありましたが、ソースコードはありませんでした。 時には、他の人の.soをアプリケーションに含める必要がありました。これにはソースコードがなく、動作を調整する必要がありました。



では、AndroidでLD_PRELOADを実行する方法は?



広く知られているように、通常のLinuxデスクトップでは、環境変数LD_PRELOADを使用してこの問題を簡単に解決できます。 このトリックは次のように機能します。動的リンカーは、この変数のライブラリを使用可能なライブラリのリストの一番上に配置します。 その結果、コードが初めてライブラリ呼び出しを行おうとすると(遅延バインディング)、リンカーは関数をライブラリで定義した関数にバインドします。



これはすべて素晴らしいことですが、Androidではこのトリックは機能しません。 UIから起動されたアプリケーションは、アプリケーションの作成者が作成したコードが実行されるまでに既にリンクされています。 理論的には、コマンドラインからアプリケーションを起動し、LD_PRELOADを設定できます。 しかし、これは難しい作業であり、デバッグのためにのみ機能します。



動的レイアウトについて少し



動的ライブラリを使用するには、他のライブラリからコードを呼び出す機能が必要です。逆の場合も同様です。 コンパイル済みのコードは、どのようにして別のライブラリからコードを呼び出すことができますか? 通常のjmp / bxタイプの遷移操作にはアドレスが必要ですが、メモリ内の異なる.soは異なる(またはランダムな)場所に移動する可能性があるため、事前に(アセンブリ.soの時点で)アドレスを知ることはできません。 すべての.soが既にメモリに配置されている場合、コード内の必要な関数のアドレスに簡単にパッチを適用できます。 しかし、これはエレガントではなく、遅く、コード領域への書き込みが必要です。さらに、各アプリケーションはコードの独自のコピーを取得する必要があり、メモリの節約はありません。



解決策は非常に簡単です。ジャンプは、実行可能コードセクションの外部に記録されたアドレスで発生します。 また、このアドレスが絶対ではなく相対アドレスになっている場合(たとえば、コマンド自体のオフセットとして書き込む場合)、コード自体はメモリ内のどこにでも配置できることがわかります。 そしてその背後にあるのが、PLTテーブル、プロシージャリンクテーブルです。 通常は(rまたはrw)としてマップされ、実行可能ではありません。 このテーブルには、「実際の」アドレスのみが含まれています。 テーブルは、レイジーモードで、開始時と実行時直接の両方に入力できます。



yyy()関数を呼び出すときにxxx.soモジュールをインターセプターにジャンプさせるために、すべてをまとめる場合:





実際には、傍受



Androidはバイオニックを使用し、glibcとは少し異なりますが、根本的な違いはありません。 内部データはsoinfo



構造に格納され、これは現時点でロードされているすべての.soデータのリンクリストです。



glibcでは、 dlopen()



は、真空中の球面void*



返します。



 void *dlopen(const char *filename, int flag)
      
      







しかし、バイオニックソースを見ると、切望されているsoinfo



れることがsoinfo





 soinfo* do_dlopen(const char* name, int flags)
      
      





ライブラリがすでにロードされている場合、そのライブラリのsoinfo



を返します。 Hooray、今私たちは私たちの興味のある.soに関するすべての情報を手にしています。



ELFでは、文字を含む行は個別に(strtab)、文字の説明を含む個別の構造体(symtab)に格納されます。 文字自体(文字列定数)については、ハッシュが計算されるため、関心のある文字のオフセットをすばやく見つけることができます。



ELF文字ハッシュカウント
  static unsigned elfhash(const char *_name) { const unsigned char *name = (const unsigned char *) _name; unsigned h = 0, g; while(*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; }
      
      







ハッシュがカウントされたら、sivolを見つける必要があります。

ハッシュによる文字検索
 static Elf32_Sym *soinfo_elf_lookup(soinfo *si, unsigned hash, const char *name) { Elf32_Sym *s; Elf32_Sym *symtab = si->symtab; const char *strtab = si->strtab; unsigned n; n = hash % si->nbucket; for(n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]){ s = symtab + n; if(strcmp(strtab + s->st_name, name)) continue; return s; } return NULL; }
      
      







目的の値を置き換える手順は次のとおりです。

  int hook_call(char *soname, char *symbol, unsigned newval) { soinfo *si = NULL; Elf32_Rel *rel = NULL; Elf32_Sym *s = NULL; uint32_t sym_offset = 0; uint32_t page_size = 0; if (!soname || !symbol || !newval) return 0; si = (soinfo*) dlopen(soname, 0); if (!si) return 0; s = soinfo_elf_lookup(si, elfhash(symbol), symbol); if (!s) return 0; page_size = getpagesize(); sym_offset = s - si->symtab; //    rel = si->plt_rel; /*           */ for (int i = 0; i < si->plt_rel_count; i++, rel++) { unsigned type = ELF32_R_TYPE(rel->r_info); unsigned sym = ELF32_R_SYM(rel->r_info); unsigned reloc = (unsigned)(rel->r_offset + si->base); unsigned oldval = 0; if (sym_offset == sym) { switch(type) { case R_ARM_JUMP_SLOT: //     RW,     page-aligned mprotect((uint32_t *) reloc& (~(page_size - 1), page_size, PROT_READ | PROT_WRITE); oldval = *(unsigned*) reloc; *((unsigned*)reloc) = newval; return 1; default: return 0; } } } return 0; }
      
      







ここで、libandroid_runtime.soからconnect()をインターセプトするには、以下を呼び出す必要があります。



 hook_call("libandroid_runtime.so", "connect", &my_connect);
      
      






All Articles