Author: hhc0null
本文は#04 Hokkaido CTF 勉強会で出されたrev100のwriteupである.
最適化のかかっていない標準的なELF x86のバイナリを解析するという問題. この問題は主に以下のようなことを行っている.
- 前処理
主に, flagとなる文字列が判別できないようなバイト列を生成する. - 比較・判定部
ユーザからの入力と前述のバイト列に何らかの処理をして, 比較を行うことでユーザからの入力がflag文字列に等しいか否かを判定する.
ここでは, それぞれの処理がどのようなことを行っているかを観察する.
0x804849e 〜 0x80484e2にかけて, flagを何らかの方法によりエンコードしたバイト列をスタック上に展開している.
804849e: c6 45 d9 b6 movb $0xb6,-0x27(%ebp)
80484a2: c6 45 da 8b movb $0x8b,-0x26(%ebp)
80484a6: c6 45 db d8 movb $0xd8,-0x25(%ebp)
80484aa: c6 45 dc 8c movb $0x8c,-0x24(%ebp)
80484ae: c6 45 dd ab movb $0xab,-0x23(%ebp)
80484b2: c6 45 de 90 movb $0x90,-0x22(%ebp)
80484b6: c6 45 df b0 movb $0xb0,-0x21(%ebp)
80484ba: c6 45 e0 ba movb $0xba,-0x20(%ebp)
80484be: c6 45 e1 9e movb $0x9e,-0x1f(%ebp)
80484c2: c6 45 e2 ac movb $0xac,-0x1e(%ebp)
80484c6: c6 45 e3 86 movb $0x86,-0x1d(%ebp)
80484ca: c6 45 e4 b9 movb $0xb9,-0x1c(%ebp)
80484ce: c6 45 e5 cf movb $0xcf,-0x1b(%ebp)
80484d2: c6 45 e6 8d movb $0x8d,-0x1a(%ebp)
80484d6: c6 45 e7 b2 movb $0xb2,-0x19(%ebp)
80484da: c6 45 e8 9a movb $0x9a,-0x18(%ebp)
80484de: c6 45 e9 de movb $0xde,-0x17(%ebp)
80484e2: c6 45 ea ff movb $0xff,-0x16(%ebp)
上記のコードより, 最後尾には0xffがあることがわかる. また, 全体的に0x80以降のバイトが多いことがわかるだろう. これを頭の隅に置いておいて欲しい.
このあとの処理については, ユーザからの入力をコマンドライン引数(argv)により受け付けたり, argv[1]の文字列の長さを判定したりしている. 本質的ではないので割愛する.
ここではユーザからの入力とflagとなる文字列の比較, flagか否かの判定を行っているが, この問題においては非常にシンプルなものとなっている. 以下に示す, 0x804855c 〜 0x8048588が比較部で, 0x804858a 〜 0x8048590が判定部となっている.
804855c: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
8048563: eb 04 jmp 8048569 <main+0xde>
8048565: 83 45 f4 01 addl $0x1,-0xc(%ebp)
8048569: 8b 55 f4 mov -0xc(%ebp),%edx
804856c: 8b 45 ec mov -0x14(%ebp),%eax
804856f: 01 d0 add %edx,%eax
8048571: 0f b6 08 movzbl (%eax),%ecx
8048574: 8d 55 d9 lea -0x27(%ebp),%edx
8048577: 8b 45 f4 mov -0xc(%ebp),%eax
804857a: 01 d0 add %edx,%eax
804857c: 0f b6 00 movzbl (%eax),%eax
804857f: 31 c8 xor %ecx,%eax
8048581: 88 45 eb mov %al,-0x15(%ebp)
8048584: 80 7d eb ff cmpb $0xff,-0x15(%ebp)
8048588: 74 db je 8048565 <main+0xda>
804858a: 8b 45 f4 mov -0xc(%ebp),%eax
804858d: 3b 45 f0 cmp -0x10(%ebp),%eax
8048590: 75 25 jne 80485b7 <main+0x12c>
8048592: 83 ec 0c sub $0xc,%esp
8048595: 68 89 86 04 08 push $0x8048689
804859a: e8 a1 fd ff ff call 8048340 <puts@plt>
804859f: 83 c4 10 add $0x10,%esp
80485a2: 83 ec 08 sub $0x8,%esp
80485a5: ff 75 ec pushl -0x14(%ebp)
80485a8: 68 94 86 04 08 push $0x8048694
80485ad: e8 7e fd ff ff call 8048330 <printf@plt>
80485b2: 83 c4 10 add $0x10,%esp
80485b5: eb 10 jmp 80485c7 <main+0x13c>
80485b7: 83 ec 0c sub $0xc,%esp
80485ba: 68 9d 86 04 08 push $0x804869d
80485bf: e8 7c fd ff ff call 8048340 <puts@plt>
ここでの処理は,
- ユーザからの入力のひとつひとつのバイトと, 前述したバイト列の排他的論理和(XOR)を求める.
- その値が0xffと比較して等しかったら0x8048565にjmpしてこれを繰り返す.
- 等しくない場合はそのまま0x804858aに進み, 今までに繰り返した回数とバイト列の長さを比較する.
- 比較した結果が等しいものだったなら, ユーザからの入力をflagとして出力する.
というものになっている.
XORについて,
- [0] XOR 1 => [1]
- [1] XOR 1 => [0]
であることから, 1でXORするということはNOTにすることに相当するということが解る(これが情報系の人間の常識である). この問題はユーザからの入力と前述のバイト列をXORし0xff(0b11111111)に等しいか否かを判定しているため, 単に「前述のバイト列をNOTすればよい」ということが解る. 以上を踏まえてソルバを書くと, 次のようなコードとなる.
#! /usr/bin/env python2
encoded_flag = (
0xb6, 0x8b, 0xd8, 0x8c, 0xab, 0x90, 0xb0, 0xba,
0x9e, 0xac, 0x86, 0xb9, 0xcf, 0x8d, 0xb2, 0x9a,
0xde, 0xff,
)
def f(x):
return chr(x ^ 0xff) # this proccess is equivalent to xor.
decoded_flag = "".join(map(f, encoded_flag))
print "key{%s}"%(decoded_flag)
以下に元々のコードを示す.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// key{It'sToOEaSyF0rMe!}
int main(int argc, char *argv[])
{
unsigned char inversed[] = {
'\xb6', '\x8b', '\xd8', '\x8c', '\xab', '\x90', '\xb0', '\xba',
'\x9e', '\xac', '\x86', '\xb9', '\xcf', '\x8d', '\xb2', '\x9a',
'\xde', '\xff',
};
int count = 0, len = sizeof(inversed);
unsigned char tmp, *input = argv[1];
if(argc < 2) {
printf("Usage: %s <flag>\n", argv[0]);
exit(EXIT_SUCCESS);
}
if(strlen(argv[1]) != len - 1) {
puts("The length is wrong...");
exit(EXIT_SUCCESS);
}
for(count = 0; (tmp = input[count] ^ inversed[count]) == 0xff; count++);
if(count == len) {
puts("Congratz!!");
printf("key{%s}\n", input);
} else {
puts("Wrong...");
}
return 0;
}
冗長に書いただけのコードだったので, x86のアセンブリが読める人なら簡単に解る基本的な問題だったと思う. 最後にflagを示しておこう.
flag: key{It'sToOEaSyForMe!}
flag: key{It'sToOEaSyF0rMe!}
writeupに書いていたflagが間違っていたので修正. 申し訳ございません><