【第二届网鼎杯】使用angr帮pwn队友找到控制流

faster0

这道题一开始要求解哈希。解哈希之后拿到一个以base64字符串形式给出的压缩包。解压压缩包,并对文件进行upx -d之后,拖入ida,查看程序流程:
func100存在栈溢出漏洞,利用十分简单。但是在哪之前,有个大问题。
类似下面这种函数一共100个,从func000func099,必须依次执行。

__int64 func000()
{
  int v0; // eax
  __int64 result; // rax

  v0 = nums++;
  if ( v0 )
    exit(-1);
  switch ( (unsigned int)read_one() )
  {
    case 0u:
      result = func064();
      break;
    case 1u:
      result = func026();
      break;
    case 2u:
      result = func020();
      break;
    case 3u:
      result = func068();
      break;
    case 4u:
      result = func060();
      break;
    case 5u:
      result = func022();
      break;
    case 6u:
      result = func043();
      break;
    case 7u:
      result = func065();
      break;
    case 8u:
      result = func001();
      break;
    case 9u:
      result = func016();
      break;
    default:
      result = func000();
      break;
  }
  return result;
}

可以使用angr来找到输入流:

import angr
import sys

def main(argv):
    bin_path = argv[1]
    p = angr.Project(bin_path)
    init_state = p.factory.blank_state(addr = 0x4008A4)
    sim = p.factory.simulation_manager(init_state)
    addr = 0x6090AC
    num = init_state.solver.BVV(0,size = 64)
    init_state.memory.store(addr,num,endness = p.arch.memory_endness)

    sim.use_technique(angr.exploration_techniques.dfs.DFS())
    sim.explore(find = 0x405EF7,avoid = 0x4006B0)
    if sim.found:
        found_state = sim.found[0]
        result = found_state.posix.dumps(0)
        for i in result:
            if i not in b'0123456789':
                print('0')
            else:
                print(chr(i))
    else :
        print("cannot found a solution")

if __name__ == "__main__":
    main(sys.argv)

这里说明一下,代码中这句设置路径查找方式的代码是必不可少的,如果不设置这个方法,则脚本会因为耗尽虚拟机内存而被killed。
sim.use_technique(angr.exploration_techniques.dfs.DFS())
因为angr默认使用BFS来查找路径。而显然,这里DFS更为有利。

【第二届网鼎杯】Write-up

bang

apk,邦邦壳

从源哥那里嫖了个脱壳机,一健脱壳。脱壳之后一目了然

signal

ida 打开,看到是个弱虚拟机。angr直接秒了
workon angr
python3 exp.py ./signal.exe

import angr
import sys

def main(argv):
    bin_path = argv[1]
    p = angr.Project(bin_path)
    init_state = p.factory.entry_state()
    sim = p.factory.simulation_manager(init_state)

    def is_good(state):
        return b'good,The answer format is:flag {}' in state.posix.dumps(1)

    def is_bad(state):
        return   b'what a shame...' in state.posix.dumps(1) \
              or b'WRONG!\n' in state.posix.dumps(1)

    sim.explore(find = is_good ,avoid = is_bad)

    if sim.found:
        found_state = sim.found[0]
        print("Flag: {}".format(found_state.posix.dumps(0)))
    else :
        print("cannot found a solution")

if __name__ == "__main__":
    main(sys.argv)

jocker

脑洞题,孤儿出题人
动调找到这里先,a1是用户输入。可以直接得到flag的前一大半。

后面一小半说是藏起来了,但是没有任何逻辑和提示说明藏在那里。
经过推测,在解密后的finally函数里头找到一串没有用过的字符串。

该字符串通过一个十分特定的方式进行了加密。这里直接给出EXP。希望下一次不要再有这样的脑洞题。

enc_flag = [0x66,0x6B,0x63,0x64,0x7F,0x61,0x67,0x64,0x3B,0x56,0x6B,0x61,0x7B,0x26,0x3B,0x50,0x63,0x5F,0x4D,0x5A,0x71,0x0C,0x37,0x66]
data = [0x0E,0x0D,0x09,0x06,0x13,0x05,0x58,0x56,0x3E,0x06,0x0C,0x3C,0x1F,0x57,0x14,0x6B,0x57,0x59,0x0D]
tar = 'hahahaha_do_you_find_me?'
for i in range(19):
    for c in range(32,128):
        if (c^ord(tar[i])) == data[i]:
            print(chr(c),end = '')
key = 58 ^ ord('}')
enc_flag = [37,116,112,38]
for i in range(4):
    print(chr(enc_flag[i]^key),end = '')
print('}')

【CPP feature】vector.earse()可能造成的uaf & double free漏洞

晚上,V&N的队友Sinon提出关于De1CTF2020上一个有关vector.earse()造成的UAF & double Free漏洞。这里总结一下讨论结果:

问题来源

关于vector.earse()的issue
有CPP用户指出,vector<class ?>.earse()在擦除非末端元素的时候,并不会调用该元素的析构函数,而是调用了末端元素的析构函数

漏洞重现

首先我们假设存在下面这样一个类,在构造函数中通过malloc申请了一块内存,在析构函数中归还了这块内存。

class test{
public:
    test();
    ~test();
    void set_index(int i);//设置序号
    bool edit(char*);     //修改字符串的内容,漏洞利用
private:
    int reg;
    unsigned char* vulunablePointer;
};

//C++标准并不建议用户使用malloc来分配内存空间,而是使用符号new
test::test():reg(0),vulunablePointer(nullptr){
    vulunablePointer = (unsigned char*)malloc(0x80);
    if(!vulunablePointer)exit(0xbadbeef);
    strcpy((char*)vulunablePointer,"Pwn!");
};


//注意这里的析构函数,漏洞利用将从这里出发
test::~test(){
    cout<<"析构对象:"<<reg<<"    被free()释放的指针:"<<(void*)vulunablePointer<<'\n';
    free(vulunablePointer);
}

void test::set_index(int i){
    reg = i;
};

bool test::edit(char* newString){
    memset(vulunablePointer,0,0x80);
    strncpy((char*)vulunablePointer,newString,0x80);
}

然后思考一下,下面这个程序会搞出什么东西来

#include <iostream> 
#include <string>
#include <cstring> 
#include <vector>
#include <cstdlib> 

int main(){
    vector<test> vec(10);
    for(int i = 0;i < 10;++i){
        vec[i].set_index(i);
    } 
    system("pause");
    vec.erase(vec.begin()+1);
    vec.erase(vec.begin()+2);
    vec.erase(vec.begin()+3);
    system("pause");
    return 0;
}

输出如下:
显而易见的,不单单是UAF,double free也搞出来了。
除了上面这两个高危以外,还出现了内存泄漏,被earse的类并没有被析构掉(原因后面会讲)。
这在实际生产环境中是十分危险的。

请按任意键继续. . .
析构对象:9    被free()释放的指针:0x9a8490
析构对象:9    被free()释放的指针:0x9a8490
析构对象:9    被free()释放的指针:0x9a8490
请按任意键继续. . .
析构对象:0    被free()释放的指针:0x9a7dd0
析构对象:2    被free()释放的指针:0x9a7f50
析构对象:4    被free()释放的指针:0x9a80d0
析构对象:5    被free()释放的指针:0x9a8190
析构对象:7    被free()释放的指针:0x9a8310
析构对象:8    被free()释放的指针:0x9a83d0
析构对象:9    被free()释放的指针:0x9a8490

--------------------------------
Process exited after 1.99 seconds with return value 0
请按任意键继续. . .

漏洞成因:忽视stl的使用约定

通过翻阅stl的源码可以很快找到漏洞所在
先说明,这里看的源码是GCC 4.9.2版。不同版本的编译器,代码实现存在差异。

stl_vector.h

vector的类成员变量中有这么一个类:
其中三个类成员变量都是指针,前两个指向vector所申请的内存块的开始与结束位置。
先记住这个结构体,后面要用

struct _Vector_impl : public _Tp_alloc_type{
    pointer _M_start;
    pointer _M_finish;
    pointer _M_end_of_storage
    //.....下边是一大堆类成员函数
}_M_impl;

然后,我们找到vector.earse()的声明:
WTF?这个_M_earse()是个什么东西?

iterator erase(iterator __position){ 
    return _M_erase(__position); 
}

vector.tcc

_M_earse()的实现在vector.tcc中。

  template<typename _Tp, typename _Alloc>
    typename vector<_Tp, _Alloc>::iterator
    vector<_Tp, _Alloc>::
    _M_erase(iterator __position)
    {
      if (__position + 1 != end())
        _GLIBCXX_MOVE3(__position + 1, end(), __position);
      --this->_M_impl._M_finish;
      _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
      return __position;
    }

其中,_GLIBCXX_MOVE3这个宏调用了std::copy(InputIt first, InputIt last,OutputIt d_first)
std::copy详情
十分惹眼的,我们注意到了调用析构函数的元凶_Alloc_traits::destroy,指向vector最后一个元素的指针被作为参数传入其中
_Alloc_traits::destroy详情
接下来,我们读读源码,看看vector.earse()到底是如何工作的。
1. 判断被earse的元素是否是处于vector尾部的元素,如果不是,则把其后的元素全部复制过来,覆盖掉前面的元素
这里要注意!!!这个复制操作是bit-wise的!这个复制操作只是单纯的把类成员变量复制了过去,包括指针。但是指针所指向的内存并没有额外开辟一片出来。这就导致,如果对象中存在指针,则会出现两个指针指向同一个堆块的情况!
注意,被覆盖掉的那个元素,其析构函数并没有被调用!
2. 修改指向vector末尾元素的指针
3. 因为后边的元素被复制到前面一次,因此,末尾的元素出现重复,需要删掉那个多余的的元素。而对于一个对象而言,删除它的方法就是调用析构函数

ok,现在的问题是,析构函数中free掉的那个指针存在一个副本。而这个副本所属的对象任然是活跃的。高危漏洞就这么出来了。
(下面这个是Sinon画的流程图)

规避方法

It’s hard to find a solution

【javaFX】学习笔记(I)

JavaFX 入门笔记(I)

一、Helloworld

第一个java可视化程序。

Application是一个虚基类。其子类必须实现方法public void start(Stage) throws Exception

System.out.println("HelloWorld");将会输出在控制台,而不是窗口中。

import javafx.application.Application;
import javafx.stage.Stage;

public class javaFXTest extends Application{
  public static void main(String[] args){
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) throws Exception{
    System.out.println("Hello World");
    primaryStage.setTitle("Hello World");
    primaryStage.show();
  }
}

二、Helloworld.class的生命周期

修改Helloword.java,重写部分虚函数,编译并运行

从控制台中可以看到程序的运行顺序,以及线程的名字

init:准备工作可以放在其中

start:程序的主要功能实现

stop:结束程序时的收尾工作

若无需要,initstop可以不实现,但是start必须实现。

需要注意的是,所有的GUI组件必须写在start()方法内,因为只有该方法属于UI线程。

import javafx.application.Application;
import javafx.stage.Stage;

public class javaFXTest extends Application{
  public static void main(String[] args){
    System.out.println("main()," + Thread.currentThread().getName());
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) throws Exception{
    System.out.println("start()," + Thread.currentThread().getName());
    primaryStage.setTitle("Hello World");
    primaryStage.show();
  }

  @Override
  public void init() throws Exception{
    System.out.println("init()," + Thread.currentThread().getName());
  }

  @Override
  public void stop() throws Exception{
    System.out.println("stop()," + Thread.currentThread().getName());
  }
}

输出如下:

PS F:\javatest\javasockets> javac javaFXTest.java
PS F:\javatest\javasockets> java javaFXTest
main(),main
init(),JavaFX-Launcher
start(),JavaFX Application Thread
stop(),JavaFX Application Thread

三、初识Stage

一些Stage类的常用接口。详细的可以看用户手册

import java.util.Observable;

import javafx.application.Application;
import javafx.beans.value.*;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;

public class javaFXTest extends Application{
  public static void main(String[] args){
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) throws Exception{
    primaryStage.setTitle("Hello World");
    primaryStage.getIcons().add(new Image("./white.jpg"));//设置图标
    primaryStage.setOpacity(0.95);//设置透明度
  //  primaryStage.setAlwaysOnTop(false);//始终在最前方
  //  primaryStage.setX(100);//窗口出现时的位置,坐标为左上角的位置
  //  primaryStage.setY(100);
  //  primaryStage.setIconified(true);//设置最小化
  //  primaryStage.setMaximized(true);//设置最大化
  //  primaryStage.setWidth(500);//设置窗口大小
  //  primaryStage.setHeight(500);
  //  primaryStage.serMaxHeight(500);//设置窗口最大大小
  //  primaryStage.setMaxWidth(500);
  //  primaryStage.getWidth();//获取窗口大小
  //  primaryStage.getHeight();
  //  primaryStage.setResizable(false);//不可改变窗口大小

  //  primaryStage.setFullScreen(true);//设置全屏
  //  primaryStage.setScene(new Scene(new Group()));

    //监视窗口大小变化
    primaryStage.heightProperty().addListener(new ChangeListener<Number>(){
      @Override
      public void changed(ObservableValue<? extends Number>obervable,Number oldValue,Number newValue){
        System.out.println("当前高度:"+newValue.doubleValue());
      }
    });

    //监视窗口的位置
    primaryStage.xProperty().addListener(new ChangeListener<Number>() {
      @Override
      public void changed(ObservableValue<? extends Number> Observable,Number oldValue,Number newValue){
        System.out.println(newValue.doubleValue());
      }
    });

    primaryStage.show();
  //  primaryStage.close();//关闭
  }
}

四、多个Stage之间的权限控制、操作限制方法

当有多个窗口存在的时候

Stage.initOwner(Stage Mother)可以为目标窗口指定其所属对象

Stage.Modality(Modality)窗口或者子窗口可以通过该方法锁定用户对其它窗口的操作(类似保存文件,选目录的时候不能点击文本编辑页面)

import javafx.application.Application;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class javaFXTest extends Application{
  public static void main(String[] args){
    launch(args);
  }
  @Override
  public void start(Stage primaryStage) throws Exception{
    primaryStage.setTitle("Hello World");
    primaryStage.getIcons().add(new Image("./white.jpg"));//设置图标
    primaryStage.setOpacity(0.90);//设置透明度
    primaryStage.initStyle(StageStyle.DECORATED);//设置风格

    primaryStage.show();

    Stage s1 = new Stage();
    s1.setTitle("s1");
    s1.setHeight(500.0d);
    s1.setWidth(500.0d);
    s1.initOwner(primaryStage);//WINDOW_MODAL下锁死母窗口
    s1.initModality(Modality.WINDOW_MODAL);//在此窗口结束之前,无法进行其它操作
    //s1.initModality(Modality.APPLICATION_MODAL);//锁死整个程序
    s1.show();
  }
}

五、platform

任务队列,程序空闲时处理

    Platform.runLater(new Runnable(){
      @Override
      public void run() {
        //如果有些任务你不是很着急做,或者想延后做,可以将其添加到这里
      }
    });

关闭窗口后程序是否继续运行

    Platform.setImplicitExit(false);
    Platform.exit();//关闭后台运行的程序

检测运行环境是否支持xxx

Platform.isSupported(ConditionalFeature.GRAPHICS)
//可以检测的项目包含在ConditionalFeature这个类中

六、Screen类——获取屏幕信息

无需多言

    Screen screen = Screen.getPrimary();
    //获取整个屏幕的大小
    Rectangle2D rec2 = screen.getBounds();
    //获取屏幕可视大小
    Rectangle2D rec1 = screen.getVisualBounds();
    System.out.println(rec1.getMinY()+" "+rec1.getMinX());
    System.out.println(rec1.getMaxY()+" "+rec1.getMaxX());
    System.out.println(rec1.getHeight()+" "+rec1.getWidth());

七、scene类 和一些小知识

Gui的本质就是套娃

  1. 组件不能直接放在Stage上
  2. 组件需要放在scene上

scene类

//创建一个布局Group
    Group group = new Group();
    //创建一个标签
    Label label = new Label("Gstalker");
    //设置标签的鼠标响应
    label.setCursor(Cursor.DISAPPEAR);
    group.getChildren().add(label);

    //创建一个scene,以group为根节点
    Scene scene = new Scene(group);
    primaryStage.setScene(scene);

    //设置鼠标放到该场景上时的样子
    scene.setCursor(javafx.scene.Cursor.WAIT);

当然,鼠标还可以这么设置

    URL url = getClass().getClassLoader().getResource(binpath)
    String path = url.toExternalForm();
    widgt.setCursor(Cursor.cursor(path));

调用浏览器打开一个网页

    HostServices host = getHostServices();
    host.showDocument("http://139.155.83.108/");
    primaryStage.show();

八、Group组件 层次结构的开始

Group组件可以容纳多个Object对象

import java.net.URL;

import javafx.application.Application;
import javafx.application.ConditionalFeature;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class javaFXTest extends Application{
  public static void main(String[] args){
    launch(args);
  }
  @Override
  public void start(Stage primaryStage) throws Exception{
    primaryStage.setTitle("Hello World");
    primaryStage.getIcons().add(new Image("./white.jpg"));
    primaryStage.setOpacity(0.90);

    Button b1 = new Button("b1");
    //设置组件的绝对布局坐标
    b1.setLayoutX(value);
    b1.setLayoutY(value);
    //设置组件的固定大小
    b1.setPrefSize(prefWidth, prefHeight);
    //设置组件的透明度
    b1.setOpacity(0.90d);
    Button b2 = new Button("b2");
    Button b3 = new Button("b3");
    Group group = new Group();

    //group.getChildren().add(b1);
    //group.getChildren().add(b2);
    //group.getChildren().add(b3);
    //上面这三句和下面这一句是等同的
    group.getChildren().addAll(b1,b2,b3);
    //移除一个组件 
    group.getChildren().remove(b3);
    //该group内的组件是否自动设定大小
    group.setAutoSizeChildren(true);
    //检测该坐标点是否有组件
    System.out.println(group.contains(0,0));
    //取出该组中的所有元素
    Object[] obj = group.getChildren().toArray();
    //批量处理组件
    for(Object o : obj){
      Button b = (Button)o;
      b.setPrefHeight(value);
      //...
    }
    Scene scene = new Scene(group);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

九、Button类——按钮

RGB颜色标记

'#'+6位十六进制数代表颜色 + 2位十进制数代表透明度

Button 基本设置

    Button b1 = new Button("按钮1");
    b1.setLayoutX(100);
    b1.setLayoutY(200);
    b1.setPrefWidth(100);
    b1.setPrefHeight(60);
    //设置组件的字体,【字号】
    b1.setFont(Font.font("微软雅黑",10));
    //设置背景(颜色)
    BackgroundFill bgf = new BackgroundFill(Paint.valueOf("#8FBC8F90"),new CornerRadii(20),new Insets(2,2,2,2));//Insets:可以设置边框线距离按钮的范围
    Background bg = new Background(bgf);
    b1.setBackground(bg);
    //设置按钮边框颜色
    BorderStroke bos = new BorderStroke(Paint.valueOf("#8A2BE290"),BorderStrokeStyle.DASHED, new CornerRadii(20), new BorderWidths(2));
    Border bo = new Border(bos);
    b1.setBorder(bo);
    //设置文字颜色
    b1.setTextFill(Paint.valueOf("#CD000090"));

    //b1按键触发
    b1.setOnAction(new EventHandler<ActionEvent>(){

      @Override
      public void handle(ActionEvent event) {
        Button bu = (Button)event.getSource();
        System.out.println(bu.getText()+"被按下");
      }
    });

十、组件事件监测与相应,全局事件监测与组件响应

局部监测,当按钮被按下的时候做出对应的响应

//b1单击按钮事件触发
    b1.setOnAction(new EventHandler<ActionEvent>(){

      @Override
      public void handle(ActionEvent event) {
        Button bu = (Button)event.getSource();
        System.out.println(bu.getText()+"的单击事件");
      }
    });

全局监测,用户按下按键的时候,该组件做出对应响应(重要)

//监测,按下了什么按键
    b1.setOnKeyPressed(new EventHandler<KeyEvent>() {
      @Override
      public void handle(KeyEvent event){
        System.out.println("Pressed: "+event.getCode().getName());
      }
    });

    //监测,松开了什么按键
    b1.setOnKeyReleased(new EventHandler<KeyEvent>() {
      @Override
      public void handle(KeyEvent event){

        System.out.println("Released: "event.getCode().getName());
      }
    });

【DwagCTF】exploit.pcap分析过程(未完成)

写在前面:感谢CSU_XZLang,V&N_5k1l在解题过程中的帮助
这道题作为逆向来出……脑洞略大,而且十分的难受。我自认为我和队友们合作,能分析到这一步已经是基于目前知识的极限……

第一步,分析流量包

题目下载下来,发现是一个.pacp后缀的流量包。使用wireShark打开可以看到其中的内容,这里CSU_XZLang直接帮我把数据提取出来了。
显而易见,这是一个任意写漏洞的执行过程

搜索关键词? (1 = yes, 0 = no)1
,可以发现hacker向受害者写入了965个字节的数据。
我们把写入的数据提取出来,转化为16进制,按照偏移量排序列出来如下:
根据数据中所写的内存地址数据,我们推测受害者主机是linux系统。又根据linux系统定义的栈空间排布,我们不难推测出,程序被劫持的控制流的实际地址。
接下来,把codes写成二进制文件,拖入ida分析。

offset:任意写执行的时候所使用的指针的偏移量

offset:1001    0x3e9~0x746
<codes>          #运行时的实际地址:0x1181489

offset:352    0x160
13 1e 40 0 0 0 0 0
0 10 18 1 0 0 0 0
11 1e 40 0 0 0 0 0
0 20 0 0 0 0 0 0
37 13 0 0 0 0 0 0
8e ab 3f a3 25 7f 0 0
7 0 0 0 0 0 0 0             
80 d7 4d a3 25 7f 0 0       0x7F25A34DD780
89 14 18 1 0 0 0            0x1181489  #实际返回地址 √!

offset:-144   -0x90
88 12 43 a3 25 7f 0 0 
0 12 18 1 0 0 0 0       
a7 32 43 a3 25 7f 0 0    

offset:-12050568    -0xb7e088 #位于bss段内的一个变量
a7 32 43 a3 25 7f 0 0

第二步 分析恶意代码

整个任意写漏洞的执行过程中,写入的代码大约0x300字节。
分析恶意代码,我们首先得确定恶意代码的入口点在哪里
目光很快聚焦在sub_1F0这个具有完整栈帧的函数上。分析其工作流程,结合wireShark对流量包的分析,我们确定这个函数就是入口点。

__int64 sub_1F0()
{
  int v1; // [rsp+0h] [rbp-150h]
  char code; // [rsp+8h] [rbp-148h]
  char fd; // [rsp+130h] [rbp-20h]

  read(4i64, &v1);
  sub_165();
  read(32i64, &code);
  sub_1D(&fd, &code, 32i64);
  open(0i64, 0i64);
  memset(&code, 0, 0x20ui64);
  read(32i64, &code);
  sub_1D(&fd, &code, 32i64);
  return write(32i64, &fd);
}

sub_1F0调用了3次read,
第一次从stdin中读取4个字节,

第二次从stdin中读取32个字节,

第三次从open打开的本地文件中读取32个字节。这32个字节我们推测,就是flag
3次read之后调用了一次write,向stdout中写入了32个字节。我们推测,读取的flag经过未知加密后被write到了stdout中

注:hacker的端口是35724,受害者的端口是5001
经查询,该函数没有交叉引用,并且函数行为和数据包中的数据完全对应。由此,确定该函数就是恶意代码的入口点。

卡壳

在获取了以上信息以后,我们的分析工作就陷入了停滞。因为sub_1Dsub_165这两个函数并不具有完整的栈帧,并且疑似破坏了栈空间布局,我们无法继续分析下去。
它ret到了那里,我们没有线索,无从得知。
第一次read读取的4个字节,和第二次read读取的32个字节,我们也没能从恶意代码中追踪到其用途。很迷。总之就是很迷。

【DwagCTF】Reversing部分题解

asknicely

打开IDA,f5,发现flag()函数

int flag()
{
  putchar('D');
  putchar('a');
  putchar('w');
  putchar('g');
  putchar('C');
  putchar('T');
  putchar('F');
  putchar('{');
  putchar('+');
  putchar('h');
  putchar('@');
  putchar('n');
  putchar('K');
  putchar('_');
  putchar('Y');
  putchar('0');
  putchar('U');
  putchar('}');
  return putchar('\n');
}

dinner_party2

打开ida,f5,发现是一个cpp程序
其中v6 == 0XDEADDEAD会在程序实际运行的时候改变。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  DWORD NumberOfBytesRead; // [esp+2Ch] [ebp-20h]
  char Dst; // [esp+32h] [ebp-1Ah]
  int v6; // [esp+3Ch] [ebp-10h]
  HANDLE hFile; // [esp+40h] [ebp-Ch]

  sub_40BFF0();
  sub_4A39B0((int)&dword_4B0800, "Enter the password: \n");
  sub_469750(&dword_4B0620, (int)&v6);
  if ( v6 == 0xDEADDEAD )                       // 67305985
  {
    sub_4A39B0((int)&dword_4B0800, "Access Granted.\n");
    hFile = CreateFileW(L"C:\\flag.txt", 0x80000000, 1u, 0, 3u, 0x80u, 0);
    memset(&Dst, 0, 0xAu);
    ReadFile(hFile, &Dst, 0xAu, &NumberOfBytesRead, 0);
    puts(&Dst);
  }
  else
  {
    sub_4A39B0((int)&dword_4B0800, "Access Denied.\n");
  }
  return 0;
}

动态调试时,v6==0XDEADDEAD会变为v6==67305985。nc连接靶机,输入67305985即可获取flag

missyelliott

程序流程:获取用户输入,用户输入按字节取反,每个字节进行按位逆序,用户输入分组交换
所有的运算都是可逆运算。直接上exp:

enc_flag = """ 41 F5 51 D1 4D 61 D5 E9
69 89 19 DD 09 11 89 CB  9D C9 69 F1 6D D1 7D 89
D9 B5 59 91 59 B1 31 59  6D D1 8B 21 9D D5 3D 19
11 79 DD 00"""
enc_flag = enc_flag.replace('  ',' ')
enc_flag = enc_flag.replace('\n',' ')
enc_flag = enc_flag.replace(' ',',0x')

print(enc_flag)

enc_flag = [0x41,0xF5,0x51,0xD1,0x4D,0x61,0xD5,0xE9,0x69,0x89,0x19,0xDD,0x09,0x11,0x89,0xCB,0x9D,0xC9,0x69,0xF1,0x6D,0xD1,0x7D,0x89,0xD9,0xB5,0x59,0x91,0x59,0xB1,0x31,0x59,0x6D,0xD1,0x8B,0x21,0x9D,0xD5,0x3D,0x19,0x11,0x79,0xDD]

for i in range(21):
    a = enc_flag[i]
    enc_flag[i] = enc_flag[42 - i]
    enc_flag[42 - i] = a

for i in range(len(enc_flag)):
    ans = 0
    for j in range(8):
        if enc_flag[i] & (1<<j) != 0:
            ans = ans | 1<<(8-1-j)
    ans = ~ans
    enc_flag[i] = ans
print(enc_flag)
print(len([chr(i) for i in range(ord("*"), ord("z")+1)]))

ocean_boutique

这个题目挺有意思的。慢了一步,只拿了二血
首先说一下程序的意思,不然不好理解解题方法。

这个程序是一个情景故事。你的老板要带你坐飞机出远门,他吩咐了你几件事情:(序号是switch中的case标号)
3.要带什么物件,并统计物件重量和物品总价值(没有先选择4来说明腰带几个,则默认为1)
4.要带几个物件
6.付钱,等于物品总价值
8.确认订单
9.拿好小票

知道这个前置信息之后,我们来看看完成任务需要哪些条件:

  1. 用户输入的是什么?
    用户输入的是若干个二元组。op num。并且最后一组输入必须是9 10第一个数字用于确定操作,第二个数字是操作数
  2. cost == 3133742total_heavy<1000
    这是一个重点,解题的关键在于这里.首先看dword_2020E0处的结构体数组
    每一件物品我们都可以带若干个。要求是总开销 == 3133742,总重量小于1000
    这里有一个bug,在计算重量的时候,不论你带了多少件物品,都只计为一个物品的重量
    同时我们注意到有一个物品名为”Authentic Meme”,重量为1,价格为1,id为2,那我们只带它就好了
struct item{
    int id;
    int zero = 0;
    const char* name;
    int value;
    int heavy;
}items[6];
.data:00000000002020E0 dword_2020E0    dd 1                    ; DATA XREF: check_1+29↑o
.data:00000000002020E0                                         ; check_1+49↑o
.data:00000000002020E4                 dd 0
.data:00000000002020E8                 dq offset aPlushRetriever ; "Plush Retriever"
.data:00000000002020F0                 dd 133700
.data:00000000002020F4                 dd 8
.data:00000000002020F8                 dd 30
.data:00000000002020FC                 dd 0
.data:0000000000202100                 dq offset aCruiseShipTick ; "Cruise Ship Ticket"
.data:0000000000202108                 dd 1000
.data:000000000020210C                 dd 3
.data:0000000000202110                 dd 0Ah
.data:0000000000202114                 dd 0
.data:0000000000202118                 dq offset aRetrieverStick ; "Retriever Sticker"
.data:0000000000202120                 dd 42
.data:0000000000202124                 dd 1
.data:0000000000202128                 dd 2
.data:000000000020212C                 dd 0
.data:0000000000202130                 dq offset aAuthenticMeme ; "Authentic Meme"
.data:0000000000202138                 dd 1
.data:000000000020213C                 dd 1
.data:0000000000202140                 dd 2Ah
.data:0000000000202144                 dd 0
.data:0000000000202148                 dq offset aAcmeAnvil    ; "ACME Anvil"
.data:0000000000202150                 dd 2FD12Eh
.data:0000000000202154                 dd 3E9h
.data:0000000000202158                 dd 4Dh
.data:000000000020215C                 dd 0
.data:0000000000202160                 dq offset aLinkOfBlockcha ; "Link of Blockchain"
.data:0000000000202168                 dd 0C350h
.data:000000000020216C                 dd 0Ah
  1. 带上物品以后,要干什么
    确认订单,付钱,拿小票。

综上,可以得出我们需要的输入

nc连接目标靶机,输入以下字符即可

4 3133742
3 2
8 8
6 3133742
9 10

【BUUOJ】Reverse_WriteUp集合 0x00~0x10

持续更新中

0x01 [BJDCTF2020]JustRE

用IDA打开,搜索字符串,发现按如图所示格式串:

点进去,查看交叉应用,直接白给

0x02 [GXYCTF2019]simple CPP

首先对flag长度进行了检测,除去包装GXY{}后还有25个字符,一共30个字符。

程序一开始的时候将用户输入与i_will_check_is_debug_or_not进行异或

然后就是一坨屎一样的逻辑运算


推导过程如图

根据推导出来的结果,flag中有一个片段是无法逆出来的。比赛的时候给了hint,但是我这是在BUUOJ上补档,于是GG。

0x03 [De1CTF2019]Re_Sign

有丶复杂。
ida打开,先查找一下字符串,发现了base64的table,推测加密算法和base64有关
截图的时候是在动调。上面哪个flag字符串是我随便搞进去的。

代码太长,静态分析难以看懂,直接上动调。跟踪输入断电,找到主要函数sub_0x401000

int checker()
{
  char *v0; // eax
  char *v1; // eax
  HANDLE v2; // eax
  char *usr_input; // [esp-4h] [ebp-18h]
  char *v5; // [esp+0h] [ebp-14h]
  char *v6; // [esp+4h] [ebp-10h]
  void *v7; // [esp+8h] [ebp-Ch]
  void *v8; // [esp+Ch] [ebp-8h]
  void *lp; // [esp+10h] [ebp-4h]

  lp = 0;
  v8 = 0;
  usr_input = (char *)read_console(1, 0, 0, 0); // readline
  lp = usr_input;
  v0 = usr_input;
  if ( !usr_input )
    v0 = (char *)&unk_41E300;
  v7 = cpy_2_heap(1, v0, 0, 0x80000004);        // 创建一个堆块,并把用户输入及其长度信息放入其中
  v6 = sub_401233(&v7);                         // 加密算法
  if ( v7 )
    sub_402258(v7);
  v1 = v6;
  if ( !v6 )
    v1 = (char *)&unk_41E300;
  v5 = sub_402F80(1, *(double *)&v1, 0x80000004);
  if ( v6 )
    sub_402258(v6);
  if ( v8 )
    sub_402258(v8);
  v8 = v5;
  if ( sub_401F0A(&v8) )                        // 判断
    console_write(2u, 0, 0, 0, (unsigned int)aSuccess);
  else
    console_write(2u, 0, 0, 0, (unsigned int)aFail);
  v2 = read_console(1, 0, 0, 0);
  if ( v2 )
    sub_402258(v2);
  if ( lp )
    sub_402258(lp);
  if ( v8 )
    sub_402258(v8);
  return 0;
}

动调跟踪进加密算法中,发现两个奇怪的循环,循环次数都是0x3f次。熟悉base64的我此时发现了什么不对劲的地方。
图示的循环运行结束之后,内存中出现了另一个base64的table,这是什么情况?


继续跟踪程序,发现事情有点不对劲,后面又一个0x3f次的循环。这个循环运行结束之后,之前出现的table不见了??

直接跑完这个函数,看它返回了个什么东西。似乎是一个base64加密后的串?(注:图中的加密结果和前面截图中的字符串对应不上。毕竟不可能一轮动调就能出结果)
但是用python自带的base64去解,却无法还原回去。这时候我推测该加密算法是换表base64。
静态分析时内存中直接可见的table是提示,实际使用的table是之前那两个0x3f次循环搞出来的。(狠啊,使用完了还抹掉)

比较一下,验证我们的想法。
Zmxh == base64.b64encode(b'fla')
HefB则是题目中加密算法的输出结果。由此,我们确认了加密算法是换表base64,table是:
0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm+/

接下来就是找enc_flag了。动态调试分析sub_401F0A,发现里头有一个qmemcpy。点击迁怒复制对象看一下,是一些二进制数据。
然后下边有这么一个函数,是一个检测函数。该检测函数把加密后的用户输入转化为字母表序号之后和enc_flag进行比较。由此,我们可以得出enc_flag保存的是base64加密后的字符在原版table里头的位置。但是enc_flag加密是用换表base64加密出来的。

写个exp提取出来吧:

import base64# 获取加密串

enc_flag = [0x8, 0x3b, 0x1, 0x20, 0x7, 0x34, 0x9, 0x1f, 0x18, 0x24, 0x13, 0x3, 0x10, 0x38, 0x9, 0x1b, 0x8, 0x34, 0x13, 0x2, 0x8, 0x22, 0x12, 0x3, 0x5, 0x6, 0x12, 0x3, 0xf, 0x22, 0x12, 0x17, 0x8, 0x1, 0x29, 0x22, 0x6, 0x24, 0x32, 0x24, 0xf, 0x1f, 0x2b, 0x24, 0x3, 0x15, 0x41, 0x41]

table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='

table = list(table)

tar = ''

for i in enc_flag:
    tar += table[i-1]

print(tar)
//解密
//一年前写的,套来用。实际上base64有更简单的解码代码
#include<iostream>
#include<cstring>
#include<cctype>
using namespace std; 
int main(){
    char code[]="0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm+/="; 

    char b[]="H6AfGzIeXjSCP3IaHzSBHhRCEFRCOhRWHAohFjxjOeqjCU==";



    char flag[100];//由于与运算具有不可逆性,该题需要爆破处理 
    char group_0[100];//第一字符的数据可能解组 
    char group_2[100];//第三字符的数据可能解组 
    int count_0;//第一字符可能解的数量 
    int count_2;//第三字符可能解的数量 
    memset(flag,0,sizeof(flag));
    for(int i=0;i<(strlen(b)/4);++i){
        memset(group_0,0,sizeof(group_0));
        memset(group_2,0,sizeof(group_2));
        count_0=0;
        count_2=0;
        for(char n=32;n!=-1;++n){
            if((code[n>>2]==b[i*4])){
                group_0[count_0++]=n;
            }
            if((code[n&0x3F]==b[i*4+3])){
                group_2[count_2++]=n;
            }
        }
        for(int n=0;n<count_0;++n){
            for(int t=0;t<count_2;++t){
                for(char c=32;c!=-1;++c){
                    char first=code[((c&0xF0)>>4)|16*(group_0[n]&3)];
                    char second=code[((group_2[t]&0xC0)>>6)|4*(c&0xF)];
                    bool mark=(first==b[i*4+1])&&(second==b[i*4+2]);
                    if(mark){
                        cout<<group_0[n]<<c<<group_2[t];
                        goto done;
                    } 
                }
            }
        }
        done:;
    } 
    cout<<flag;
    return 0;
} 

【符号执行】angr入门笔记 part2

angr常用类

SimState

种类,创建和用途

常用的SimState对象有3种:
1. blank_state(**kwargs)
返回一个未初始化的state,此时需要主动设置入口地址,以及自己想要设置的参数。
2. entry_state(**kwargs)
返回程序入口地址的state,通常来说都会使用该状态
3. full_init_state(**kwargs)
entry_state(**kwargs) 类似,但是调用在执行到达入口点之前应该调用每个初始化函数

这三种对象通过angr.Project(bin_file).factory.xxxxx_state()创建,并作为angt.Project(bin_file).simgr(state)的参数来创建一个simulator对象。
在实际分析的过程中,我们根据实际情况选择其中的一种即可。

类成员与方法

官方文档上可以查询得到这部分的全部内容。这里整理一下常用的几个类成员/方法

initState = angr.Project('./test_bin').factory.blank_state()
###----我们假定./test_bin是32位的小端序elf文件----###
initState.regs # 一个结构体,包含了寄存器信息
initState.regs.eax #这个就不必多说了吧
initState.mem  # 一个类,包含了模拟的内存信息和内存读写方法
initState.solver #一个类,包含了约束求解引擎(claripy)
initState.solver.add_constraints(expression)#添加约束条件
initState.solver.And(BVS1,BVS2)#符号变量的与运算
initState.solver.Or(BVS1,BVS2)#符号变量的或运算
initState.solver.eval(BVS)#返回符号变量的结果,以数字返回
initState.solver.eval(BVS,cast_to = bytes)#返回符号变量的结果,以bytes返回
initState.posix #关于操作系统或环境模型的信息
initState.posix.dumps(num) #导出虚拟控制台中的输入输出内容的第num行

claripy.BVS() & claripy.BVV()

claripy是一个约束求解框架(类似于Z3)。angr引入claripy来求解目标向量。
claripy.BVS():符号变量
claripy.BVS()等价于initState.solver.BVS(),函数返回一个符号变量类
函数原型:

claripy.ast.bv.BVS(
    name, #变量的名字,是一个字符串,必须参数
    size, #变量的大小,是一个数字。大小指的是位长度,一个字节8个位(微笑),必须参数
    min = None,#变量的最小值,默认为不设置
    max = None,#变量的最大值,默认为不设置
    stride = None,#只用于值集分析
    uninitialized = False,#在分析中,这个变量的值是否被初始化
    explicit_name = None,
    discrete_set = False,
    discrete_set_max_card = None,
    **kwargs
)
  • claripy.BVV()符号常量
    claripy.BVV()等价于initState.solver.BVV(),函数返回一个符号常量类
    函数原型:
claripy.ast.bv.BVV(
    value,#符号常量的值
    size = None, #符号常量的大小(位长度)
    **kwargs
)

claripy.concat(*bitVectors1,bitVectors2)

这个函数用于拼接两个符号向量列表

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(28)]
    flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])

【MRCTF】补档:逆向部分题解

MRCTF补档

陆续更新中

Reverse

Transform

一个简单的映射+异或,直接写脚本打就行了

index = ' 9, 0Ah, 0Fh, 17h, 7, 18h, 0Ch, 6, 1, 10h, 3, 11h, 20h, 1Dh, 0Bh, 1Eh, 1Bh, 16h, 4, 0Dh, 13h, 14h, 15h, 2, 19h, 5, 1Fh, 8, 12h, 1Ah, 1Ch, 0Eh, 0, 0, 0, 0, 0, 0, 0, 0'
index = index.replace('h','')
index = index.replace(' ','0x')
print(index)
index = [0x9,0x0A,0x0F,0x17,0x7,0x18,0x0C,0x6,0x1,0x10,0x3,0x11,0x20,0x1D,0x0B,0x1E,0x1B,0x16,0x4,0x0D,0x13,0x14,0x15,0x2,0x19,0x5,0x1F,0x8,0x12,0x1A,0x1C,0x0E,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]

enc_flag = ' 67h, 79h, 7Bh, 7Fh, 75h, 2Bh, 3Ch, 52h, 53h, 79h, 57h, 5Eh, 5Dh, 42h, 7Bh, 2Dh, 2Ah, 66h, 42h, 7Eh, 4Ch, 57h, 79h, 41h, 6Bh, 7Eh, 65h, 3Ch, 5Ch, 45h, 6Fh, 62h, 4Dh, 3Fh'
enc_flag = enc_flag.replace('h','')
enc_flag = enc_flag.replace(' ','0x')
print(enc_flag)

enc_flag = [0x67,0x79,0x7B,0x7F,0x75,0x2B,0x3C,0x52,0x53,0x79,0x57,0x5E,0x5D,0x42,0x7B,0x2D,0x2A,0x66,0x42,0x7E,0x4C,0x57,0x79,0x41,0x6B,0x7E,0x65,0x3C,0x5C,0x45,0x6F,0x62,0x4D,0x3F]
print(len(enc_flag))
flag = [None] * (len(enc_flag)-1)

for i in range(len(enc_flag)):
    flag[index[i]] = chr(enc_flag[i] ^ index[i])
for i in range(len(enc_flag)-1):
    print(flag[i],end = '')

PixelShooter

打游戏爆出来的。
事后看别人wp,解压后找到Assembly-CSharp.dll,反编译,搜索字符串即可找到flag
说是unity游戏的主逻辑存放再Assembly-CSharp.dll

XOR

出题人在函数调用上做了点手脚,没办法decomplie
那就直接看汇编,反正不难
十分简单的异或,直接写exp打就行了。加密的flag位于byte_130EA08 - 1

enc_flag = '\x4dSAWB~FXZ:J:`tQJ"N@ bpdd}8g'
enc_flag = list(enc_flag)

for i in range(len(enc_flag)):
    print(chr(ord(enc_flag[i])^i),end = '')

hello_world_go

这个应该不是C语言写的,个人感觉有点像golang

IDA反编译,点开main函数后看到这坨玩意

 if ( v8 == 24 )
  {
    runtime_memequal(a1, a2, v6, (unsigned __int64)&unk_4D3C58);
    if ( v14 )
    {
      v11 = 0LL;
      goto LABEL_5;
    }
    v8 = 24LL;
  }

点进去unk_4d3c58就是flag了

很明显的能看到,这个程序的字符串不是Zero-Mark风格

人生第一顿go屎

EasyCPP

题目拿到手,发现是一个64位的ELF,大喜,终于可以用上IDA7.2了

ida打开一看,发现是C++11编写的

程序流程:

  1. std::cout<<“give me your key!";
  2. 用户需要输入9个int
  3. 9个int被存放在一个vector
  4. vector.for_each(main::{lambda(int &)#1),这9个int都被^=1
  5. depart(num, &str);,9个int分别被因数分解分解得到的因子以字符串形式储存
  6. {lambda(std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>> &)#1}::operator()将存放因子的字符串进行字符一对一替换反向替换表见exp
  7. v6 = (unsigned __int64){lambda(std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>,int)#2}::operator()对处理后的字符串进行strncmp,判断输入是否合法
  8. 根据用户输入是否合法,输出结果

解题思路:

动态调试->在程序流程的第7步处下断点,单步进入函数->找到string数组ans[abi:cxx11]->从中拷贝下答案字符串->写exp获取正确输入->flag{站长工具.MD5_32_uppercase(input_stream)}


def replace(tar): tar = tar.replace('O','0') tar = tar.replace('l','1') tar = tar.replace('z','2') tar = tar.replace('E','3') tar = tar.replace('A','4') tar = tar.replace('s','5') tar = tar.replace('G','6') tar = tar.replace('T','7') tar = tar.replace('B','8') tar = tar.replace('q','9') tar = tar.replace('=',' ') return tar def str2int(tar): tar = tar.split(' ') for i in range(len(tar)): tar[i] = int(tar[i],10) return tar def print_piece(tar): piece = 1 for i in range(len(tar)): piece = piece*tar[i] piece ^= 1 print(piece) #以下字符串都被删去了第一个 ‘=’ ,目的是方便处理数据 flag_piece = str2int(replace('lzzE')) print_piece(flag_piece) flag_piece = str2int(replace('ll=T=s=s=E')) print_piece(flag_piece) flag_piece = str2int(replace('zATT')) print_piece(flag_piece) flag_piece = str2int(replace('s=s=s=E=E=E')) print_piece(flag_piece) flag_piece = str2int(replace('EOll=E')) print_piece(flag_piece) flag_piece = str2int(replace('lE=T=E=E=E')) print_piece(flag_piece) flag_piece = str2int(replace('EsE=s=z')) print_piece(flag_piece) flag_piece = str2int(replace('AT=lE=ll')) print_piece(flag_piece)

【符号执行】angr入门笔记 Part1

angr_ctf 解题记录(0x00~0x0a)

这份记录本质上是一个笔记,angr_ctf题集是用来帮助新手入门angr的一套题目

题目来源,点击这里访问github

推广一下这个人的B站,2020年3月新鲜出炉的angr_ctf题解

angr项目地址,点击这里访问github

0x00_find

使用angr加载一个可执行二进制文件:p = angr.Project('./00_angr_find')

入口点数据:init_state = p.factory.entry_state()

以参数作为起点的模拟器对象:sim = p.factory.simulation_manager(init_state)

让模拟器找到通往目标地址的输入:sim.explore(find = 0x08048678)

如果找到了正确的输入流,则使用下面这两行代码来输出它

found_state = sim.found[0] # 控制台中出现的第一行数据,输入和输出都包括在sim.found内

print(found_state.posix.dumps(0))

0x01_avoid

上一题的exp完全可以用于这一题,通过穷举的方式找出flag。

但是实际的ctf竞赛中,不太可能用这种简单的穷举找出flag。我们需要手动找到题目中那些需要被回避的分支,然后通过avoid参数告诉simulator哪些分支可以被忽略

avoid参数可以是一个列表

sim.explore(find = 0x080485E0,avoid = [0x80485ef,0x80485a8])

这里有一个遗漏的地方。这道题可以通过设定字符串中包含的字符取值范围来进一步缩小穷举空间

问题在于如何设定这个取值范围。接口代码我还没找到,找到以后会补充上

0x02_find_condition

这一题引入了一个标准模板

sim.explore最多接受两个参数。

find可以是一个地址,也可以是一个bool函数

avoid可以是一个地址,可以是一个地址列表,也可以是一个bool函数

import angr
import sys

def main(argv):
    bin_path = argv[1]
    p = angr.Project(bin_path)
    init_state = p.factory.entry_state()
    sim = p.factory.simulation_manager(init_state)

    def is_good(state):
        return b'Good Job' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try_again' in state.posix.dumps(1)

    sim.explore(find = is_good ,avoid = is_bad)

    if sim.found:
        found_state = sim.found[0]
        print("Flag: {}".format(found_state.posix.dumps(0)))
    else :
        print("cannot found a solution")

if __name__ == "__main__":
    main(sys.argv)

0x03_symbolic_registers

截止这篇文章写下的日期:2020年3月31日,这道题已经可以使用上一题的模板直接打爆。但是这里我们可以使用另一种方式来处理该题(当作学习新的处理方法就好)

但是这道题和之前两道题有所不同。注意如下的输入函数:

int get_user_input()
{
  int v1; // [esp+0h] [ebp-18h]
  int v2; // [esp+4h] [ebp-14h]
  int v3; // [esp+8h] [ebp-10h]
  unsigned int v4; // [esp+Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  __isoc99_scanf("%x %x %x", &v1, &v2, &v3); // 【这里需要3个参数,早期的angr无法直接处理】
  return v1;
}

看汇编代码,我们注意到,v1,v2,v3在get_user_input结束之前被存放在了eax,ebx和edx中

.text:0804892A push    offset aXXX     ; "%x %x %x"
.text:0804892F call    ___isoc99_scanf
.text:08048934 add     esp, 10h
.text:08048937 mov     ecx, [ebp-0x18]
.text:0804893A mov     eax, ecx
.text:0804893C mov     ecx, [ebp-0x14]
.text:0804893F mov     ebx, ecx
.text:08048941 mov     ecx, [ebp-0x10]
.text:08048944 mov     edx

在更早的angr版本中,angr无法处理多个函数。需要我们手动处理一下

这里使用 claripy 模块来进行符号求解

import angr
import sys

import claripy     // angr-claripy:一个符号求解模块

def main(argv):
    p = angr.Project(argv[1])

    start_addr = 0x08048980 //获取用户输入之后,详见IDA—database

    init_state = p.factory.blank_state(addr = start_addr)

    pass1 = claripy.BVS('pass1',32)
    pass2 = claripy.BVS('pass2',32)
    pass3 = claripy.BVS('pass3',32)

    init_state.regs.eax = pass1    //目标:参数所在的寄存器
    init_state.regs.ebx = pass2
    init_state.regs.edx = pass3
    sim = p.factory.simulation_manager(init_state)

    def is_good(state):
        return b'Good Job' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try again' in state.posix.dumps(1)

    sim.explore(find = is_good ,avoid = is_bad)

    if sim.found:
        found_state = sim.found[0]

        password1 = found_state.solver.eval(pass1)
        password2 = found_state.solver.eval(pass2)
        password3 = found_state.solver.eval(pass3)

        print("Flag: {:x} {:x} {:x}".format(password1,password2,password3))
    else :
        print("cannot found a solution")

if __name__ == "__main__":
    main(sys.argv)

0x04_angr_symbolic_stack

这一题的主题是符号化栈空间,我们首先来看一下关键函数

可知,v2和v1就是我们要求解的目标变量。我们只需要通过符号化栈空间对其进行约束求解就行了

int handle_user()
{
  int result; // eax
  int v1; // [esp+8h] [ebp-10h]
  int v2; // [esp+Ch] [ebp-Ch]

  __isoc99_scanf("%u %u", &v2, &v1);
  v2 = complex_function0(v2);
  v1 = complex_function1(v1);
  if ( v2 == 0x773024D1 && v1 == 0xBC4311CF )
    result = puts("Good Job.");
  else
    result = puts("Try again.");
  return result;
}

符号化栈并不像符号化寄存器那样方便,我们需要还原栈空间布局。首先看看汇编代码(部分):

.text:08048679     public handle_user
.text:08048679     handle_user proc near
.text:08048679
.text:08048679     var_10= dword ptr -10h
.text:08048679     var_C= dword ptr -0Ch
.text:08048679
.text:08048679     ; __unwind {
.text:08048679 000 push    ebp
.text:0804867A 004 mov     ebp, esp
.text:0804867C 004 sub     esp, 18h
.text:0804867F 01C sub     esp, 4
.text:08048682 020 lea     eax, [ebp+var_10]
.text:08048685 020 push    eax
.text:08048686 024 lea     eax, [ebp+var_C]
.text:08048689 024 push    eax
.text:0804868A 028 push    offset aUU      ; "%u %u"
.text:0804868F 02C call    ___isoc99_scanf
.text:08048694 02C add     esp, 10h
.text:08048697 01C mov     eax, [ebp+var_C]

我们可以直接把分析的起始点设置在0x8048697,也就是调用了scanf之后。此时的栈空间分布如下

offset(exp based) 含义
ebp-0x0 前栈帧esp
ebp-0x4 undefined
ebp-0x8 undefined
ebp-0xC v2(汇编代码中的ebp+var_C)
ebp-0x10 v1(汇编代码中的ebp+var_10)

符号化栈空间使用的代码如下。其实不难理解,就是搞个虚拟的栈空间出来……

基本模拟了汇编代码的前几行

这里需要说明的是init_state.regs.esp -= 0x8,这一句的意思可以理解为忽略那两个undefined变量(预留空间)。(<-汇编基础好的同志不用管这句话,你们自己的理解肯定是对的)

p = angr.Project(argv[1])
start_addr = 0x8048697
init_state = p.factory.blank_state(addr = start_addr)

init_state.stack_push(init_state.regs.ebp)
init_state.regs.ebp = init_state.regs.esp
init_state.regs.esp -= 0x8

pass1 = init_state.solver.BVS('pass1',32) #构建变量,用于约束求解
pass2 = init_state.solver.BVS('pass2',32) #构建变量,用于约束求解

init_state.stack_push(pass1)              #约束变量放入栈中
init_state.stack_push(pass2)              #约束变量放入栈中

sim = p.factory.simgr(init_state)
#最后这一行代码等价于  sim = p.factory.simulation_manager(init_state)

完整的exp就不放了,太长。把上面这段套到前面的模板里头就行。

0x05_symbolic_memory

如果我们需要符号化的变量在bss段或者其它同时具有RW权限的段中,我们该怎么办?

这时候我们就需要符号化内存。符号化内存的代码复杂程度要小于符号化栈

首先来看看代码:scanf读入的四个变量都储存在bss段的内存空间中。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+Ch] [ebp-Ch]

  memset(user_input, 0, 0x21u);
  printf("Enter the password: ");
  __isoc99_scanf("%8s %8s %8s %8s", &unk_A1BA1C0, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8);// 输入储存在.bss段中,需要符号化内存数据
  for ( i = 0; i <= 31; ++i )
    *(_BYTE *)(i + 0xA1BA1C0) = complex_function(*(char *)(i + 0xA1BA1C0), i);
  if ( !strncmp(user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 0x20u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

由于思路比较简单,只需要在angr中符号化这几个内存变量并执行模拟即可。这里直接放个代码。

    start_addr = 0x8048601
    init_state = p.factory.blank_state(addr = start_addr)

    str1 = init_state.solver.BVS('str1',64)  #设定需要约束求解的变量
    str2 = init_state.solver.BVS('str2',64)
    str3 = init_state.solver.BVS('str3',64)
    str4 = init_state.solver.BVS('str4',64)

    str1_addr = 0x0A1BA1C0                   #变量地址
    str2_addr = 0x0A1BA1C8
    str3_addr = 0x0A1BA1D0
    str4_addr = 0x0A1BA1D8

    init_state.memory.store(str1_addr,str1)  #设定init_state
    init_state.memory.store(str2_addr,str2)
    init_state.memory.store(str3_addr,str3)
    init_state.memory.store(str4_addr,str4)

    sim = p.factory.simgr(init_state)

0x06_symbolic_dynomic_memory

如果目标变量被储存在由malloc等动态内存分配机制提供的内存空间中……

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v3; // ebx
  char *v4; // ebx
  int v6; // [esp-10h] [ebp-1Ch]
  int i; // [esp+0h] [ebp-Ch]

  buffer0 = (char *)malloc(9u);
  buffer1 = (char *)malloc(9u);
  memset(buffer0, 0, 9u);
  memset(buffer1, 0, 9u);
  printf("Enter the password: ");
  __isoc99_scanf("%8s %8s", buffer0, buffer1, v6);
  for ( i = 0; i <= 7; ++i )
  {
    v3 = &buffer0[i];
    *v3 = complex_function(buffer0[i], i);      // 当输入被放在堆区时,应该如何符号化?
    v4 = &buffer1[i];
    *v4 = complex_function(buffer1[i], i + 32);
  }
  if ( !strncmp(buffer0, "UODXLZBI", 8u) && !strncmp(buffer1, "UAORRAYF", 8u) )
    puts("Good Job.");
  else
    puts("Try again.");
  free(buffer0);
  free(buffer1);
  return 0;
}

我们需要手动指定一个未被使用的,固定的内存空间。

在直接使用init_state.memory.store()存放数据的时候,需要注意操作系统储存方式的问题。

p.arch.memory_endness变量代表当前程序的变量储存方式(大端序?小端序?)

import angr
import sys

def main(argv):
    p = angr.Project(argv[1])
    start_addr = 0x08048699  #scanf结束之后
    init_state = p.factory.blank_state(addr = start_addr)

    init_esp = 0xbadbeef     #这一块虚拟内存肯定没被使用。我们就假设malloc分配出来的内存在这里
    buf0 = init_esp - 0x100  #offset
    buf1 = init_esp - 0x200
    buf0_pointer = 0x0ABCC8A4  #存放指针的地址,在.bss段
    buf1_pointer = 0x0ABCC8AC

    #把“堆块地址”存入指针变量
    #这里如果不加上endness变量,最后将得不到正确的结果
    init_state.memory.store(buf0_pointer,buf0,endness = p.arch.memory_endness)
    init_state.memory.store(buf1_pointer,buf1,endness = p.arch.memory_endness)

    str0 = init_state.solver.BVS('str1',64)
    str1 = init_state.solver.BVS('str2',64)
    #把需要求解的向量放入“堆块”
    init_state.memory.store(buf0,str0)
    init_state.memory.store(buf1,str1)


    sim = p.factory.simgr(init_state)

    def is_good(state):
        return b'Good Job.' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try again.' in state.posix.dumps(1)

    sim.explore(find = is_good,avoid = is_bad)
    if sim.found:
        found_state = sim.found[0]
        pass0 = found_state.solver.eval(str0,cast_to = bytes).decode()
        pass1 = found_state.solver.eval(str1,cast_to = bytes).decode()
        print("flag: {} {}".format(pass0,pass1))
    else :
        print("404 not found!")

if __name__ == '__main__':
    main(sys.argv)

07_symbolic_file

现在考虑一下,如果变量是从文件中读取的。

我们需要构造一个符号化的文件

  //前面的代码可以无视。作者想表达的意思就是从文档中读入flag
  memset(buffer, 0, 0x40u);
  fp = fopen("OJKSQYDP.txt", "rb");
  fread(buffer, 1u, 0x40u, fp);    //从目标文件中读入0x40个字符
  fclose(fp);
  unlink("OJKSQYDP.txt");          //毁尸灭迹
                                   //notice:这里不是堆溢出的unlink漏洞,而是linux系统的api
  for ( i = 0; i <= 7; ++i )
    *(_BYTE *)(i + 134520992) = complex_function(*(char *)(i + 134520992), i);
  if ( strncmp(buffer, "AQWLCTXB", 9u) )
  {
    puts("Try again.");
    exit(1);
  }
  puts("Good Job.");

就直接放关键代码了。主要是找到接口并用上接口,没什么难的

    p = angr.Project(argv[1])
    start_addr = 0x080488D6 #程序创建了文件之后
    init_state = p.factory.blank_state(addr = start_addr)

    str0 = init_state.solver.BVS('str0',0x40) #求解向量
    file_name = "OJKSQYDP.txt"                #文件名字
    file_size = 0x40                          #文件大小
    sim_file = angr.storage.SimFile(file_name,content = str0,size = file_size)

    init_state.fs.insert(file_name,sim_file)  #向simulator中插入这个文件

08_constraints(条件约束)

前面7题算是基础题目,这一题开始算是进阶内容。

首先来看看代码。从代码中我们注意到,这一次的输入扩展到了16个字符,并且还有一个特殊的检查函数。

这个特殊的检查函数一个字符一个字符的进行检查。对于写逆算法解题的方法来说,这个函数平平无奇。但是对于angr来说,这样的函数十分致命,很可能导致路径爆炸。

_BOOL4 __cdecl check_equals_AUPDNNPROEZRJWKB(int a1, unsigned int a2);
int __cdecl main(int argc, const char **argv, const char **envp)
{
  signed int i; // [esp+Ch] [ebp-Ch]

  password = 'DPUA';
  dword_804A044 = 'RPNN';
  dword_804A048 = 'RZEO';
  dword_804A04C = 'BKWJ';
  memset(&buffer, 0, 0x11u);
  printf("Enter the password: ");
  __isoc99_scanf("%16s", &buffer);
  for ( i = 0; i <= 15; ++i )
    *(_BYTE *)(i + 134520912) = complex_function(*(char *)(i + 134520912), 15 - i);
  if ( check_equals_AUPDNNPROEZRJWKB((int)&buffer, 0x10u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

_BOOL4 __cdecl check_equals_AUPDNNPROEZRJWKB(int a1, unsigned int a2)
{
  int v3; // [esp+8h] [ebp-8h]
  unsigned int i; // [esp+Ch] [ebp-4h]

  v3 = 0;
  for ( i = 0; i < a2; ++i )
  {
    if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 134520896) )
      ++v3;
  }
  return v3 == a2;
}

什么是路径爆炸?截止目前所学的内容来说,咱没法跟你们解释其定义和成因。就结果而言,路径爆炸是angr的一个短板。这里直接放一份错误示例

#错误示例:这份exp会迅速消耗你的电脑资源!并且它在找到正确的答案之前就会暴毙!
#这份exp中的大部分内容在之前的题目中出现过
import angr
import sys

def main(argv):
    p =angr.Project(argv[1])
    start_addr = 0x08048625
    init_state = p.factory.blank_state(addr = start_addr)

    str0 = init_state.solver.BVS('str0',16*8)
    str0_addr = 0x0804A050
    init_state.memory.store(str0_addr,str0)

    #目标字符串
    init_state.memory.store(0x804a040,0x44505541)
    init_state.memory.store(0x804a044,0x52504E4E)
    init_state.memory.store(0x804a048,0x525A454F)
    init_state.memory.store(0x804a04c,0x424B574A)

    sim = p.factory.simgr(init_state)

    def is_good(state):
        return b'Good Job.' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try again.' in state.posix.dumps(1)

    sim.explore(find = is_good,avoid = is_bad)
    if(sim.found):
        found_state = sim.found[0]
        pass0 = found_state.solver.eval(str0,cast_to = bytes).decode()
        print("FLAG: {}".format(pass0))
    else:
        raise Exception("404 not found")


if __name__ =='__main__':
    main(sys.argv)

运行结果如下:这份py脚本跑了3分钟左右,最后由于内存空间占用过大,CPU资源使用过多而被强制结束。

(angr) gstalker@ubuntu:~/Desktop/angr_test/08_constraints$ python3 another_exp.py ./08_angr_constraints 
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-04-04 00:53:11,756 | angr.state_plugins.symbolic_memory | Filling register ebp with 4 unconstrained bytes referenced from 0x8048625 (main+0x72 in 08_angr_constraints (0x8048625))
Killed

那么思考一下,如何让simulator在模拟过程中规避由于函数check_equals_AUPDNNPROEZRJWKB()导致的路径爆炸?

通过逆向工程,我们可知函数check_equals_AUPDNNPROEZRJWKB()的功能是把经过处理之后的用户输入与字符串AUPDNNPROEZRJWKB进行逐个字符的比较。当且仅当两个字符串完全一致的时候才返回true。既然知道了这样的比较逻辑,我们让angr直接对两个字符串进行整体比较,不必让angr继续分析对比函数。

import angr
import sys
def main(argv):
    p = angr.Project(argv[1])
    start_addr = 0x08048625
    init_state = p.factory.blank_state(addr = start_addr)

    str0 = init_state.solver.BVS('str0',16*8)
    buf_addr = 0x0804A050
    init_state.memory.store(0x0804A050,str0)

    sim = p.factory.simgr(init_state)

    ########################关键代码############################
    check_addr = 0x08048565       #chekc_equals()函数地址
    sim.explore(find = check_addr)
    if sim.found:
        check_state = sim.found[0]

        target = 'AUPDNNPROEZRJWKB'
        check_param1 = buf_addr
        check_param2 = 0x10

        #读取状态内存,就是把转换后的字符串读取出来
        check_bvs = check_state.memory.load(check_param1,check_param2)

        #条件
        check_constraint = target == check_bvs
        #将条件加入state中
        check_state.add_constraints(check_constraint)
        #命令solver解方程
        pass0 = check_state.solver.eval(str0,cast_to = bytes).decode()
        print("FLAG: {}".format(pass0))

if __name__ == '__main__':
    main(sys.argv)

09_hooks(钩子)

08_constraints中的做法是用户手动分析了最后一步,simulator并没有继续运行下去。但是在实际分析中,往往有很多函数是需要跳过或者替换掉的。

这种时候就需要用到hook

钩子是什么?了解windows编程的应该多多少少知道hook机制。angr也提供了类似的机制让用户来指定跳过几条汇编指令替换其实现

首先看看题目代码。这一次用户需要输入32个字符,并且用户输入被分为了两个16bytes的组,其中一个16bytes使用08_constraints中的check函数。

我们的目标就是使用angr的hook机制替换掉call check_equals_XYMKBKUHNIQYNQXE指令的功能

int __cdecl main(int argc, const char **argv, const char **envp)
{
  _BOOL4 v3; // eax
  signed int i; // [esp+8h] [ebp-10h]
  signed int j; // [esp+Ch] [ebp-Ch]

  qmemcpy(password, "XYMKBKUHNIQYNQXE", 16);
  memset(buffer, 0, 0x11u);
  printf("Enter the password: ");
  __isoc99_scanf("%16s", buffer);
  for ( i = 0; i <= 15; ++i )
    *(_BYTE *)(i + 134520916) = complex_function(*(char *)(i + 134520916), 18 - i);
  equals = check_equals_XYMKBKUHNIQYNQXE(buffer, 16);
  for ( j = 0; j <= 15; ++j )
    *(_BYTE *)(j + 134520900) = complex_function(*(char *)(j + 134520900), j + 9);
  __isoc99_scanf("%16s", buffer);
  v3 = equals && !strncmp(buffer, password, 0x10u);
  equals = v3;
  if ( v3 )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

exp如下,后附对其中关键代码的讲解:

import angr
import sys
import claripy
def main(argv):
    p = angr.Project(argv[1])
    init_state = p.factory.blank_state()

    check_addr = 0x80486b3
    check_skip_size = 5

    @p.hook(check_addr,length = check_skip_size)
    def check_hook(state):
        usr_input_addr = 0x0804A054
        usr_input_length = 0x10

        usr_input_bvs = state.memory.load(
            usr_input_addr,
            usr_input_length
        )
        desired = 'XYMKBKUHNIQYNQXE'
        state.regs.eax = claripy.If(
            desired == usr_input_bvs,
            claripy.BVV(1,32),
            claripy.BVV(0,32)
        )

    sim = p.factory.simgr(init_state)
    def is_good(state):
        return b'Good Job.' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try again.' in state.posix.dumps(1)
    sim.explore(find = is_good,avoid = is_bad)
    if(sim.found):
        found_state = sim.found[0]
        print("FLAG: {}".format(found_state.posix.dumps(0).decode()))
    else:
        raise Exception("404 not found")


if __name__ == '__main__':
    main(sys.argv)
  1. @p.hook(check_addr,length = check_skip_size)

    设定Project,在simulator分析到check_addr时,跳过长度为check_skip_size个字节的指令,并返回一个state对象作为check_hook(state)的参数

  2. check_hook(state):

    被替换的目标函数。返回值放在eax中(state.regs.eax = claripy.If()

0x0a_simprocedures

09_hooks中,我们指定了一个call指令来hook。不过并不是任何时候我们都能这么精准的找到目标指令。10_simprocedures中出现了控制流平坦化。从汇编代码中我们很难看出调用函数的call指令到底在哪里。这时我们就没法用09_hooks的exp来打这道题。

由于这道题的平坦化方式较为单一,IDA可以轻易识别并忽略掉其中的多余分支,反编译出来的伪码十分间接可读,我们先从读伪码入手:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  signed int i; // [esp+20h] [ebp-28h]
  char s[17]; // [esp+2Bh] [ebp-1Dh]
  unsigned int v6; // [esp+3Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  memcpy(&password, "ORSDDWXHZURJRBDH", 0x10u);
  memset(s, 0, 0x11u);
  printf("Enter the password: ");
  __isoc99_scanf("%16s", s);
  for ( i = 0; i <= 15; ++i )
    s[i] = complex_function(s[i], 18 - i);
  if ( check_equals_ORSDDWXHZURJRBDH((int)s, 0x10u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

从伪码的结果来看,我们只需要hook掉check_equals_ORSDDWXHZURJRBDH()函数即可。

exp如下,关键代码解释在后边:

import angr
import sys
import claripy

def main(argv):
    p = angr.Project(argv[1])
    init_state = p.factory.blank_state()

    class mySimPro(angr.SimProcedure):
        def run(self,user_input_addr,user_input_length):
            angr_bvs = self.state.memory.load(
                user_input_addr,
                user_input_length
            )
            target = 'ORSDDWXHZURJRBDH'

            return claripy.If(
                target == angr_bvs,
                claripy.BVV(1,32),
                claripy.BVV(0,32)
            )


    check_symbol = 'check_equals_ORSDDWXHZURJRBDH'
    p.hook_symbol(check_symbol,mySimPro())

    sim = p.factory.simgr(init_state)

    def is_good(state):
        return b'Good Job.' in state.posix.dumps(1)

    def is_bad(state):
        return b'Try again.' in state.posix.dumps(1)
    sim.explore(find = is_good,avoid = is_bad)
    if(sim.found):
        found_state = sim.found[0]
        print("FLAG: {}".format(found_state.posix.dumps(0).decode()))
    else:
        raise Exception("404 not found")

if __name__ == '__main__':
    main(sys.argv)
  1. p.hook_symbol(check_symbol,mySimPro())
  • 参数1 check_symbol:

    被hook的函数的符号(名字),前提是源程序被编译时保留了符号表信息

  • 参数2 mySimPro()

    被hook的函数的替换实现。这是一个类。声明方式见exp

  1. def run(self,user_input_addr,user_input_length):

    被hook函数的替换实现。除去self以外的参数,从左到右分别对应原函数接受的参数

toBeContinue