hitb-2017 1000levels writeup
来源:互联网 发布:js获得窗口宽度 编辑:程序博客网 时间:2024/06/02 00:57
题目分析
题目设置的还是比较巧妙的。
本身是一个二进制的文件,linux 64环境,保护情况如下:
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
功能一共三个:
1. go: 选择一个level,然后会再问你一次level,输入之后,回答两次level想加这么多次的问题,就是a * b类型,回答完成之后输出你在多少秒内完成了多少level
2. hint: 打印NO PWN NO FUN
3. give up: 退出
实现如下:
go
int go(void){ int v1; // ST0C_4@10 __int64 v2; // [sp+0h] [bp-120h]@1 int v3; // [sp+8h] [bp-118h]@9 __int64 v4; // [sp+10h] [bp-110h]@0 __int64 v5; // [sp+10h] [bp-110h]@4 signed __int64 v6; // [sp+18h] [bp-108h]@7 __int64 v7; // [sp+20h] [bp-100h]@10 puts("How many levels?"); v2 = read_num(); if ( v2 > 0 ) v4 = v2; else puts("Coward"); puts("Any more?"); v5 = v4 + read_num(); if ( v5 > 0 ) { if ( v5 <= 999 ) { v6 = v5; } else { puts("More levels than before!"); v6 = 1000LL; } puts("Let's go!'"); v3 = time(0LL); if ( (unsigned int)level(v6) != 0 ) { v1 = time(0LL); sprintf((char *)&v7, "Great job! You finished %d levels in %d seconds\n", v6, (unsigned int)(v1 - v3)); puts((const char *)&v7); } else { puts("You failed."); } exit(0); } return puts("Coward");}
其中level函数如下
__int64 __fastcall level(signed int a1){ __int64 result; // rax@2 __int64 inputs; // rax@8 char buf[32]; // [sp+10h] [bp-30h]@1 int answer; // [sp+30h] [bp-10h]@5 int num2; // [sp+34h] [bp-Ch]@5 int num1; // [sp+38h] [bp-8h]@5 int i; // [sp+3Ch] [bp-4h]@5 *(_QWORD *)buf = 0LL; *(_QWORD *)&buf[8] = 0LL; *(_QWORD *)&buf[16] = 0LL; *(_QWORD *)&buf[24] = 0LL; if ( a1 ) { if ( (unsigned int)level(a1 - 1) == 0 ) { result = 0LL; } else { num1 = rand() % a1; num2 = rand() % a1; answer = num2 * num1; puts("===================================================="); printf("Level %d\n", (unsigned int)a1); printf("Question: %d * %d = ? Answer:", (unsigned int)num1, (unsigned int)num2); for ( i = read(0, buf, 0x400uLL); i & 7; ++i ) buf[i] = 0; inputs = strtol(buf, 0LL, 10); result = inputs == answer; } } else { result = 1LL; } return result;}
总结来说,首先是问level,如果小于等于0了,输出coward,然后再问一次level,这次无论大小,直接加在第一次问的level上。 这里就有一个洞了,如果第一次给出的值小于等于0的话,这里的v4是没有初始化的。另外还有一个问题,就是第二次问level,并没有判断是不是小于0。
之后进入level,来生成问题判断答案是否正确。
level的实现使用了递归,在read answer的时候读取了0x400个字符,明显的栈溢出,不过这里需要注意,这里的栈溢出是没有办法使用partial write的,那个循环处理了partial write的情况。
hint
函数如下:
int hint(void){ signed __int64 v1; // [sp+8h] [bp-108h]@2 signed int v2; // [sp+10h] [bp-100h]@3 signed __int16 v3; // [sp+14h] [bp-FCh]@3 if ( show_hint ) { sprintf((char *)&v1, "Hint: %p\n", &system, &system); } else { v1 = 0x4E204E5750204F4ELL; v2 = 0x5546204F; v3 = 0x4E; } return puts((const char *)&v1);}
show_hint变量位于BSS,由于开启了PIE,是没法拿到地址的。 这里有个问题,只看C函数是看不出来的,我们来看汇编:
var_110 = qword ptr -110h.text:0000000000000CF0.text:0000000000000CF0 push rbp.text:0000000000000CF1 mov rbp, rsp.text:0000000000000CF4 sub rsp, 110h.text:0000000000000CFB ; 8: sprintf((char *)&v1, "Hint: %p\n", &system, &system);.text:0000000000000CFB mov rax, cs:system_ptr.text:0000000000000D02 mov [rbp+var_110], rax.text:0000000000000D09 ; 6: if ( show_hint ).text:0000000000000D09 lea rax, show_hint.text:0000000000000D10 mov eax, [rax].text:0000000000000D12 test eax, eax.text:0000000000000D14 jz short loc_D41.text:0000000000000D16 mov rax, [rbp+var_110].text:0000000000000D1D lea rdx, [rbp+var_110].text:0000000000000D24 lea rcx, [rdx+8].text:0000000000000D28 mov rdx, rax.text:0000000000000D2B lea rsi, aHintP ; "Hint: %p\n".text:0000000000000D32 mov rdi, rcx ; s.text:0000000000000D35 mov eax, 0.text:0000000000000D3A call _sprintf.text:0000000000000D3F jmp short loc_D66
这一段汇编是在进入分支之前的部分,所以system无论哪个分支,都会被放在栈上。
漏洞分析
根据刚刚对题目的分析,其实漏洞的点已经找到了:
1. go函数中的两次level询问,第一次如果小于等于0会导致本应该记录第一次询问的level结果的变量未初始化,第二次询问没有判断是否小于0
2. level函数存在栈溢出
3. hint函数始终会将system的值放在栈上
这么看两个漏洞有关联,但是还没办法结合,但是巧合的是,system在栈上的位置刚好和第一次询问记录level的v4变量位置重合。那么事情就好办了。
利用思路
- 使用hint,将system放在栈上
- 进入go,第一次给出小于等于0的值,使得v4=system的地址。
- 第二次询问,填0,可以导致最后可以进入system,但是参数不太好处理,所以可以使用one_gadget,那么第二次询问又不会判断大小,直接给出one_gadget和system地址的偏移,这样level值通过想加就变成了one_gadget的地址
- 完成999次回答
- 最后一次回答利用栈溢出,返回地址处填入vsyscall的gettimeofday(其实就是vsyscall的最开始位置)地址,填入3次(这里的三次是调试时候计算得出的),使得从返回地址位置一直到保存在栈上的one_gadget之间的位置全部填为gettimeofday
- 触发,搞定
总结
关于未初始化
以后做pwn的时候应该注意一下这种未初始化的情况,之前没怎么见过由栈上位置未初始化造成的问题,这次明显就忽略了这一点
关于vsyscall
vsyscall是以前linux内核使用的用来处理syscall的一个解决方案,后来被废弃,由vdso方案代替,但是这个方案由于历史原因保留了下来。
vsyscall的特点是在于其地址是固定的,所以可以用来在PIE+ASLR的情况中进行一定的利用。不过他的利用也有一些限制,vsyscall有一些固定的entry入口,内核在处理的时候会判断一下,如果执行的部分在vsyscall内,但是不是从entry入口开始的,会直接seg fault掉。
从这道题中可以看出来,vsyscall有这么一个用法,由于vsyscall直接进行syscall,并没有利用栈空间,所以在处理这种有栈溢出,但是由于PIE没有别的地址可以用,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链,这个rop链没有别的作用,就是用来ret,每次ret都会消耗掉一个地址,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。
- hitb-2017 1000levels writeup
- NJCTF 2017 web Writeup
- 2017TCTF RisingStar writeup
- BCTF 2017 WEB WriteUp
- 2017 SSCTF Writeup
- ISCC 2017 writeup(部分)
- 2017 bctf boj writeup
- 2017 rctf RNote2 writeup
- 2017 GCTF Web WriteUp
- 2017 GCTF writeup
- 2017 某校赛 Writeup
- 2017 火种CTF Writeup
- 2017ctf writeup
- Xp0intCTF 2017 writeup
- 2017湖湘杯Writeup
- 2017NJCTF get flag writeup
- 0ctf 2017 babyheap writeup
- google ctf 2017 inst_prof writeup
- nodejs(3):使用 Ant Design 开发web项目
- Python中的生产者与消费者 实现多线程
- java提高篇(十一)-----强制类型转换
- 【学习笔记】JQuery学习笔记
- sleep和wait区别
- hitb-2017 1000levels writeup
- [转]ubuntu 安装bcompare
- 【OpenCV开发之二】图像白平衡part_one
- DPDK-IP分片和重组库
- 协作方法——黑板模型
- 插入排序算法简介
- spring boot 集成 mybatis 入门级学习
- Android Material Design常用控件学习笔记
- Android开发总结: eclipse缺少Android XML File项