100levels
题目
检查1
2
3
4
5Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
两个功能: go hint
go: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
47int __fastcall go(__int64 a1, __int64 a2)
{
int v3; // ST0C_4
__int64 v4; // [rsp+0h] [rbp-120h]
__int64 num2; // [rsp+0h] [rbp-120h]
int v6; // [rsp+8h] [rbp-118h]
__int64 num1; // [rsp+10h] [rbp-110h]
signed __int64 num1a; // [rsp+10h] [rbp-110h]
signed __int64 cnt; // [rsp+18h] [rbp-108h]
__int64 v10; // [rsp+20h] [rbp-100h]
puts("How many levels?");
v4 = get_num();
if ( v4 > 0 )
num1 = v4;
else
puts("Coward");
puts("Any more?");
num2 = get_num();
num1a = num1 + num2;
if ( num1a > 0 )
{
if ( num1a <= 99 )
{
cnt = num1a;
}
else
{
puts("You are being a real man.");
cnt = 100LL;
}
puts("Let's go!'");
v6 = time(0LL);
if ( (unsigned int)check(cnt) != 0 )
{
v3 = time(0LL);
sprintf((char *)&v10, "Great job! You finished %d levels in %d seconds\n", cnt, (unsigned int)(v3 - v6), num2);
puts((const char *)&v10);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward Coward Coward Coward Coward");
}
go调用的check1
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_BOOL8 __fastcall check(signed int cnt)
{
int v2; // eax
__int64 v3; // rax
__int64 buf; // [rsp+10h] [rbp-30h]
__int64 v5; // [rsp+18h] [rbp-28h]
__int64 v6; // [rsp+20h] [rbp-20h]
__int64 v7; // [rsp+28h] [rbp-18h]
unsigned int v8; // [rsp+34h] [rbp-Ch]
unsigned int num2; // [rsp+38h] [rbp-8h]
unsigned int num1; // [rsp+3Ch] [rbp-4h]
buf = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
if ( !cnt )
return 1LL;
if ( (unsigned int)check(cnt - 1) == 0 )
return 0LL;
num1 = rand() % cnt;
v2 = rand();
num2 = v2 % cnt;
v8 = v2 % cnt * num1;
puts("====================================================");
printf("Level %d\n", (unsigned int)cnt);
printf("Question: %d * %d = ? Answer:", num1, num2);
read(0, &buf, 0x400uLL); // overflow
v3 = strtol((const char *)&buf, 0LL, 10);
return v3 == v8;
}
hint1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int hint()
{
signed __int64 v1; // [rsp+8h] [rbp-108h]
int v2; // [rsp+10h] [rbp-100h]
__int16 v3; // [rsp+14h] [rbp-FCh]
if ( flag )
{
sprintf((char *)&v1, "Hint: %p\n", &system, &system);
}
else
{
v1 = 'N NWP ON';
v2 = 'UF O';
v3 = 'N';
}
return puts((const char *)&v1);
}
分析
思路一
容易发现的漏洞就是check时的溢出,还有问题就是go初始时输入两次值,最后使用的是两次加起来的值,但是如果第一次输入的值小于等于0就不会初始化第一个值。
最刚开始的想法是通过溢出修改存储在bss段的flag位然后返回到主函数,再使用hint泄露system函数地址
但是因为程序开启了pie,因此bss段位置也就是不定的,而如果想要泄露libc,又因为是64位程序,因此需要gadget,但是gadgets的位置也因为pie变得位置不定,所以行不通。
再观察程序,发现:
hint函数是先将system函数地址读到了栈上,之后调用fprintf函数输出的,其地址为rbp-0x1101
2
3
4var_110 = qword ptr -110h
mov rax, cs:system_ptr
mov [rbp+var_110], rax
而如果go读取的第一个数没有初始化的话,他的值就是栈上的值,而它的地址恰巧也是rbp-0x1101
__int64 num1; // [rsp+10h] [rbp-110h]
所以我们按理说是可以利用那个对大小判断的函数来逐位爆破system的地址
这里需要注意的是如果和大于0进入check,如果顺着令其运行完会直接退出,所以也需要在check中溢出覆盖返回地址以便能够继续爆破
而覆盖的返回地址依旧是不确定的,观察程序运行时的内存映射,发现程序的最后vsyscall段是恒定不变的,我们可以利用它来绕过pie;1
2
3
4
5
6pwndbg> x/10i 0xffffffffff600400
0xffffffffff600400: mov rax,0xc9
0xffffffffff600407: syscall
0xffffffffff600409: ret
0xffffffffff60040a: int3
0xffffffffff60040b: int3
思路二
思路一没有复现成功,看到了大佬们的简单一点的思路
前面基本一致,但是不再去爆破system的地址,而是将其覆盖位one_gadget的地址,也就是计算二者偏移的差值然后将其加上去直接得到one_gadget RCE,可以看到这个地址被存放在了栈上,我们的目标就是在后面的栈溢出中想办法使得返回到此处。
执行最后一次时的栈情况:1
2
3
4
5
6
7
803:0018│ rsi 0x7ffe36cd7780 ◂— 0x0
... ↓
07:0038│ 0x7ffe36cd77a0 ◂— 0x1ae00000000
08:0040│ 0x7ffe36cd77a8 ◂— 0x2b0000000a /* '\n' */
09:0048│ rbp 0x7ffe36cd77b0 —▸ 0x7ffe36cd78e0 —▸ 0x7ffe36cd7920 —▸ 0x556f0672efd0 ◂— push r15
0a:0050│ 0x7ffe36cd77b8 —▸ 0x556f0672ec8a ◂— test eax, eax
0b:0058│ 0x7ffe36cd77c0 ◂— 0xfffffffffffffeda
0c:0060│ 0x7ffe36cd77c8 ◂— 0x556f5caa9f45 -------》 one_gadget 在这里
我们就可以使用vsyscall中的不变量来使返回地址滑向此处执行one_gadget
- 关于 vsyscall
简单地说,现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall.
exp
1 | from pwn import * |
未成功的思路一
1 | from pwn import * |
参考:
https://bbs.ichunqiu.com/thread-43627-1-1.html
https://nandynarwhals.org/hitbgsec2017-1000levels/
https://znqt.github.io/hitb-gsec-pwn-1000levels/
monkey
题目 & 分析 & exp
算是js的沙箱逃逸,给了个The SpiderMonkey shell,可以查询到它的源码之类的东西,
可以发现其没有任何过滤,所以给个os.system(“/bin/sh”) 直接拿shell即可
Escape_From_Jail-50
题目
python沙箱逃逸,只给了个远程连接,检测到被过滤的字符会报错
banned:1
. / ' / import /flag /eval /exec / dir(__builtins__)无反馈 等等
分析 & exp
许多常见的基本上都被过滤了
不能使用import关键字且不允许使用 “.”,一大部分路就被绝了,
os,未被过滤,但是基本调用需要”.”,所以需要其他方式。
查询资料发现 getattr函数可以使用
相关资料可以看菜鸟:http://www.runoob.com/python/python-func-getattr.html
getattr(os,”system”)(“/bin/sh”)
time_formatter
比较有意思的一个题目,刚开始没看懂题。。
题目
重点函数
1 | __int64 __fastcall print(__int64 a1, __int64 a2, __int64 a3) |
退出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20signed __int64 __noreturn exit()
{
signed __int64 result; // rax
char s; // [rsp+8h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-10h]
v2 = __readfsqword(0x28u);
free__(ptr);
free__(value);
__printf_chk(1LL, "Are you sure you want to exit (y/N)? ");
fflush(stdout);
fgets(&s, 16, stdin);
result = 0LL;
if ( (s & 0xDF) == 89 )
{
puts("OK, exiting.");
result = 1LL;
}
return result;
}
分析&&exp
问题出在退出时先free然后再询问是否退出,这个时候选择不退出的话就会出现UAF漏洞
在system中执行command命令,command字符串通过snprintf_chk函数拼接起来,可以百度一下这个函数的用法,注意字符串/bin/date -d @%d +’%s’,按照linux 64的函数传参顺序,分别是rdi rsi rdx rcx r8 r9 然后是栈 ,所以此时%s对应第一个入栈的参数,也就是rax 也就是qword_602118,所以只要控制了qword_602118,就可以执行任意系统命令(注意闭合单引号)。
所以依次执行 1,5 / N ,3 / ‘;/bin/sh#’ , 4 即可
babyheap
题目&&分析
正常题目,edit时没有检查存储的size
所以虽然保护全开,我们仍然可以使用溢出来overlap来泄露地址及其他
exp
1 | from pwn import * |