2024 NYCU 電腦安全 Project 4 CTF

總共6題

  1. shop
  2. magic
  3. ret2libc
  4. doll
  5. fmt
  6. hello

shop

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
#include <stdio.h>
#include <stdlib.h>
void purchase(int price, int your_money) {
int amount;
puts("Input the amount:");
scanf("%d", &amount);
if(amount > 0){
int total_price = price*amount;
if(your_money >= total_price){
your_money -= total_price;
printf("You have purchased the flag\n");
getFlag();
}
else {
puts("You don't have enough money!");
}
}
else{
puts("Invalid amount!");
}
}


void getFlag() {
puts(getenv("FLAG"));
}

int main(int argc, char **argv){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
int flag_price = 999999, your_money = 10, choice;
puts("Welcome to the server:");
while(1) {
printf("Current money: %d\n", your_money);
puts("1. Purchase Flag");
puts("2. Exit");
puts("Input your choice:");
scanf("%d", &choice);
switch(choice) {
case 1:
purchase(flag_price, your_money);
break;
case 2:
exit(0);
default:
puts("Invalid choice!");
break;
}
}
}

可以看到main會呼叫purchase,然後purchase會比較輸入的數量 * 999999和10,只要運用integer overflow讓數量 * 999999爆開就可以拿到flag了

magic

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

char secret[0x10];

void init()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
srand(time(0));
for (int i = 0; i < 0x10; i++)
{
secret[i] = 48 + (rand() % (126 - 47) + 1);
}
}

int main(){
init();
puts("Please enter the secret: ");
char input[0x10];
read(0, input, 0x10);
if (strcmp(input, secret) == 0)
{
puts("You got it! Here is your flag!");
puts(getenv("FLAG"));
}
else
{
puts("Guess wrong!");
}
return 0;
}

這邊可以看到secret是用srand(time(0))產生的,可以用python裡的ctype來設srand的seed,把它設成time.time()然後用一樣的方法產生secret再送過去就可以拿到flag了

ret2libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

void hackMe() {
char buf[128];
read(0, buf, 256);
}

int main(int argc, char **argv){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
puts("Welcome to the server!");
hackMe();
puts("Goodbye!");
}

這題的flag存在機器上的flag.txt,要rce才行。

我有裝gdb peda,checksec看一下

只有開NX,我選擇用ret2libc來做。

一開始的思路是直接送到overflow之後,用plt puts出puts的got,洩漏出libc的base address,然後再用system(“/bin/sh”)來拿flag,但是這個小小的程式裡面找不到pop rdi的gadget,這條路沒辦法拿到libc base

後來我看了看main裡面,發現stdin和stdout的真實地址是存在0x404050裡面,這個地址是got table的範圍,裡面存的address是在libc裡面的。

目前想到用got table hijacking,把setvbuf變成puts。

objdump -R 看一下got table有什麼

GOT table範圍

因為GOT table裡面是可以寫的,我們可能可以把setvbuf的got改成puts@plt的位置,然後把stdin改成0x404040,等於puts的參數是指向0x7ffff7e1b780的一個pointer,這樣在跳回main執行setvbuf(stdin, …)的時候,就等同於執行puts(*(0x7ffff7e1b780)),我們就可以得到這個值減掉offset拿到libc base了。

現在的重點是要怎麼覆寫setvbuf的got,目前唯一可以讓我們任意寫的地方就只有read了。

我在這個 raddit的貼文 看到有人用overflow把stack上的rbp變成got附近的位置,leave的時後相當於mov rsp, rbp; pop rbp,所以就可以控制rbp的值;之後再執行到read的時候,輸入的東西就會寫到rpb - 0x80的位置,只要把rpb蓋成setvbuf在got table的地址 + 0x80,就可以把puts的地址寫到setvbuf的got table裡面了。

用read把puts的plt寫到setvbuf的got table裡面的時候要注意,只要程式一執行read,在ret之前一定就會leave,要蓋掉rpb + buffer size的話整個stack都會跑到got table附近,我們需要自己把stack盡可能的塞到可寫的區域裡面,然後好好控制rbp和rsp的位置。

現在要注意的一點就是setvbuf因為在比stdin和stdout都低的位置,而因為puts的時候會檢查stdout有沒有壞掉,所以要想一下要怎麼蓋掉setvbuf和stdin,而且之後可以ret到main裡面執行setvbuf。

1
2
3
4
5
6
7
8
9
10
11
12
13
GOT table:
0x405000 xxx <- 可寫的最高位置
... <- rpb和rsp會在這
0x404058 ...
0x404050 | stdin | <- 要蓋成0x404040 (相當於指向stdout的地址的pointer,puts才洩漏得出來)
0x404048 | |
0x404040 | stdout | <- 不可以蓋掉
...
0x404028 | setvbuf | <- 蓋成puts@plt (0x404018)
0x404020 | read |
0x404018 | puts |
...
0x404000 xxx <- 可寫的最低位置

我的方法就是要先在setvbuf + 0x80附近先設好在覆蓋完setvbuf之後rbp,ret的地址,順便把stdin蓋成0x404040;rbp需要到更高的地方加大stack的大小 (0x405000附近),ret會是main+19的地方,之後還會有一次機會可以read,去讓程式執行system(‘bin/sh’)。

exp: ret2libc.py

doll

這題算是misc,給了一張圖。

Matryoshka dolls.jpg

用hex editor打開,查了一下jpg結尾的EOI bytes(0xFFD9),發現他在offset 0x6ADC的位置,然後那附近又看到PNG的bytes,推測可能是又有一張png圖在檔案的尾部。

用dd把它切出來,然後用hex editor打開,發現正好就是一個有flag的png。

1
dd if="$filename" of=flag.png bs=1 skip=27424

flag.png

fmt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdint.h>
#include <stdio.h>

int main(){
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);

char flag[40] = "FLAG{??????????????????????????????????}";
char input[24];

scanf("%s", input);
printf(input);

return 0;
}

這邊是要我們好好用format string的弱點來洩漏flag。

用gdb找一找應該就可以看出來offset在哪了。

exp: fmt.py

hello

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char name[0x20] = "user";

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

int read_int(){
char buf[0x10];
read(0, buf, 0x10);
return atoi(buf);
}

void hello() {
puts("Hello, Hello, Hello~~~");
}

void editName(){
char input[0x20];
char y = '\0';
while (1){
puts("Enter your new name");
write(1, "> ", 2);
read(0, input, 0x80);
printf("Set fans name to %s (Y/N)\n", input);
y = getchar();
if (y == 'Y' || y == 'y'){
break;
}
}
strcpy(name, input);
puts("Name changed!");
}

int main(){
init();
puts("Welcome to the hello server, try to get the flag!\n");

int choice = 0;
while (1){
puts("1. Edit Name");
puts("2. Say Hello");
puts("3. Exit");
puts("Input your choice:");
choice = read_int();
switch (choice){
case 1:
editName();
break;
case 2:
hello();
break;
case 3:
exit(0);
break;
default:
puts("Invalid argument!!");
break;
}
}
return 0;
}

在editName那邊有一個迴圈可以輸入到input,他是用read(0, input, 0x80),而input只有開0x20。


看一下checksec,發現有開PIE,CANARY和NX,可能需要leak出ret address、canary和libc base,然後再用ret2libc來做。

首先gdb看一下,需要傳0x28 + 1個bytes才能leak出canary。

然後要執行system需要libc base,這支程式也沒有pop rdi; ret的gadget,所以要看一下程式執行到read的時候附近的記憶體有沒有在libc範圍內的地址,讓它在printf的時候洩漏出來。


可以看到libc的範圍在0x7ffff7c00000 ~ 0x7ffff7e1c000。


editName read的input會從rbp - 0x30的地方開始寫,我們就去看一下rbp - 0x30的地方有沒有在libc範圍內的地址。


我設了一個斷點到read的地方,然後輸入1111111111,在去看記憶體從rbp - 0x30開始往高記憶體有沒有在libc範圍內的地址,結果還真的有,就是這張圖裡面框起來的地方。

拿這個值減掉0x7ffff7c00000就可以拿到這個地方的offset是0x29d90,之後可以用read來leak出每次process跑起來的libc base了。

有了這些就可以拿libc裡面的pop rdi; ret gadget來做ret2libc了。

exp: hello.py

  1. shop
  2. magic
  3. ret2libc
  4. doll
  5. fmt
  6. hello