前2問と比べかなり複雑な内容になっております。
デコンパイルを試す
色々writeupを見たところ、今回は逆アセンブルしただけではまだ把握しにくいらしく、そこからソースコードを再現して考えているものが多くありました。
なお、IDAのプラグイン、Hex-Rays Decompiler(有料、たぶん高価)などを用意しておけば、IDAでF5キーを押すと逆コンパイルできます。
持ってませんけどね。
RecStudioっていう逆コンパイラを試したら、分岐がif-elseやswitch-caseになって見やすくなった以外に嬉しいことはあまりない感じの出力で、しかも、目的とすべき場所が逆コンパイルできてませんでした。(使い方がわかってないだけかも)
https://retdec.com/のRetargetable Decompilerっていうのはサーバ上で逆コンパイルしてくれるし結構速いし出すコードもいくぶんかわかりやすいので今後はこちらを使います。しかし5分以上かかるファイルは扱えません。そのときはたぶん公開されているIDA用のプラグインを使えば良いです。
とこんな感じになります。試しにやっただけなので、読みやすく書き換えたりしてません。後述の参考URLのソースコードを読んだ方がわかりやすいです。
ちなみに逆コンパイルは、逆アセンブルと同じで、人間がコメントを追加したり変数名・関数名などを書き換えて読みやすいようにする必要があります。変数がv1とかv2とかそのままだと理解しにくいので...。
RecStudioっていう逆コンパイラを試したら、分岐がif-elseやswitch-caseになって見やすくなった以外に嬉しいことはあまりない感じの出力で、しかも、目的とすべき場所が逆コンパイルできてませんでした。(使い方がわかってないだけかも)
https://retdec.com/のRetargetable Decompilerっていうのはサーバ上で逆コンパイルしてくれるし結構速いし出すコードもいくぶんかわかりやすいので今後はこちらを使います。しかし5分以上かかるファイルは扱えません。そのときはたぶん公開されているIDA用のプラグインを使えば良いです。
//
// This file was generated by the Retargetable Decompiler
// Website: https://retdec.com
// Copyright (c) 2018 Retargetable Decompiler <info@retdec.com>
//
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// ------------------------ Structures ------------------------
struct _IO_FILE {
int32_t e0;
};
// ------------------- Function Prototypes --------------------
int32_t calc(void);
int32_t eval(int32_t * a1, int32_t a2);
int32_t parse_expr(int32_t a1, int32_t * a2);
// --------------------- Global Variables ---------------------
int32_t g1 = 0; // ebp
char * g2 = "0"; // 0x80bf7a8
struct _IO_FILE * g3 = (struct _IO_FILE *)0x80ec200; // 0x80ec4c0
// ------------------------ Functions -------------------------
// Address range: 0x8048ee1 - 0x8048ff7
int32_t eval(int32_t * a1, int32_t a2) {
int32_t result = (int32_t)a1;
int32_t v1 = 0x1000000 * a2;
if (v1 == 0x2b000000) {
int32_t v2 = result - 4; // 0x8048f2d
int32_t v3 = *(int32_t *)(v2 + 4 * *a1); // 0x8048f2d
int32_t v4 = *(int32_t *)(result + 4 + 4 * (*a1 - 1)); // 0x8048f3c
*(int32_t *)(v2 + 4 * *a1) = v4 + v3;
// branch -> 0x8048fe4
// 0x8048fe4
*a1 = *a1 - 1;
return result;
}
// 0x8048ef8
if (v1 <= 0x2b000000) {
// 0x8048efd
if (v1 == 0x2a000000) {
int32_t v5 = result - 4; // 0x8048f92
int32_t v6 = *(int32_t *)(v5 + 4 * *a1); // 0x8048f92
int32_t v7 = *(int32_t *)(result + 4 + 4 * (*a1 - 1)); // 0x8048fa1
*(int32_t *)(v5 + 4 * *a1) = v7 * v6;
// branch -> 0x8048fe4
}
// 0x8048fe4
*a1 = *a1 - 1;
return result;
}
// 0x8048f07
switch (v1 / 0x1000000) {
case 45: {
int32_t v8 = result - 4; // 0x8048f61
int32_t v9 = *(int32_t *)(v8 + 4 * *a1); // 0x8048f61
int32_t v10 = *(int32_t *)(result + 4 + 4 * (*a1 - 1)); // 0x8048f70
*(int32_t *)(v8 + 4 * *a1) = v9 - v10;
// branch -> 0x8048fe4
break;
}
case 47: {
int32_t v11 = *a1; // 0x8048fb4
int32_t v12 = result - 4; // 0x8048fc4
uint32_t v13 = *(int32_t *)(v12 + 4 * *a1); // 0x8048fc4
int32_t v14 = *(int32_t *)(result + 4 + 4 * (*a1 - 1)); // 0x8048fd3
*(int32_t *)(v12 + 4 * v11) = (int32_t)((0x100000000 * (int64_t)(v13 / 0x80000000) | (int64_t)v13) / (int64_t)v14);
// branch -> 0x8048fe4
break;
}
}
// 0x8048fe4
*a1 = *a1 - 1;
return result;
}
// Address range: 0x804902a - 0x8049378
int32_t parse_expr(int32_t a1, int32_t * a2) {
int32_t v1 = *(int32_t *)20; // 0x8049047
int32_t v2;
int32_t v3 = &v2; // 0x804906c_0
function_8048240(v3, 100);
int32_t v4 = 0; // 0x804935449
int32_t v5 = a1; // 0x80491a747
int32_t v6 = 0;
// branch -> 0x8049081
while (true) {
int32_t v7 = v6 + a1; // 0x804908d
char * v8 = (char *)v7;
int32_t v9 = v4; // 0x804935450
int32_t v10 = v5; // 0x80491a746
if ((int32_t)*v8 >= 58) {
int32_t v11 = v7 - v5; // 0x80490b7
char * mem = malloc(v11 + 1); // 0x80490c7
int32_t v12 = (int32_t)mem; // 0x80490c7_3
memcpy(mem, (char *)v5, v11);
*(char *)(v12 + v11) = 0;
function_80482a0(v12, (int32_t)&g2);
int32_t v13;
if (v12 == 0) {
// 0x804910d
puts("prevent division by zero");
fflush((struct _IO_FILE *)*(int32_t *)0x80ec4c0);
// branch -> 0x804935f
} else {
uint32_t str_as_i = atoi(mem); // 0x8049136
if (str_as_i >= 1) {
int32_t v14 = *a2; // 0x804914a
*a2 = v14 + 1;
*(int32_t *)((int32_t)a2 + 4 + 4 * v14) = str_as_i;
// branch -> 0x8049164
}
int32_t v15 = a1 + 1 + v6;
if (*v8 != 0) {
// 0x8049179
if ((int32_t)*(char *)v15 > 57) {
// 0x80491c0
puts("expression error!");
fflush(g3);
// branch -> 0x804935f
// 0x804935f
if (*(int32_t *)20 != v1) {
// 0x804936b
__stack_chk_fail();
// branch -> 0x8049370
}
// 0x8049370
g1 = v13;
return 0;
}
}
char * v16 = (char *)(v4 + v3);
char v17 = *v8; // 0x80491f1
int32_t v18; // 0x804935451
if (*v16 == 0) {
// 0x80491e3
*v16 = v17;
v18 = v4;
// branch -> 0x804930c
lab_0x804930c_5:
// 0x804930c
if (*v8 == 0) {
// 0x8049354
if (v18 <= 0) {
// 0x804935a
// branch -> 0x804935f
// 0x804935f
if (*(int32_t *)20 != v1) {
// 0x804936b
__stack_chk_fail();
// branch -> 0x8049370
}
// 0x8049370
g1 = v13;
return 1;
}
for (int32_t i = v18; i >= 1; i--) {
// 0x8049330
eval(a2, (int32_t)*(char *)(i + v3));
// continue -> 0x8049330
}
// branch -> 0x804935f
// 0x804935f
if (*(int32_t *)20 != v1) {
// 0x804936b
__stack_chk_fail();
// branch -> 0x8049370
}
// 0x8049370
g1 = v13;
return 1;
}
// 0x8049324
v4 = v18;
v5 = v15;
v6++;
// branch -> 0x8049081
continue;
} else {
// 0x8049203
switch ((int32_t)v17) {
default: {
// 0x80492e8
eval(a2, (int32_t)*v16);
v18 = v4 - 1;
// branch -> 0x804930c
goto lab_0x804930c_5;
}
case 37: {
// 0x804926c
if (*v16 == 43) {
lab_0x804928a_4:;
int32_t v19 = v4 + 1; // 0x804928a
*(char *)(v19 + v3) = *v8;
v18 = v19;
// branch -> 0x804930c
goto lab_0x804930c_5;
}
break;
}
case 42: {
// 0x804926c
if (*v16 == 43) {
goto lab_0x804928a_4;
}
lab_0x804927b:
// 0x804927b
if (*v16 == 45) {
goto lab_0x804928a_4;
}
lab_0x80492ab:
// 0x80492ab
eval(a2, (int32_t)*v16);
*v16 = *v8;
v18 = v4;
// branch -> 0x804930c
goto lab_0x804930c_5;
break;
}
case 43: {
// 0x804922c
eval(a2, (int32_t)*v16);
*v16 = *v8;
v18 = v4;
// branch -> 0x804930c
goto lab_0x804930c_5;
}
case 45: {
// 0x804922c
eval(a2, (int32_t)*v16);
*v16 = *v8;
v18 = v4;
// branch -> 0x804930c
goto lab_0x804930c_5;
}
case 47: {
// 0x804926c
if (*v16 == 43) {
goto lab_0x804928a_4;
}
goto lab_0x804927b;
}
}
// 0x804927b
if (*v16 == 45) {
goto lab_0x804928a_4;
}
goto lab_0x80492ab;
}
}
// 0x804935f
if (*(int32_t *)20 != v1) {
// 0x804936b
__stack_chk_fail();
// branch -> 0x8049370
}
// 0x8049370
g1 = v13;
return 0;
}
// 0x8049324
v4 = v9;
v5 = v10;
v6++;
// branch -> 0x8049081
}
}
// Address range: 0x8049379 - 0x8049433
int32_t calc(void) {
int32_t v1 = *(int32_t *)20; // 0x8049383
int32_t v2;
int32_t v3 = &v2; // 0x8049395_0
function_8048240(v3, 1024);
int32_t v4; // 0x80493c2
if (get_expr(v3, 1024) == 0) {
// 0x80493bd
v4 = *(int32_t *)20;
if (v4 != v1) {
// 0x804942d
__stack_chk_fail();
// branch -> 0x8049432
}
// 0x8049432
return v4 ^ v1;
}
// 0x80493cc
// branch -> 0x80493cc
while (true) {
// 0x80493cc
int32_t v5;
init_pool((int32_t)&v5);
if (parse_expr(v3, &v5) != 0) {
// 0x80493f6
printf("%d\n", *(int32_t *)(g1 - 1436 + 4 * (v5 - 1)));
fflush(g3);
// branch -> 0x804938d
}
// 0x804938d
function_8048240(v3, 1024);
if (get_expr(v3, 1024) == 0) {
// break -> 0x80493bd
break;
}
// continue -> 0x80493cc
}
// 0x80493bd
v4 = *(int32_t *)20;
if (v4 != v1) {
// 0x804942d
__stack_chk_fail();
// branch -> 0x8049432
}
// 0x8049432
return v4 ^ v1;
}
// Address range: 0x8049452 - 0x80494af
int main(int argc, char ** argv) {
// 0x8049452
signal(SIGALARM, (void (**)(int32_t))timeout);
alarm(60);
puts("=== Welcome to SECPROG calculator ===");
fflush(g3);
calc();
return puts("Merry Christmas!");
}
// --------------- Statically Linked Functions ----------------
// void __stack_chk_fail(void);
// unsigned int alarm(unsigned int seconds);
// int atoi(const char * nptr);
// int fflush(FILE * stream);
// void * malloc(size_t size);
// void * memcpy(void * restrict dest, const void * restrict src, size_t n);
// int printf(const char * restrict format, ...);
// int puts(const char * s);
// __sighandler_t signal(int sig, __sighandler_t handler);
// --------------------- Meta-Information ---------------------
// Detected compiler/packer: gcc (4.8.2)
// Detected functions: 4
// Functions selected to be decompiled but not found: initpool
// Decompiler release: v2.2.1 (2016-09-07)
// Decompilation date: 2018-02-21 06:47:59
とこんな感じになります。試しにやっただけなので、読みやすく書き換えたりしてません。後述の参考URLのソースコードを読んだ方がわかりやすいです。
ちなみに逆コンパイルは、逆アセンブルと同じで、人間がコメントを追加したり変数名・関数名などを書き換えて読みやすいようにする必要があります。変数がv1とかv2とかそのままだと理解しにくいので...。
逆アセンブルを試す
あわせてIDAで逆アセンブルしますが...
私レベルでは逆アセンブルしただけじゃよくワカラナイ…
無理なので他人のWriteupにて再現されたソースコードを見る
他の人のwriteupにてわかりやすく再現されたソースコードを見ます。
各関数はこれ
全体の流れは
どうやら...演算子と被演算子をそれぞれスタックにプッシュして計算するそうです。
先ほどのリンク先でわかりやすく逆コンパイルして書き換えたソースコードが掲載されているのでそちらを見て欲しいです。関数名や変数名もそちらに準じます。
入力を、数字以外の文字が来るまで読みます。
数字の場合は被演算子スタック(expr->buf配列)に追加し、演算子スタック(op_s配列)の中身があればeval関数で計算します。
演算子の場合は、'+'や'-'の場合は出現次第eval関数に被演算子スタックと演算子スタックのトップを渡して計算します。つまり、一つ前の演算子の計算を行います。その後で、今読んだ演算子を演算子スタックのトップとすげ替えます。
プログラムの流れ
parse_expr関数で入力の内容を判断します。入力を、数字以外の文字が来るまで読みます。
数字の場合は被演算子スタック(expr->buf配列)に追加し、演算子スタック(op_s配列)の中身があればeval関数で計算します。
演算子の場合は、'+'や'-'の場合は出現次第eval関数に被演算子スタックと演算子スタックのトップを渡して計算します。つまり、一つ前の演算子の計算を行います。その後で、今読んだ演算子を演算子スタックのトップとすげ替えます。
それ以外、つまり'*'や'/', '%'のときは、'+'や'-'より優先度が高いので処理が少し複雑です。演算子スタックのトップ(=一つ前に出現した演算子)が'+'や'-'で無い場合に限り、一つ前に出現した演算子の計算を行います。そして演算子スタックのトップをすげ替えます。もし'+'や'-'だった場合、こちらの演算子の方が優先順位が高いため、演算子スタックのトップに今回出現した演算子を積みます。
全ての入力を読み終わった後、演算子スタックが空になるまでポップしeval関数で計算を進めていきます。
バグのありか
そして実際に計算を行うeval関数に実装のミスがあります。
渡された演算子に対応する計算を行うのですが、被演算子配列をevalとし、その長さをいれてある変数をeval->bufsizeとします。すると例えば'+'演算子の場合expr->buf[expr->bufsize-2] += expr->buf[expr->bufsize-1]を実行することで所望の計算をしています。
これだともし「+100」のように演算子の左側に被演算子が無い場合、上の計算方法では配列の範囲外を書き換えてしまうことになります。
そして、スタックの状態を調べると、演算子の配列expr->bufは、配列の長さexpr->bufsizeのすぐ次に配置されており、eval関数のバグを利用すれば配列の長さexpr->bufsizeを書き換えられます。
すると
expr->buf[expr->bufsize-2] += expr->buf[expr->bufsize-1]
では、インデックスの指定にexpr->bufsizeを用いているため、好きな場所を書き換えられます。
すると
expr->buf[expr->bufsize-2] += expr->buf[expr->bufsize-1]
では、インデックスの指定にexpr->bufsizeを用いているため、好きな場所を書き換えられます。
バグを利用した動作例
例えば「+361 + 1」と入力すると
parse_expr関数で+を読んだときは初めての演算子なのでop_s演算子スタックに突っ込まれるだけです。
次にparse_expr関数で361を読んだあとですが、
expr->buf[0]が361、expr->bufsizeが1になります。
そして前回読んだ+演算子についてeval関数で計算されます。
expr->buf[expr->bufsize -2] += expr->buf[expr->bufsize -1]
により、範囲外アクセスが起こります。つまりexpr->buf[-1] += expr->buf[0]
となります。
このとき&(expr->buf[-1]) == &(expr->bufsize)なので、expr->buf[-1] += expr->buf[0]は1 += 361と同じです。 このように脆弱性を利用しexpr->bufsizeが362になってしまいます。
このあとexpr->bufsizeはデクリメントされ、expr->bufsize == 361になります。
次に2個目の+を読んだときは、op_s演算子スタックに突っ込まれるだけです。
このあとparse_expr関数で1が読まれます。
expr->buf[361]は1、expr->bufsizeは362になります。
そして2個目の+についてeval関数で計算が行われます。
eval関数を呼びますと、expr->buf[expr->bufsize -2] += expr->buf[expr->bufsize -1]
、つまりexpr->buf[360] += expr->buf[361]
が行われます。
このあとexpr->bufsizeはデクリメントされ、expr->bufsize == 361になります。
そして答えとしてexpr->buf[expr->bufsize -1]の値が表示されます。この例でいうとexpr->buf[360]です。
このようにして、「+361+1」と入力したときは、expr->bufのあるアドレスから+361オフセットしたアドレスの指す先を1にし、+360オフセットしたアドレスの指す先に+1してしまいます。答えとして+360オフセットしたアドレスの中身が表示されます。
...という風な感じでメモリを弄っていきます。
試してみると、「+1」でexpr->buf[0]の1が出力されています。expr->buf[]は長さ100の配列なので、expr->buf[100]は範囲外のはずです。「+101」にはちゃんとカナリアが入っており、正しそうです。後述しますが「+357」もカナリアが表示されてます
ちなみに「+0」は0で割るなというメッセージが出てしまいます。なぜか0が被演算子だと、除数のとき以外でもこうなる実装なので「+00」で回避しましょう。
検証
上の流れでいくと、「+n」とだけ入力したときは、expr->bufsizeが=nになり、答えとしてexpr->buf[n-1]が表示されます。試してみると、「+1」でexpr->buf[0]の1が出力されています。expr->buf[]は長さ100の配列なので、expr->buf[100]は範囲外のはずです。「+101」にはちゃんとカナリアが入っており、正しそうです。後述しますが「+357」もカナリアが表示されてます
ちなみに「+0」は0で割るなというメッセージが出てしまいます。なぜか0が被演算子だと、除数のとき以外でもこうなる実装なので「+00」で回避しましょう。
user1@user1-VirtualBox:~/ダウンロード$ ./calc === Welcome to SECPROG calculator === +00 0 +1 1 +2 0 +3 0 +4 0 +100 0 +101 825241899 +102 0 +356 0 +357 1082368768 +358 0 Merry Christmas!
解法
セキュリティ機構に関して
スタックカナリア
配布されたプログラムcalcが備えたセキュリティ機構を調べてみると、スタックカナリアが使われています。
だいたい書き換えたくなるリターンアドレスの前にカナリアが配置されており、バッファオーバーフローなどでスタックを書き換えて行くと、連続した領域を書き換えることになるのでカナリアも書き換えてしまうので検知されます。
(演算子スタック用の配列は長さが100なので、eval関数においてなぜか実装されていない%演算子を上手に使うことでオーバフローさせて書き換えることもできますがカナリアで検知されるのでダメなのです)
しかし今回のように、アドレスを指定して書き換えができる場合、カナリアを避けることができるので検知されずに済みます。
NX
そしてNXも有効です。
スタックにエクスプロイトコードを積んでも使えません。
よって共有ライブラリ上にあるコードを再利用する、ROPを用いる必要があります。
ランダマイズ関係
ほか、アドレスのランダマイズは一切オフのようです。(自分じゃ調べてないけど、他人のwriteupでアドレス決め打ちな回答があるので)
具体的な解法
具体的に何を入力すれば良いかは
や、長いけど
が詳しいです
前提として、
具体的には、擬似コードでいうとexecve("/bin/sh", {"/bin/sh", null}, null)をROPで実行します。
EAXにシステムコール 番号0x0b、EBXにファイルネーム"/bin/sh\0"、ECXにargv(渡す引数){"/bin/sh\0", null}、EDXにenvp(環境変数へのポインタ、今回はnull)を格納し、INT命令を実行します。
ROPチェインをスタックに格納し、calc関数からのret実行時に開始します。
それにはcalc関数からのret実行時に使うリターンアドレスの位置を調べなくてはなりません。
eval関数のバグで最初に書き換えられる、配列の長さの値の格納されたアドレスから、カナリアまでのオフセットを調べると、1428です。
配列の長さexpr->bufの値に設定すべき値は、(1428-4)/4 = 356 (DWORD、4バイト単位)...に+1して357です。先ほど説明したように、確かに「+357」とするとexpr->bufのある位置から+356オフセットした場所に格納されているカナリアが表示されます。
スタックの状態を調べると、「+357」にはカナリア、「+358」はパディング、「+360」には前回のEBP、「+361」にはリターンアドレスが積まれている...らしいです。
よって「+361」からROPチェインを積んでいきます。
単純にメモリに書き込めるのではなく、その一つ前にも加算されてしまうという(→前述の太字のところを読んでください)ことに注意して書き込みます。
以上です。
単純にメモリに書き込めるのではなく、その一つ前にも加算されてしまうという(→前述の太字のところを読んでください)ことに注意して書き込みます。
以上です。

0 件のコメント:
コメントを投稿