鉴定主任's profile信息产业部专业鉴定室PhotosBlogLists Tools Help

Blog


    April 25

    Sparc 平台上的 FlexLM 7.0 用户滤波函数分析

    Sparc 平台上的 FlexLM 7.0 用户滤波函数分析
     
    作为一个比较老旧的保护工具,FlexLM的初级保护已经被分析的差不多了,默认情况下内存中能截到明文的签名,这点很多高手的文章中都有提到。但俺近日在分析一款SunOS Sparc平台下的FlexLM 7.0时遇上了 user_crypt_filter 机制,这种机制会对签名加以变形运算,导致内存中不会出现明文,给破解的捕获增加了一些难度。后来参考Nolan Blender大牛的文章,总算破掉了这个用户滤波函数的保护,在此写出来总结一下供有相同需要的朋友参考与讨论。
    本文没有提到到具体的软件名称,也不涉及到Feature、Vendor与版本号的跟踪捕获,甚至也和抓取VendorKey和加密种子等没有关系。另外Sparc平台的汇编可能大伙都不熟悉,这儿尽量多加以解释。
    工具:DDD+GDB、IDA
     
    一、按传统方法定位明文比较处。
     
    参考laoqian的《制作Flexlm license总结》的文章,以及laowanghai的《LabWindows CVI 8.0》中列出来的详尽x86汇编代码,通过在IDA中搜索常数66D8B337,可以定位到几个地方。以下是laowanghai的《LabWindows CVI 8.0》文章中列出的部分x86的代码供参考:
     
    105C9CDB      894D F4                    mov dword ptr ss:[ebp-C],ecx
    105C9CDE    ^ E9 EEFAFFFF                jmp CVI_1.105C97D1
    105C9CE3      817D 18 37B3D866           cmp dword ptr ss:[ebp+18],66D8B337
    105C9CEA      0F85 96000000              jnz CVI_1.105C9D86
    105C9CF0      33D2                       xor edx,edx
    ……
    105C9D86      C785 78FEFFFF 08000000     mov dword ptr ss:[ebp-188],8
    105C9D90      817D 18 37B3D866           cmp dword ptr ss:[ebp+18],66D8B337
    105C9D97      75 0F                      jnz short CVI_1.105C9DA8
    ……
    105C9EB0      8B95 30FEFFFF              mov edx,dword ptr ss:[ebp-1D0]
    105C9EB6      81E2 FF000000              and edx,0FF
    105C9EBC      8B85 7CFEFFFF              mov eax,dword ptr ss:[ebp-184]
    105C9EC2      33C9                       xor ecx,ecx
    105C9EC4      8A88 98358A10              mov cl,byte ptr ds:[eax+108A3598]
    105C9ECA      3BD1                       cmp edx,ecx                     ; 与正确SIGN逐字节的比较
     
    这里,比较之处前面一个and edx,0FF比较的显目,可以作为一个小标志。
    类似的,在我们对付的SPARC平台下的软件,其比较代码也类似:
     
     loc_3515CC:                             ! CODE XREF: sub_3508E4+6B0j
    .text:003515CC                                         ! sub_3508E4+CE0j
    .text:003515CC                 ld      [%fp+arg_54], %o5
    .text:003515D0                 set     0x66D8B337, %o7 ! 绝对是这儿的常数
    .text:003515D8                 cmp     %o5, %o7
    .text:003515DC                 be      loc_3515EC
    .text:003515E0                 nop
    ……
     loc_351684:                             ! CODE XREF: sub_3508E4+D00j
    .text:00351684                 mov     8, %o7          ! 也是这儿的常数
    .text:00351688                 st      %o7, [%fp+var_8]
    .text:0035168C                 ld      [%fp+arg_54], %l0
    .text:00351690                 set     0x66D8B337, %l1
    .text:00351698                 cmp     %l0, %l1
    .text:0035169C                 be      loc_3516AC
    .text:003516A0                 nop
    ……
    .text:003517F8 loc_3517F8:                             ! CODE XREF: sub_3508E4+EE0j
    .text:003517F8                 ld      [%fp+var_4_idx], %o3
    .text:003517FC                 set     byte_4DD515, %o4
    .text:00351804                 ldub    [%o3+%o4], %o5  ! ldub取一位字节,相当于同时做了上面的and 0xff了
    .text:00351808                 ldub    [%fp+var_180_oursign+3], %o7
    .text:0035180C                 cmp     %o7, %o5        ! 是这儿的比较
    .text:00351810                 bne     loc_351820
    .text:00351814                 nop
    .text:00351814
     
    由x86与Sparc的汇编代码对比可以看出,.text:0035180C处确实是我们传入的伪造签名值(我传的是1234567890AB)与正确签名值的比较之处,byte_4DD515这个地址所指的一片内存区域,应该是我们需要的正确签名。于是在0035180C处下断,第一次来到此处时查看0x4DD515地址的内容,得到六个字节EEB53723B248。想当然地拿它放到license文件里头一试,很不幸,通不过。
    于是开始了摸索,在0035180C之前下断,逐步来,发现0035180C之前有一处很关键的代码:
     
    .text:003517CC loc_3517CC:                             ! CODE XREF: sub_3508E4+ED8j
    .text:003517CC                 ld      [%fp+arg_44_job], %o0
    .text:003517D0                 ld      [%o0+0x234], %g1 ! Job偏移234处是啥?原来是user_crypt_filter函数地址。
    .text:003517D4                 ld      [%fp+var_4_idx], %o1
    .text:003517D8                 set     byte_4DD515, %o2
    .text:003517E0                 add     %o1, %o2, %o1   ! char*,指向正确的未filter的内容
    .text:003517E4                 ldub    [%fp+var_180_oursign+3], %o3 ! 我们的sign的字符
    .text:003517E8                 ld      [%fp+arg_44_job], %o0 ! Job!
    .text:003517EC                 ld      [%fp+var_4_idx], %o2 ! Index?
    .text:003517F0                 call    %g1             ! 3323a0,user_crypt_filter,结果放在o1指的内容
    .text:003517F4                 nop
     
    那个call %g1是对解出的明文密码进行的一次变换。刚才抓到的六个字节EEB53723B248,其中EE是已经经过了第一次变换的结果,变换出来的结果和我们传入的伪造签名的第一个字节12不一致,因此出错。如果在003517F0之前下断查看byte_4DD515的内容,则可得到未经变换的第一个字节值。抓出的初始的正确签名值是C1B53723B248,但如果拿这个签名值去license文件里头试的话仍然通不过,因为这串签名值还会经过一次变换,这个变换便是臭名昭著的user_crypt_filter:用户滤波函数。
     
    二、分析用户滤波函数
     
    查FlexLM的资料得知,user_crypt_filter是对签名字符进行的进一步可逆变换机制,包括生成时的user_crypt_filter解码函数和user_crypt_filter_gen编码函数。我们的任务,是根据user_crypt_filter函数的实现,反推出user_crypt_filter_gen函数来。
    从代码中以及从大牛们的破解资料中可得知,user_crypt_filter 附近的调用机制基本上是:
     
    for (i = 0; i < j; i++) /* compare user-input and real checksums */
    {
      ......
      if (user_crypt_filter)
        (*user_crypt_filter)(job, &x, i, y[i]); // 使用了滤波函数进行进一步的对比!
       
      if (x != y[i])
        return 0;
    }
     
    而网上查到的很多中文文章中的FlexLM都是没用user_crypt_filter的,也就是user_crypt_filter为false而跳过(*user_crypt_filter)指针所指函数的变换,因此下面的x != y[i]便是正确签名字符与我们输入的伪造签名字符的明文比较。而我们碰到的情况是会调用user_crypt_filter的函数的。
     
    user_crypt_filter 的函数原型为:
    user_crypt_filter(job, char* KeySign, int index, char OurChar);
     
    其中job在函数体内基本无用。KeySign为传入的未变换的签名的地址(下面称为原始签名),index为此字符在密文中的位置,如签名为1234567890AB,则12的index为0,34的为1,以此类推。
     
    user_crypt_filter是逐个处理字符的,它根据未变换的原始签名字符、此字符的位置、以及我们传入的伪造签名字符进行一系列的运算返回一个字符,这个字符必须等于我们传入的伪造签名字符。这么说可能有点绕,换种说法就是:未变换的原始签名字符、此字符的位置、以及变换后的签名字符三者必须符合一定的运算法则,user_crypt_filter函数拿我们传入的伪造签名字符作为验证来和原始签名字符参与运算,如果运算通过,则表示伪造的签名字符是正确的变换后的签名字符,那么就将此正确的签名字符返回供上层代码(x != y[i])比较,其实此时已经无需比较了。
     
    可以看出,user_crypt_filter函数中不会出现我们需要的正确签名字符,只有当我们传入的伪造签名字符等于正确签名字符时才会验证通过,才会返回正确签名字符。下面我们要搞清楚的是user_crypt_filter函数究竟对未变换的原始签名字符、此字符的位置、以及我们传入的伪造签名字符三者进行了怎样的变换,从而努力反推出来如何根据未变换的原始签名字符以及此字符的位置计算正确的签名字符。
     
    user_crypt_filter函数非常长,从.text:003323A0 到.text:003374B8,反汇编代码有七千多行,很是吓人。不过经过大致浏览,可以看出它是根据传入的Idx的范围0到19有比较重复的20大段代码,20大段里头每大段会对字符进行异或并对其8位进行处理,共20加160处变换,搞明白了它做啥就好办了。
     
    首先是将传入的字符异或,比如我们传入的Idx是0的话:
     
    .text:003324E0                 ld      [$XADqrkBrM9j3y7__num0], %l0
    .text:003324E4                 cmp     %l1, %l0
    .text:003324E8                 bne     loc_332504
    .text:003324EC                 nop
    .text:003324EC
    .text:003324F0                 ldub    [%fp+var_2_GoodChar_xor], %l2
    .text:003324F4                 sethi   %hi(unk_496800), %l0
    .text:003324F8                 ldub    [$XADqrkBrM9j3y7__x_0], %l1 !
    // 取出num0对应的某常量值(x_0)和GoodChar一异或,存入局部变量 GoodChar_Xor。
    .text:003324FC                 xor     %l2, %l1, %l0
    .text:00332500                 stb     %l0, [%fp+var_2_GoodChar_xor]
    根据传入的Idx的不同,将未变换的原始签名字符和某一值进行异或,得到临时变量GoodChar_xor。
    然后启用另外一个Char变量,这里命名为TmpChar,用来挨个检查GoodChar_xor的位,比如这儿是检查其中一位:
     loc_332740:                             ! CODE XREF: user_crypt_filter+384j
    .text:00332740                 ld      [%fp+arg_4C_Idx], %l1
    .text:00332744                 sethi   %hi(unk_496800), %l0
    .text:00332748                 ld      [$XADqrkBrM9j3y7__num8], %l0
    .text:0033274C                 cmp     %l1, %l0
    .text:00332750                 bne     loc_3327BC // 如果此字符的index是8,那么检查
    .text:00332754                 nop
    .text:00332754
    .text:00332758                 ldub    [%fp+var_2_GoodChar_xor], %l2
    .text:0033275C                 sethi   %hi(unk_496800), %l0
    .text:00332760                 ld      [$XADqrkBrM9j3y7__bit3], %l1
    .text:00332764                 andcc   %l2, %l1, %l0
    .text:00332768                 be      loc_332784  // 如果异或后的字符的bit3位是1 则执行下面的,
    .text:0033276C                 nop
    .text:0033276C
    .text:00332770                 ldub    [%fp+var_1_TmpChar], %l2
    .text:00332774                 sethi   %hi(unk_496800), %l0
    .text:00332778                 ld      [$XADqrkBrM9j3y7__bit4], %l1 
    // 如果异或后的字符的bit3位是1,则将临时变量TmpChar的bit4置为1,
    .text:0033277C                 or      %l2, %l1, %l0
    .text:00332780                 stb     %l0, [%fp+var_1_TmpChar]
    .text:00332780
    .text:00332784
    .text:00332784 loc_332784:                             ! CODE XREF: user_crypt_filter+3C8j
    .text:00332784                 ldub    [%fp+var_1_TmpChar], %l1
    .text:00332788                 sethi   %hi(unk_496800), %l0
    .text:0033278C                 ld      [$XADqrkBrM9j3y7__bit4], %l2
    .text:00332790                 and     %l1, %l2, %l1
    .text:00332794                 ldub    [%fp+var_3_OurChar], %l0
    .text:00332798                 and     %l0, %l2, %l0
    .text:0033279C                 cmp     %l1, %l0
    .text:003327A0                 be      loc_3327BC // 然后检查我们伪造的签名字符的bit4,和TmpChar的bit4是否相同。
    .text:003327A4                 nop
    .text:003327A4
    .text:003327A8                 ld      [%fp+arg_48_charX], %l1
    .text:003327AC                 ldsb    [%l1], %l0
    .text:003327B0                 btog    -0x3E, %l0 // 在俩bit不同的情况下,返回垃圾值3E
    .text:003327B4                 ba      locret_3374B8
    .text:003327B8                 stb     %l0, [%l1]
    .text:003327B8
     
    以上是对Index为8时的未签名字符bit3位的判断检查过程:如果GoodChar_xor的bit3是1,则TmpChar的bit4也置一,因为TmpChar原始为0,因此bit4原始也为0,所以这一步就是把GoodChar_xor的bit3搬移到了TmpChar的bit4上。
    然后TmpChar的bit4和我们传入的伪造签名字符OurChar的bit4进行比较,相同的话这位检查通过,继续检查下一位,否则返回一个垃圾值-0x3E用以迷惑俺们。总的来说,这步就是检查异或后的GoodChar_xor的bit3是否等于伪造签名字符的bit4,等的话才通过。
    参考Nolan Blender的文章,这里检查一个bit的过程用C代码描述如下(这里是index为0时检查GoodChar_xor的bit2与TmpChar的bit5是否相等的情况):
     
    if (idx == num0)
    {
      if (GoodChar_xor & bit2) TmpChar |= bit5;
      // 如果inc_c的2置位,那么把另外一个C的5置位,相当于2位换到5位去
      if ((TmpChar & bit5) != (OurChar & bit5)) { *KeySign = 0xxx; return; // 不等则返回垃圾}
      // 其实就是GoodChar_xor的bit2要和输入签名字符的bit5相等。
    }
     
    以下还有很多类似的检验,对于每一个index,都要检查GoodChar_xor的8个bit和OurChar的8个bit是否相同,但这8个bit并不是顺序上一一对应,而是乱序(permute)了。比如GoodChar_xor的bit0应该等于OurChar的bit3、GoodChar_xor的bit2必须等于OurChar的bit7,等等。
     
    由此可知,只要将GoodChar_xor按user_crypt_filter中特定的index处所指明的bit置换规则,把各个bit换一下,则可得到解密后的明文签名字符。这也就是说,如果我们在代码中找到了针对一个index的字符的八条位置换规则,如GoodChar_xor的bit0应该等于OurChar的bit3、GoodChar_xor的bit2应该等于OurChar的bit7等共八条,那么我们只要新起一个变量TmpChar,将GoodChar_xor的bit0放到变量TmpChar的bit3,GoodChar_xor的bit2换到TmpChar的bit7,换过8个bit后,这个TmpChar的每一位必然等于解密后的明文签名字符OurChar。这就是user_crypt_filter检查运算的逆运算!
     
    三、编写逆运算程序
     
    逆运算分析出来了,就可以写还原程序了。
    这儿参考了Nolan Blender的思想,将0到19个index,每个index所对应的8位bit置换规则写成一个大数组,共一百六十项,从七千多行汇编代码中整理出来相当费力,但所幸只是体力活儿。而且如果是短的签名的话,只要搜集0到5的index所对应的四十八条bit置换规则即可。
     
    以下代码用Delphi实现:
     
    type
      TShiftVals = array[0..7] of Integer;
      TPermute = packed record
        shiftvals: TShiftVals;
      end;
     
    PerTab: array[0..19] of TPermute = // 搜集得到的位置换规则数组
    (           // BIT  0 1 2 3 4 5 6 7
           (shiftvals: (0,4,5,3,1,2,6,7)), // idx 00 //
           (shiftvals: (4,0,5,2,1,3,6,7)), // idx 01 //
           (shiftvals: (7,1,3,4,0,5,2,6)), // idx 02 //
           (shiftvals: (4,7,3,6,1,5,2,0)), // idx 03 //
           (shiftvals: (0,3,7,4,2,5,6,1)), // idx 04 //
           (shiftvals: (4,3,5,6,7,0,1,2)), // idx 05 //
           (shiftvals: (2,7,4,0,6,5,3,1)), // idx 06 //
           (shiftvals: (4,5,1,7,0,3,6,2)), // idx 07 //
           (shiftvals: (1,0,5,4,3,6,7,2)), // idx 08 //
           (shiftvals: (2,7,5,3,0,6,1,4)), // idx 09 //
           (shiftvals: (3,5,7,0,6,4,2,1)), // idx 10 //
           (shiftvals: (7,6,5,0,4,3,2,1)), // idx 11 //
           (shiftvals: (3,4,5,6,0,2,1,7)), // idx 12 //
           (shiftvals: (0,4,6,3,5,2,1,7)), // idx 13 //
           (shiftvals: (1,5,0,2,6,3,4,7)), // idx 14 //
           (shiftvals: (2,6,5,7,4,3,1,0)), // idx 15 //
           (shiftvals: (2,3,0,5,1,7,6,4)), // idx 16 //
           (shiftvals: (1,7,4,2,3,0,6,5)), // idx 17 //
           (shiftvals: (1,5,2,3,4,7,6,0)), // idx 18 //
           (shiftvals: (3,0,6,7,4,5,2,1))  // idx 19 //
    );
     
    如 (shiftvals: (0,4,5,3,1,2,6,7)), // idx 00 //,表示对于index为0的字符,其bit0位置换到bit0位(两位可以相同),bit1置换到bit4,bit2置换到bit5,等等以此类推。
     
    另外还有一次异或,此异或的数字也是常数,根据index不同而不同,是上文代码中的x_0等形式的变量。总结得出一个异或数组:
     
    var
      xorvals: array[0..19] of Integer =
      (
        $1E, $16, $3E, $24,
        $04, $1E, $1E, $13,
        $15, $0C, $2D, $33,
        $3D, $21, $26, $2E,
        $12, $34, $01, $2B
      );
     
    然后写一个解密函数与bit置换函数(参考了:Nolan Blender的思想)
     
    // 传入计算来的原始的签名字符与位置,返回变换后的正确签名字符
    function user_crypt_filter_gen(inchar: Char; idx: Integer): Char;
    var
      tmpchr: Char;
    begin
      tmpchr := Chr(Ord(inchar) xor xorvals[idx]); // 先异或还原
      Result := Permute(tmpchr, PerTab[idx].shiftvals); // 再置换位置
    end;
     
    // 将传入字符按大数组表内的规则进行置换位置
    function permute(inchar: Char; shiftvals: TShiftVals): Char;
    var
      outval: Integer;
      i: Integer;
      shbit: Integer; // Test bit */
    begin
      outval := 0;
      shbit := 1;
      for i := 0 to 7 do
      begin
        if ((Ord(inchar) and shbit) <> 0) then
        begin
          outval := outval or (1 shl (shiftvals[i]));
        end;
        shbit := shbit shl 1;
      end;
      Result := Chr(outval and $FF);
    end;
     
    然后,利用上面两个写好的函数对抓出的未变换签名值C1B53723B248进行运算:
     
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Edit1.Text := IntToHex(Ord(user_crypt_filter_gen(#$C1, 0)), 2)+
        IntToHex(Ord(user_crypt_filter_gen(#$B5, 1)), 2) +
        IntToHex(Ord(user_crypt_filter_gen(#$37, 2)), 2) +
        IntToHex(Ord(user_crypt_filter_gen(#$23, 3)), 2) +
        IntToHex(Ord(user_crypt_filter_gen(#$B2, 4)), 2) +
        IntToHex(Ord(user_crypt_filter_gen(#$48, 5)), 2);
    end;
     
    运行程序后,Edit1.Text中得到生成的正确签名值FB999098AEAA。拿这串签名填入license文件中,测试通过。
     
    再次跟踪user_crypt_filter可知,比如对index为0的情况,传入原始字符C1与index 0,C1与1E异或后的值经过八次位置换得到FB,和我们传入的FB相等,因此通过,返回FB(只要有一位不等,就会返回垃圾值,外部的比较必然通不过)。user_crypt_filter外部再用返回的FB与我们传入的FB比较,自然相等,也就通过了。
     
    四、总结
     
    user_crypt_filter的核心是字符的位置换规则,需要通篇阅读代码以总结出一百六十个位置换规则来,这一点比较的耗体力并且不能出错,否则通不过的情况下再次调试就需要跟入七千多行的user_crypt_filter以找到出错点,这体力耗费的就不是总结位置换规则所能比的了。俺在总结过程中还好没在位置换规则中出错,但搞错了一位xorvals中的异或值,事后查起来也耗了一点力气,等到所幸最后还是找出来的时候已经快累趴下了。
     
    参考资料:
    http://www.woodmann.com/crackz/Tutorials/Nbufilt.htm
    laoqian的《制作Flexlm license总结》的文章
    laowanghai的《LabWindows CVI 8.0》
    其他看雪论坛的精华文章
    http://www-curri.u-strasbg.fr/documentation/calcul/doc/ProPack/3SP1/docs/doc/lmsgi-9.2.3/flexref/chap21.htm
    April 23

    骂一通无知的粪青和无耻的冷漠者

     
    最近网络不平静的很,垃圾消息充斥着QQ与MSN,一群群粪青的本性在垃圾中暴露无遗:冲动、无知、排异、意淫。喊抵制是弱者的叫嚣,比弱者的叫嚣更可悲的是认识不到自己是弱者。敏感的自尊受了刺激却又找不到藏独分子出气,于是拿一家本地化已经相当严重的超市开刀,且不说打砸抢的提议本身就可笑,即使有那么一批稍许清醒的人提倡静静的抵制,这种提议也被淹没在积极购物不付款等损毁性的馊主意中。随着恶意降临的还有无知与意淫,什么捏造的令人呕吐的李白的诗,什么法国政府拨款二千万美金,无数缺乏起码思考能力的粪青们被利用了还不自知,还在积极无偿地宣传家乐福的五一促销活动。更无知的还有老一套的网站攻击,先是南联盟时期发动网民Ping美国,再是现在发动网民刷新CNN网站,中国那点儿可怜的出口带宽被挤得一塌糊涂,只平白无故替对手增加了访问量,没准其Alexa排名还会因此上升。——我们的网民啊,我们的年轻人啊,我们什么时候才能冷静一点地思考?有些人MSN/QQ好友栏里头明明没一个外国人,却叫嚣着改昵称改头像让全世界看华人的团结?中国在世界上的地位很低四处是敌,要获得敌人的尊重比从敌人处得到嘲弄和蔑视难得多。现在的闹剧,你们以为得到的是什么?一盘散沙是粪青的最大特征,从来没法凝聚起来做实事,从来以为一时的口眼之快就是爱国。中国这位母亲需要的是每一个位儿女的踏实奉献,而不是粉墨登场地——事实上,登场的档次根本算不上,除非真去打砸抢——出演让国内外都感到可笑的闹剧。一边体育不要和政治挂钩,一边经济和政治又在挂钩,中国是美国国债的第二大股东,中东地区有没闹着抵制俺们的加班中兴与跳楼华为?如果你说抵制日货是习惯没法改了,那么以前为抵制日货而买美法等货的人怎么现在也成了粪青的眼中钉人中奸?和自己的切身利益无关的人容易跟着瞎起哄,起哄完毕一转身继续屁颠屁颠地照样吃吃买买地亲美亲法,这就是俺们的抵制?
     
    第二类该骂的是那些貌似冷静实则冷漠的无耻者。国家不关你的事,民族不关你的事,奥运不关你的事,就钱关你的事。如果您是低保户无法糊口,这种现象俺能理解,可你丫还不是装白领上班上网玩MSN?你可以问国家民族是什么,可以问如果是一块面包它有多大,也可以问如果是一件衣服它有多暖和,更可以问如果是一间房子能不能为我们挡住风雨,但你又是什么?你是游荡的无业民?你是吃喝的啃老族?还是说你是只会索取不会奉献的寄生虫?一个国家不能只靠雇佣军战斗,一个青年也不能只靠冷漠来活着。别告诉你丫不想做青年,五四歇半天丫还不是屁颠屁颠地羡慕得捶胸顿足?死水一样的冷漠者和洪水一样的粪青同样可恶,后者是无知,前者则是无耻。不是一般的无耻,还是很黄很暴力很傻很天真很不和谐的无耻!
     
    骂完了,该干啥还干啥。请随便对号入座来着。
    April 16

    早晨路上联诗

    笑死俺了,原来室友们都这么有才:
     
    上海四月雨茫茫,
    吃喝嫖赌诸事忙。
    三把小伞上班去,
    到了路上变色狼。
    牛仔包臀弹性好,
    黑发披肩瀑布长。
    两只手套玩细腻,
    一根宽粉笑疯狂。
    April 02

    迎风喝豆花的恶果

    近日早餐比较喜欢吃摊韭菜煎蛋饼与咸豆腐花,时常伙同俩室友晚起坐在那儿饕餮而不管是否迟到。只是卖豆花的老板不知道没钱租场地了还是怎么的,原先的店面愣是锁着空着,只在外头路边摆下一长条木桌供人挤坐着匆忙地吃。这天一早风很大,照例俺们仨挤在桌子角边,吃得差不多时,对面的一女的没吃完就不厚道的起身离开,那挡风的身板一走,她没喝完的豆花碗被风一吹哗啦就朝这边盖过来洒了俺们一腿,我靠!悻悻然地放下餐具找纸来擦,刚放下,室友自己没喝完的豆花碗被风一吹哗啦又朝这边盖过来又洒了俺们一身!我靠!俺们赶紧站起身来胡乱擦,这时,第三个俺的没喝完的豆花碗被风一吹哗啦啦地又盖了过来……