首页
社区
课程
招聘
[原创]独行孤客CrackMe-第五题的writeup(一路的艰辛)
2017-6-11 01:02 3176

[原创]独行孤客CrackMe-第五题的writeup(一路的艰辛)

2017-6-11 01:02
3176

程序的特点:MFC程序,反调试,隐蔽的驱动文件,驱动反调试等。


1 MFC程序的特点

MFC程序使用了大量的深度封装的类,这些类除了结构复杂的特点,还有虚函数。虚函数支持了C++的多态性。在C++内存模型中,如果想访问一个对象的虚函数,需要做两件事,首先判断该对象的类型(找到虚函数表,对象前一个DWORD指向该虚表),然后根据索引访问虚表中的函数。这意味着大量使用虚函数和继承的MFC程序有很多张虚表,每有一个实现了虚函数的类,就可能有一张虚表。在MFC中,对虚函数很少有直接的访问,通常是通过虚表地址加上偏移来计算实际调用地址,很不直观。

幸运的是该MFC程序没有在虚函数上绕弯路,尽管如此,深入分析仍然很困难。


2 隐蔽的驱动文件

MFC仍然是一个win32程序,离不开消息事件的驱动,离不开窗口过程的处理。

使用IDA打开程序,没有受到任何阻挠,来到 0x40C14E处的 call _WinMain@16,有兴趣的可以阅读CWinApp类的源码,CWinApp类封装了win32程序的几个关键函数。

连续跟随调用地址,便会看到几个头疼的虚函数调用。

.text:0041C6EC                 call    dword ptr [eax+84h]
.text:0041C6FA                 call    dword ptr [eax+50h]
.text:0041C711                 call    dword ptr [eax+58h]
.text:0041C711                 call    dword ptr [eax+68h]

上面的eax便是虚函数表的地址,仅靠代码是无法知道这些对应什么函数调用。

当程序运行在非xp系统时,提示“请在xp平台中重新运行CrackMe",搜索关键字"xp",然后ctrl+x找到唯一引用,来到sub_4013E0。

快速浏览一下,调用了GetSystemMenu和AppendMenuA(创建系统菜单相关的调用),和SendMessageA(msg=80h,查找得知是让图标与窗口关联的消息),还有PostMessageA(msg=12h,让程序退出的消息)

在程序退出提示框之前有一个call sub_402210,判断是否是符合的xp系统。

.text:004014BE                 mov     ecx, ebp
.text:004014C0                 call    sub_402210                           ;判断是否是xp系统
.text:004014C5                 mov     ebx, ds:PostMessageA
.text:004014CB                 test    eax, eax
.text:004014CD                 jnz     short loc_4014E9
.text:004014CF                 push    0               ; int
.text:004014D1                 push    0               ; uType
.text:004014D3                 push    offset Text     ; "请在xp平台中重新运行CrackMe"
.text:004014D8                 call    sub_41DB51
.text:004014DD                 mov     edx, [ebp+1Ch]
.text:004014E0                 push    0               ; lParam
.text:004014E2                 push    0               ; wParam
.text:004014E4                 push    12h             ; Msg
.text:004014E6                 push    edx             ; hWnd
.text:004014E7                 call    ebx ; PostMessageA

如果是xp系统,然后 jnz short loc_4014E9

.text:004014E9 loc_4014E9:                             ; CODE XREF: sub_4013E0+EDj
.text:004014E9                 lea     eax, [esp+12Ch+Buffer]
.text:004014ED                 push    eax             ; lpBuffer
.text:004014EE                 push    104h            ; nBufferLength
.text:004014F3                 call    ds:GetCurrentDirectoryA                          ;获取当前工作目录
.text:004014F9                 mov     edi, offset aVmxdrv_sys ; "\\vmxdrv.sys"

经过各种字符串的操作后, jnz     short loc_401587

.text:00401587 loc_401587:                             ; CODE XREF: sub_4013E0+18Dj
.text:00401587                 lea     edx, [esp+12Ch+Buffer]
.text:0040158B                 mov     ecx, ebp
.text:0040158D                 push    edx             ; lpFileName
.text:0040158E                 push    offset ServiceName ; "vmxdrv"
.text:00401593                 call    sub_401AA0                     ;这是一个重要的函数
.text:00401598                 test    eax, eax
.text:0040159A                 mov     [ebp+64h], eax
.text:0040159D                 jnz     short loc_4015C5

细细分析一下 sub_401AA0

.text:00401AC6                 lea     eax, [esp+10Ch+Buffer]
.text:00401ACA                 push    eax             ; lpBuffer
.text:00401ACB                 push    100h            ; nBufferLength
.text:00401AD0                 push    ecx             ; lpFileName
.text:00401AD1                 call    ds:GetFullPathNameA                 ;获取指定文件的全路径
.text:00401AD7                 push    0F003Fh         ; dwDesiredAccess
.text:00401ADC                 push    0               ; lpDatabaseName
.text:00401ADE                 push    0               ; lpMachineName
.text:00401AE0                 call    ds:OpenSCManagerA                   ;连接到服务控制管理器的数据库,并获得可创建服务的权限
.text:00401AE6                 mov     ebp, eax
.text:00401AE8                 test    ebp, ebp
.text:00401AEA                 jnz     short loc_401B0D
.text:00401AEC                 call    ds:GetLastError
.text:00401AF2                 push    eax
.text:00401AF3                 push    offset aOpenscmanagerF ; "OpenSCManager() Faild %d ! \n"
.text:00401AF8                 call    sub_40B946

.text:00401B0D loc_401B0D:                             ; CODE XREF: sub_401AA0+4Aj
.text:00401B0D                 push    ebx
.text:00401B0E                 push    esi
.text:00401B0F                 push    offset aOpenscmanagerO ; "OpenSCManager() ok ! \n"
.text:00401B14                 call    sub_40B946
.text:00401B19                 add     esp, 4
.text:00401B1C                 mov     edi, [esp+110h+lpServiceName]
.text:00401B23                 lea     edx, [esp+110h+Buffer]
.text:00401B27                 push    0               ; lpPassword
.text:00401B29                 push    0               ; lpServiceStartName
.text:00401B2B                 push    0               ; lpDependencies
.text:00401B2D                 push    0               ; lpdwTagId
.text:00401B2F                 push    0               ; lpLoadOrderGroup
.text:00401B31                 push    edx             ; lpBinaryPathName
.text:00401B32                 push    0               ; dwErrorControl
.text:00401B34                 push    3               ; dwStartType
.text:00401B36                 push    1               ; dwServiceType
.text:00401B38                 push    0F01FFh         ; dwDesiredAccess
.text:00401B3D                 push    edi             ; lpDisplayName
.text:00401B3E                 push    edi             ; lpServiceName
.text:00401B3F                 push    ebp             ; hSCManager
.text:00401B40                 call    ds:CreateServiceA                 ;创建服务,相当于执行sc create ServiceName binPath= BinaryPathName
                                                                        DisplayName=displayName   type= kernel start= demand

.text:00401B77 loc_401B77:                             ; CODE XREF: sub_401AA0+B9j
.text:00401B77                                         ; sub_401AA0+C0j
.text:00401B77                 push    offset aCrateservice_0 ; "CrateService() Faild Service is ERROR_I"...
.text:00401B7C                 call    sub_40B946
.text:00401B81                 add     esp, 4
.text:00401B84                 push    0F01FFh         ; dwDesiredAccess
.text:00401B89                 push    edi             ; lpServiceName
.text:00401B8A                 push    ebp             ; hSCManager
.text:00401B8B                 call    ds:OpenServiceA                  ;如果创建服务失败,可能是已经存在该服务,然后尝试打开该服务
.text:00401B91                 mov     esi, eax
.text:00401B93                 test    esi, esi
.text:00401B95                 jnz     short loc_401BAB
.text:00401B97                 call    ebx ; GetLastError
.text:00401B99                 push    eax
.text:00401B9A                 push    offset aOpenserviceFai ; "OpenService() Faild %d ! \n"
.text:00401B9F                 call    sub_40B946
.text:00401BA4                 add     esp, 8
.text:00401BA7                 xor     edi, edi
.text:00401BA9                 jmp     short loc_401C13

突然意识到创建服务传入的BinaryPathName=...\vmxdrv.sys在哪里,原来在 sub_401F20处创建了一个驱动文件在

.text:00401566               call    sub_401F20 

下一行指令下断点,在该程序当前目录下会找到一个vmxdrv.sys驱动文件。

第一次分析驱动文件,经过查找相关资料,得知访问驱动文件,需要一下几个关键函数 CreateFile,DeviceIoControl,WriteFile,ReadFile。

CreateFile是通过"\\.\vmxdrv"作为文件名访问服务的,DeviceIoControl传入控制码,而WriteFile和ReadFile是从驱动设备写入和读取数据。

找到DeviceIoControl的两个调用 sub_4022A0 和sub_401D50。

简单分析下sub_4022A0,发现该调用只是简单发送一个0x222004H的IoControlCode,调用该过程的父调用恰好位于上述的打开vmxdrv服务之后,而且调用了5次该过程。

接下来分析sub_401D50,经一下分析,得知该过程的作用就是往驱动设备发送输入的字符串,然后读取输出,并将输出的16个字节变成长度为32的HexString。

.text:00401E61                 lea     edx, [esp+33Ch+NumberOfBytesRead]
.text:00401E65                 push    ebx             ; lpOverlapped
.text:00401E66                 push    edx             ; lpNumberOfBytesRead
.text:00401E67                 lea     eax, [esp+344h+encrypted]
.text:00401E6E                 push    10h             ; nNumberOfBytesToRead
.text:00401E70                 push    eax             ; lpBuffer            ;读取10h个字节,并使用lpBuffer接收
.text:00401E71                 push    edi             ; hFile
.text:00401E72                 call    ds:ReadFile
.text:00401E78                 lea     ecx, [esp+33Ch+encrypted]
.text:00401E7F                 push    10h
.text:00401E81                 lea     edx, [esp+340h+transformed]
.text:00401E85                 push    ecx
.text:00401E86                 push    edx
.text:00401E87                 call    HexString                             ;这是一个Byte2HexString,在反调试讲述
.text:00401E8C                 add     esp, 0Ch
.text:00401E8F                 mov     eax, [eax+4]
.text:00401E92                 mov     [esp+33Ch+var_4], ebx

该过程的caller分析如下

.text:004017C2                 mov     eax, [eax+4]
.text:004017C5                 cmp     dword ptr [ecx-8], 6                                               ;输入的字符串长度需要满足为6
.text:004017C9                 jnz     loc_4018C2                                                         ;这是一个什么都不做的分支跳转
.text:004017CF                 mov     ecx, eax
.text:004017D1                 call    IsDebuggerPresent                                                  ;调试标志
.text:004017D6                 test    eax, eax
.text:004017D8                 jnz     loc_4018C2
.text:004017DE                 mov     eax, [esi+64h]
.text:004017E1                 test    eax, eax
.text:004017E3                 jz      short loc_401802
.text:004017E5                 mov     edx, [esp+2Ch+var_20]
.text:004017E9                 lea     ecx, [esp+2Ch+var_20]
.text:004017ED                 mov     eax, [edx-8]
.text:004017F0                 push    eax             ; size_t
.text:004017F1                 push    0
.text:004017F3                 call    sub_418263                                                                  
.text:004017F8                 push    eax             ; char *
.text:004017F9                 mov     ecx, esi
.text:004017FB                 call    Input2HexString
.text:00401800                 jmp     short loc_401812
.text:00401812 loc_401812:                                      ; CODE XREF: sub_401760+A0j
.text:00401812                 lea     ecx, [esp+2Ch+var_1C]
.text:00401816                 lea     edx, [esi+5Ch]
.text:00401819                 push    ecx
.text:0040181A                 push    ecx
.text:0040181B                 mov     ecx, esp
.text:0040181D                 mov     [esp+34h+var_14], esp
.text:00401821                 push    edx
.text:00401822                 call    sub_417D43
.text:00401827                 mov     ecx, esi
.text:00401829                 call    sub_401920
.text:0040182E                 push    0Ah             ; size_t
.text:00401830                 lea     eax, [esp+30h+var_18]
.text:00401834                 push    2               ; int
.text:00401836                 push    eax             ; int
.text:00401837                 lea     ecx, [esp+38h+var_1C]
.text:0040183B                 call    sub_415A78                                                  ;这是一个MD5处理过程,根据几个常量特征可以快速得知
.text:00401840                 lea     ecx, [esp+2Ch+var_18]
.text:00401844                 mov     [esp+2Ch+var_8], 2
.text:00401849                 call    sub_4182FA
.text:0040184E                 lea     ebp, [esi+6Ch]
.text:00401851                 push    offset byte_431398 ; lpString
.text:00401856                 mov     ecx, ebp        ; this
.text:00401858                 call    ??4CString@@QAEABV0@PBD@Z ; CString::operator=(char const *)
.text:0040185D                 push    offset byte_431398 ; lpString
.text:00401862                 mov     ecx, edi        ; this
.text:00401864                 call    ??4CString@@QAEABV0@PBD@Z ; CString::operator=(char const *)
.text:00401869                 push    0
.text:0040186B                 mov     ecx, esi
.text:0040186D                 call    sub_41A4F7
.text:00401872                 mov     ecx, [esp+30h+var_1C]
.text:00401876                 push    offset a888aeda4ab ; "888aeda4ab"                            ;将加密后的字符串与"888aeda4ab"比较
.text:0040187B                 push    ecx             ; unsigned __int8 *
.text:0040187C                 call    __mbsicmp
.text:00401881                 add     esp, 8
.text:00401884                 test    eax, eax
.text:00401886                 jnz     short loc_401891
.text:00401888                 mov     ecx, esi
.text:0040188A                 call    sub_402030                                                                  ;输出success


3分析驱动文件

驱动文件的入口是DriverEntry,尽量参考一些驱动资料。

通常在加载驱动结束前有重要的一个MajorFunction。

      memset32(DriverObject->MajorFunction, (int)sub_106EC, 0x1Bu);
      DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_106C8;          ;IRP_MJ_CREATE, sub_106C8是默认的分发处理 IRP_DISPATCH
      DriverObject->MajorFunction[3] = (PDRIVER_DISPATCH)sub_105A8;          ;IRP_MJ_READ
      DriverObject->MajorFunction[4] = (PDRIVER_DISPATCH)sub_1061C;          ;IRP_MJ_WRITE
      DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)sub_1071A;         ;IRP_MJ_DEVICE_CONTROL
      DriverObject->MajorFunction[18] = (PDRIVER_DISPATCH)sub_106C8;         ;IRP_MJ_CLEANUP 
      DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)sub_106C8;          ;IRP_MJ_CLOSE (来自wdm.h)
      DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_10564;

再来分析 sub_1071A 设备控制处理

int __stdcall sub_1071A(int a1, PIRP Irp)
{
  switch ( *(_DWORD *)(Irp->Tail.Overlay.PacketType + 12) )
  {
    case 0x222004:                                                  ;前几个小节提到DeviceIoControl API的几个调用控制码都是0x222004
      dword_114D8 = 1;                                              ;这是一个流程控制标志,俗称暗桩
      dword_114E0 = (int)IoGetCurrentProcess();
      sub_10486();                                                  ;反调试
      break;
    case 0x222008:
      DbgPrint("%s\n", Irp->AssociatedIrp.IrpCount);
      break;
    case 0x22200C:
      DbgPrint("bye!\n");
      break;
    default:
      DbgPrint("unkonw code!\n");
      break;
  }
  Irp->IoStatus.Status = 0;
  Irp->IoStatus.Information = 0;
  IofCompleteRequest(Irp, 0);
  return 0;
}
PEPROCESS sub_10486()
{
  PEPROCESS result; // eax@1
  struct _EPROCESS *v1; // edx@1
  result = IoGetCurrentProcess();
  v1 = result;
  while ( result != (PEPROCESS)dword_114E0 )
  {
    result = (PEPROCESS)(*((_DWORD *)result + 34) - 136);
    if ( result == v1 )
      return result;
  }
  *((_DWORD *)result + 47) = 0;                                    ;and     dword ptr [eax+0BCh], 0, 0BCH DebugPort(参考PEPROCESS)置零
  return result;
}

然后分析输入处理 sub_1061C

int __stdcall sub_1061C(int a1, PIRP Irp)
{
  PIRP v2; // esi@1
  size_t v3; // edi@1
  PVOID v4; // eax@1
  void *v5; // ebx@1
  int result; // eax@2
  struct _IRP *Irpa; // [sp+18h] [bp+Ch]@1
  v2 = Irp;
  Irpa = Irp->AssociatedIrp.MasterIrp;
  v3 = *(_DWORD *)(v2->Tail.Overlay.PacketType + 4);
  v4 = ExAllocatePoolWithTag(PagedPool, *(_DWORD *)(v2->Tail.Overlay.PacketType + 4), 0x4C4D544Eu);
  v5 = v4;
  if ( v4 )
  {
    memset(v4, 0, v3);
    memcpy(v5, Irpa, v3);                                        ;v5是输入的字符串
    if ( dword_114D8 )                                           ;这个标志来自于Device Control, 经前文分析,程序中调用deviceiocontrol后,dword_114D8置1
    {
      sub_104B6(v5, (int)byte_114C8);                            ;在该函数中,先经过简单的处理后,进行MD5加密
      dword_114DC = 1;                                           ;加密完成后,dword_114DC置1
    }
    ExFreePoolWithTag(v5, 0);
    v2->IoStatus.Status = 0;
    v2->IoStatus.Information = v3;
    IofCompleteRequest(v2, 0);
    DbgPrint("DispatchWrite called\n");
    result = 0;
  }
  else
  {
    v2->IoStatus.Information = 0;
    v2->IoStatus.Status = -1073741670;
    IofCompleteRequest(v2, 0);
    result = -1073741670;
  }
  return result;
}
sub_104B6:
  if ( result <= 16 )
  {
    memcpy(&v6, a1, result);
    v4 = 0;
    if ( dword_114D8 )
      ++v6;
    if ( v3 > 0 )
    {
      do
      {
        *(&v6 + v4) += v4;                                       ;byte[6]分别与byte[6]{1,1,2,3,4,5}相加
        ++v4;  
      }
      while ( v4 < v3 );
    }
    sub_108B2(&v5);
    sub_11124((int)&v5, &v6, strlen(&v6));                       ;MD5加密,该加密算法非常复杂,是通过几个常量特征在网上搜索得知
    result = sub_111CE(&v5, a2);
  }

最后分析读取处理

int __stdcall sub_105A8(int a1, PIRP Irp)
{
  struct _IRP *v2; // edi@1
  signed int v3; // edx@2
  int v4; // edi@5
  v2 = Irp->AssociatedIrp.MasterIrp;
  if ( !dword_114DC )                                            ;如果没有加密,始终输出这些值 byte_114C8是4个DWORD,16个字节
  {
    v3 = 3;
    do
    {
      byte_114C8[v3] = 3 * v3 - 100;
      ++v3;
    }
    while ( v3 < 16 );
    byte_114C8[0] = -53;
    byte_114C9 = -86;
    byte_114CA = -34;
    byte_114CB = -80;
  }
  *(_DWORD *)&v2->Type = *(_DWORD *)byte_114C8;               ;如果已经加密,使用4个DWORD复制
  v4 = (int)&v2->MdlAddress;
  *(_DWORD *)v4 = *(_DWORD *)&byte_114C8[4];
  v4 += 4;
  *(_DWORD *)v4 = *(_DWORD *)&byte_114C8[8];
  *(_DWORD *)(v4 + 4) = *(_DWORD *)&byte_114C8[12];
  Irp->IoStatus.Status = 0;
  Irp->IoStatus.Information = 16;
  IofCompleteRequest(Irp, 0);
  return 0;
}

经过以上简单分析,驱动文件的算法如下:

1 接受IoControlCode=222004H后,设置dword_114D8=1,并使DebugPort清零

2 接受输入时,当dword_114D8=1时,先讲字符串与byte{1,1,2,3,4,5}对应相加,然后使用MD5加密,最后设置dword_114DC=1

3 处理读取时,判断dword_114DC=1,如果为1,则输出加密后的16个字节,否则固定输出某些字节

隐藏的dword_114E0,存放IoGetCurrentProcess()返回值,猜测和多线程有关。

而MFC程序的加密算法大致如下:

1 接受6位字符串输入

2 向驱动设备输入该6位字符串

3 读取加密处理后的16个字节,然后转成HexString

4 再次HexString进行MD5加密处理,再次转成HexString

5 讲HexString与"888aeda4ab"比较,似乎有些不对劲,32长度的HexString如何与10长度的比较,肯定还有一些不清楚的地方。


4反调试

1 IsDebuggerPresent 这是一个很常见的反调试。

如果eax=1,则跳到loc_4018C2。

由于这个函数不需要任何参数,所以无需调整堆栈,分别使用5个nop指令和一个xor eax, xor eax替换掉call 和 test指令

.text:004017D1                 call    IsDebuggerPresent
.text:004017D6                 test    eax, eax
.text:004017D8                 jnz     loc_4018C2

2 头疼的DeviceIoControl

当在调试该程序时,运行到222004H设备控制码时便会抛出内核访问异常,程序无法正常调试。

本来想对驱动文件patch,不让DebugPort置零,然后创建服务。但是这会导致驱动校验无法通过。

观察到除了222004H设备控制码外,还有一个222008H,在修改驱动文件失败后,只能转向修改MFC程序,缺陷就是发送222004H控制码,导致输出的结果只有一种,无法确认对驱动的分析是否正确。

接下来修改MFC程序的几处DeviceIoControl调用,ptach程序,将设备控制码改成222008H。


5使用OD调试MFC程序

果然没有让人头疼的内核异常了。

1.在 0x004017FB( call    Input2HexString )处下断点

输入字符串ABCdef,

观察栈顶的指针指向的内容,发现并非是ABCdef,居然是fedcba,多试几次可以确认是未发现的转小写和翻转字符串的处理。

2.在 0x00401E78(call    ds:ReadFile指令之后)处下断点

得到输出为16个字节, 0xCB, 0xAA, 0xDE, 0xB0,0xA8,0xAB,0xAE, 0xB1,0xB4,0xB7,0xBA,0xBD,0xC0,0xC3,0xC6,0xC9 

3在0x00401E87(call    sub_403200)处下断点

步进观察最佳,可以发现hexstring的踪迹 cbaadeb0a8abaeb1b4b7babdc0c3c6c9

4在0x0040183B(call    sub_415A78)处下断点

仍然是步进观察最佳,可以发现hexstring的踪迹 c8ebbe345d3b2e7d0b60748aa182d69e

发现该过程除了再次MD5加密外,还将3-12位置的字符串取出来

5在0x0040187B(push    ecx)处下断点

发现需要比较的内容果然是ebbe345d3b。

可以通过改变call ds:ReadFile返回的十六个字节的内容来对程序这端的加密过程进行确认。

6 最后写算法穷举

算法过程就是

1 将输入的6位字符串转小写

2 颠倒字符串

3 在驱动程序里,将6位字符串分别与{1,1,2,3,4,5}对应位置的数字相加

4 在驱动程序里MD5处理

5 HexString处理,再次MD5处理,还有再次HexString处理

6 取出3-12位的字符串并与888aeda4ab比较。


双核4线程,穷举时间小于15分钟。

成果不敢独享,默默感谢群里几位大牛的提示和帮助。

附上穷举代码,多线程,Java代码

public class CM5 {
    public static void main(String[] args) throws Exception {
        long cnt = 2176782336L;
        int threadNum = 4;
        for(int i = 0;i< threadNum;i++){
            new RoutineThread(cnt*(i+1)/4,cnt*(i+1)/4).start();
        }
    }
    static class RoutineThread extends Thread{
        MessageDigest md5= MessageDigest.getInstance("MD5");
        long start, end;

        RoutineThread(long start, long end) throws Exception {
            this.start = start;
            this.end = end;
        };
        public void run(){
            byte [] inc = new byte[]{1,1,2,3,4,5};
            for(long i = start; i < end; i ++){
                byte[] bytes = Long.toString(i,36).getBytes();
                int displacement = bytes.length - 6;
                byte[] origin = new byte[]{0x30,0x30,0x30,0x30,0x30,0x30};
                for(int m = 5; m >= 6 - bytes.length; m--){
                    origin[m] = (byte) (bytes[m + displacement] + inc[m]);
                }
                String dst = bytesToHexString(md5.digest(bytesToHexString(md5.digest((origin))).getBytes())).substring(2,12);
                if(dst.equals("888aeda4ab")){
                    System.out.println(new String(bytes));
                    //输出6891us
                }
            }
        }
    }
    private static  String bytesToHexString(byte[] buf) {
        StringBuilder sb = new StringBuilder(buf.length * 2);
        String tmp = "";
        for (int i = 0; i < buf.length; i ++) {
            tmp = Integer.toHexString(0xff & buf[i]);
            tmp = tmp.length() == 1 ? "0" + tmp : tmp;
            sb.append(tmp);
        }
        return sb.toString();
    }
}

 





[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回